Source: lib/polyhedron.js

/** @module polyhedron */

/**
 *  @file
 *
 *  Summary.
 *
 * <p>Creates the model of a sphere by continuously subdividing an initial
 * {@link https://en.wikipedia.org/wiki/Regular_polyhedron convex regular polyhedron}.</p>
 *
 * The algorithm starts with just four, six, twenty, or twelve points, corresponding
 * to a {@link https://www.brainsofsteel.co.uk/post/how-to-make-a-tetrahedron tetrahedron},
 *      {@link https://www.youtube.com/watch?v=47yZf6GHqzg octahedron},
 *      {@link https://www.polyhedra.net/en/model.php?name-en=dodecahedron dodecahedron}, or
 *      {@link https://www.mathhappens.org/take-and-make-icosahedron-from-golden-rectangles/ icosahedron},
 * inscribed in the unit sphere,
 * and recursively subdivides each triangle by inserting a new vertex
 * at the midpoint of its three edges,
 * which is then projected onto the surface of the sphere.
 *
 *  @author Paulo Roma Cavalcanti
 *  @since 21/11/2016
 *  @see <a href="/cwdc/13-webgl/lib/polyhedron.js">source</a>
 *  @see {@link https://www.cs.unm.edu/~angel/BOOK/INTERACTIVE_COMPUTER_GRAPHICS/SIXTH_EDITION/CODE/WebGL/7E/06 Angel's code}
 *  @see <a href="https://www.britannica.com/science/Platonic-solid">Platonic solid</a>
 *  @see <a href="https://www.mathsisfun.com/geometry/platonic-solids-why-five.html">Platonic Solids - Why Five?</a>
 *  @see <a href="https://users.monash.edu.au/~normd/documents/MATH-348-lecture-32.pdf">Platonic Solids and Beyond</a>
 *  @see <img src="/cwdc/13-webgl/lib/tets/tet1.png" width="256"> <img src="/cwdc/13-webgl/lib/tets/tet2.png" width="256">
 *  @see <img src="/cwdc/13-webgl/lib/tets/tet3.png" width="256"> <img src="/cwdc/13-webgl/lib/tets/tet4.png" width="256">
 *  @see <img src="/cwdc/13-webgl/lib/tets/octa1.png" width="256"> <img src="/cwdc/13-webgl/lib/tets/octa2.png" width="256">
 *  @see <img src="/cwdc/13-webgl/lib/tets/octa3.png" width="256"> <img src="/cwdc/13-webgl/lib/tets/octa4.png" width="256">
 */

"use strict";

// import * as THREE from "three";
import * as THREE from "/cwdc/13-webgl/lib/three.module.js";
import { vec3 } from "/cwdc/13-webgl/lib/gl-matrix/dist/esm/index.js";

/**
 * An object containing raw data for
 * vertices, normal vectors, texture coordinates, mercator coordinates and indices.
 * <p>{@link https://threejs.org/docs/#api/en/geometries/PolyhedronGeometry Polyhedra} have no index.</p>
 * @typedef {Object} polyData
 * @property {Float32Array} vertexPositions vertex coordinates.
 * @property {Float32Array} vertexNormals vertex normals.
 * @property {Float32Array} vertexTextureCoords texture coordinates.
 * @property {Float32Array} vertexMercatorCoords mercator texture coordinates.
 * @property {Uint16Array} indices index array.
 * @property {String} name polyhedron name.
 * @property {Number} nfaces initial number of triangles.
 * @property {Number} maxNumSubdivisions maximum number of subdivisions.
 * @property {Function} ntri return the number of triangles given the level of detail.
 * @property {Function} level return the level of detail given the number of triangles.
 */

/**
 * gl-matrix {@link https://glmatrix.net/docs/module-vec3.html 3 Dimensional Vector}.
 * @name vec3
 * @type {glMatrix.vec3}
 */

/**
 * <p>Four points of a {@link https://www.brainsofsteel.co.uk/post/how-to-make-a-tetrahedron tetrahedron}
 * inscribed in the unit sphere, in three different vertex arrangements.</p>
 *
 * Radius of circunsphere:
 * <ul>
 *  <li>R = √6 a/4 = 1</li>
 * </ul>
 *
 * Radius of circuncircle:
 * <ul>
 *  <li>r = a/√3 = 2/3 √6 / √3 = 2/3 √2 = √(8/9) = 0.9428090415820634</li>
 * </ul>
 *
 * Edge length:
 * <ul>
 *  <li>a = 4R / √6 = 2/3 √6 = 1.6329931618554523</li>
 * </ul>
 *
 * Vertex coordinates:
 * <ul>
 *  <li>x = r sin(π/6) = √(2/9) = 0.4714045207910317</li>
 *  <li>y = r cos(π/6) = √(2/3) = 0.816496580927726</li>
 *  <li>z = 1/3 = 0.3333333333333333 (24/9 = 8/9 + (z+1)²)</li>
 * </ul>
 *
 * Four vertices, lower face parallell to the xy plane:
 * <ul>
 *  <li>(r, 0, -z)</li>
 *  <li>(-x, y, -z)</li>
 *  <li>(-x, -y, -z)</li>
 *  <li>(0, 0, 1)</li>
 * </ul>
 * Alternatively, higher face parallell to the xy plane:
 * <ul>
 *  <li>(0, r, z)</li>
 *  <li>(y, -x, z)</li>
 *  <li>(-y, -x, z)</li>
 *  <li>(0, 0, -1)</li>
 * </ul>
 * Embedded inside a cube:
 * <ul>
 *  <li>(1/√3, 1/√3, 1/√3)</li>
 *  <li>(-1/√3, -1/√3, 1/√3)</li>
 *  <li>(-1/√3, 1/√3, -1/√3)</li>
 *  <li>(1/√3, -1/√3, -1/√3)</li>
 * </ul>
 * @type {Array<vec3>}
 * @see {@link https://en.wikipedia.org/wiki/Tetrahedron Tetrahedron}
 * @see {@link https://en.wikipedia.org/wiki/Equilateral_triangle Equilateral triangle}
 * @see <figure>
 *      <img src="../images/Tetrahedron.svg" width="128">
 *      <img src="../images/tet.png" width="164">
 *      <figcaption>
 *        Lower face parallell to the xy plane.
 *      </figcaption>
 *      <img src="../images/Tetraeder_animation_with_cube.gif" width="128">
 *      <figcaption>
 *        Embedded in a cube.
 *      </figcaption>
 *      </figure>
 */
const initialTet = (() => {
  const r = Math.sqrt(8 / 9);
  const x = r * 0.5; // r * sin(Math.PI/6)
  const y = Math.sqrt(2 / 3); // r * cos(Math.PI/6)
  const z = 1 / 3;
  const d = 1 / Math.sqrt(3);
  return {
    normal: [
      vec3.fromValues(0, r, z),
      vec3.fromValues(y, -x, z),
      vec3.fromValues(-y, -x, z),
      vec3.fromValues(0, 0, -1),
    ],
    alternative: [
      vec3.fromValues(r, 0, -z),
      vec3.fromValues(-x, y, -z),
      vec3.fromValues(-x, -y, -z),
      vec3.fromValues(0, 0, 1),
    ],
    cube: [
      vec3.fromValues(d, d, d),
      vec3.fromValues(-d, -d, d),
      vec3.fromValues(-d, d, -d),
      vec3.fromValues(d, -d, -d),
    ],
  };
})();

/**
 * <p>Six points of an {@link https://www.youtube.com/watch?v=47yZf6GHqzg octahedron}
 * inscribed in the unit sphere.</p>
 *
 * Radius of circunsphere:
 * <ul>
 *  <li>R = √2/2 a = 1</li>
 * </ul>
 *
 * Edge length:
 * <ul>
 *  <li>a = 2R / √2 = 1.414213562373095</li>
 * </ul>
 *
 * Six vertices:
 * <ul>
 *  <li>(±1, 0, 0)</li>
 *  <li>(0, ±1, 0)</li>
 *  <li>(0, 0, ±1)</li>
 * </ul>
 *
 * @type {Array<vec3>}
 * @see {@link https://en.wikipedia.org/wiki/Octahedron Octahedron}
 * @see <img src="../images/Octahedron.gif" width="256">
 */
const initialOcta = [
  vec3.fromValues(0.0, 0.0, 1.0),
  vec3.fromValues(0.0, 0.0, -1.0),
  vec3.fromValues(0.0, 1.0, 0.0),
  vec3.fromValues(0.0, -1.0, 0.0),
  vec3.fromValues(1.0, 0.0, 0.0),
  vec3.fromValues(-1.0, 0.0, 0.0),
];

/**
 * <p>Twelve points of an {@link https://www.mathhappens.org/take-and-make-icosahedron-from-golden-rectangles/ icosahedron}
 * inscribed in the unit sphere.</p>
 *
 * Golden Ratio:
 * <ul>
 *  <li>Φ = (√5+1)/2 = 1.618033988749895 </li>
 * </ul>
 *
 * Radius of circunsphere:
 * <ul>
 *  <li>R = √(Φ² + 1)/2 a = 1</li>
 *  <li>r = √(Φ² + 1) = 1.902113032590307 (a=2)</li>
 * </ul>
 *
 * Edge length:
 * <ul>
 *  <li>a = 2R / √(Φ² + 1) = 0.7639320225002103</li>
 * </ul>
 *
 * Vertex Coordinates:
 * <ul>
 *  <li>Φ / √(Φ² + 1)) = 0.85065080835204</li>
 *  <li>1 / √(Φ² + 1) = 0.5257311121191336</li>
 * </ul>
 * Twelve vertices:
 * <ul>
 *  <li>(0, ±1/r, ±Φ/r) </li>
 *  <li>(±1/r, ±Φ/r, 0) </li>
 *  <li>(±Φ/r, 0, ±1/r) </li>
 * </ul>
 *
 * @type {Array<vec3>}
 * @see {@link https://en.wikipedia.org/wiki/Icosahedron Icosahedron}
 * @see {@link https://en.wikipedia.org/wiki/Regular_icosahedron Regular icosahedron}
 * @see <img src="../images/Icosahedron-golden-rectangles.svg" width="256">
 */
const initialIco = (() => {
  const a = (Math.sqrt(5) + 1) / 2;
  const r = Math.sqrt(a * a + 1);
  const b = 1 / r;
  const c = a / r;
  return [
    vec3.fromValues(0, -b, c),

    vec3.fromValues(c, 0, b),
    vec3.fromValues(c, 0, -b),
    vec3.fromValues(-c, 0, -b),
    vec3.fromValues(-c, 0, b),

    vec3.fromValues(-b, c, 0),
    vec3.fromValues(b, c, 0),
    vec3.fromValues(b, -c, 0),
    vec3.fromValues(-b, -c, 0),

    vec3.fromValues(0, -b, -c),
    vec3.fromValues(0, b, -c),
    vec3.fromValues(0, b, c),
  ];
})();

/**
 * <p>Twenty points of a {@link https://www.polyhedra.net/en/model.php?name-en=dodecahedron dodecahedron}
 * inscribed in the unit sphere.</p>
 * Golden Ratio:
 * <ul>
 *  <li>Φ = (√5+1)/2 = 1.618033988749895 </li>
 *  <ki>2/Φ = √5 - 1 = 1.2360679774997898</li>
 * </ul>
 * Radius of circunscribed sphere:
 * <ul>
 *  <li>R = √3 Φ/2 a = 1</li>
 * </ul>
 * Edge length:
 * <ul>
 *  <li>a = 4R / ((√5 + 1)√3) = 2/Φ R/√3 = R (√5 - 1) / √3 = 0.71364417954618</li>
 * </ul>
 *
 * Vertex coordinates:
 * <ul>
 *  <li>1/√3 = 0.5773502691896258</li>
 *  <li>1 / Φ√3 = (√5-1) / (2√3) = 0.35682208977309</li>
 *  <li>Φ / √3 = (√5+1) / (2√3) = 0.9341723589627158</li>
 * </ul>
 *
 * The eight vertices of a cube:
 * <ul>
 *  <li>(±1/√3, ±1/√3, ±1/√3)</li>
 * </ul>
 * The coordinates of the 12 additional vertices:
 * <ul>
 *  <li>(0, ±(Φ / √3), ±(1 / Φ√3)) </li>
 *  <li>(±(1 / Φ√3), 0, ±(Φ / √3)) </li>
 *  <li>(±(Φ / √3), ±(1 / Φ√3), 0) </li>
 * </ul>
 * @type {Array<vec3>}
 * @see {@link https://en.wikipedia.org/wiki/Dodecahedron Dodecahedron}
 * @see {@link https://en.wikipedia.org/wiki/Regular_dodecahedron Regular dodecahedron}
 * @see {@link https://www.scientificamerican.com/article/why-did-ancient-romans-make-this-baffling-metal-dodecahedron/ Why Did Ancient Romans Make this Baffling Metal Dodecahedron?}
 * @see <img src="../images/dodecahedron.png" width="256">
 * @see <iframe title="Cube in a Dodecahedron" src="/cwdc/13-webgl/examples/three/content/stl.html" style="transform: scale(0.85); width: 380px; height: 380px"></iframe>
 * @see {@link https://www.thingiverse.com/thing:3279291 Cube in a Dodecahedron}
 */
const initialDod = (() => {
  const a = 1 / Math.sqrt(3);
  const b = (Math.sqrt(5) - 1) * 0.5 * a;
  const c = (Math.sqrt(5) + 1) * 0.5 * a;
  return [
    vec3.fromValues(-b, 0, c),
    vec3.fromValues(b, 0, c),
    vec3.fromValues(a, a, a),
    vec3.fromValues(0, c, b),
    vec3.fromValues(-a, a, a),
    vec3.fromValues(-a, -a, a),
    vec3.fromValues(0, -c, b),
    vec3.fromValues(a, -a, a),
    vec3.fromValues(c, -b, 0),
    vec3.fromValues(c, b, 0),
    vec3.fromValues(0, -c, -b),
    vec3.fromValues(a, -a, -a),
    vec3.fromValues(a, a, -a),
    vec3.fromValues(0, c, -b),
    vec3.fromValues(-a, -a, -a),
    vec3.fromValues(-b, 0, -c),
    vec3.fromValues(b, 0, -c),
    vec3.fromValues(-c, -b, 0),
    vec3.fromValues(-c, b, 0),
    vec3.fromValues(-a, a, -a),
  ];
})();

/**
 * Maximum subdivision level without overflowing any buffer (16 bits - 65536).
 * @type {Object<{tet:Number, oct:Number, dod:Number, ico: Number}>}
 */
export const limit = {
  tet_hws: Math.floor(Math.log(65536 / (4 * 3)) / Math.log(4)),
  oct_hws: Math.floor(Math.log(65536 / (8 * 3)) / Math.log(4)),
  ico_hws: Math.floor(Math.log(65536 / (20 * 3)) / Math.log(4)),
  dod_hws: Math.floor(Math.log(65536 / (36 * 3)) / Math.log(4)),
  tet: 24,
  oct: 20,
  dod: 12,
  ico: 16,
};

/**
 * Constrain a value to lie between two further values.
 * @param {Number} x value.
 * @param {Number} min minimum value.
 * @param {Number} max maximum value.
 * @return {Number} min ≤ x ≤ max.
 * @function
 */
export const clamp = (x, min, max) => Math.min(Math.max(min, x), max);

/**
 * Convert degrees to radians.
 * @param {Number} deg angle in degrees.
 * @returns {Number} angle in radians.
 * @function
 */
export const radians = (deg) => (deg * Math.PI) / 180;

/**
 * Default number of segments (points - 1) for drawing a meridian or parallel.
 * @type {Number}
 */
export const nsegments = 36;

/**
 * <p>Return a pair of spherical coordinates, in the range [0,1],
 * corresponding to a point p onto the unit sphere.</p>
 *
 * The forward projection transforms spherical coordinates into planar coordinates:
 * <ul>
 * <li>if point p is plotted on a plane, we have the
 * {@link https://docs.qgis.org/3.4/en/docs/gentle_gis_introduction/coordinate_reference_systems.html <i>plate carrée</i> projection},
 * a special case of the equirectangular projection.</li>
 * <li>this projection maps x to be the value of the longitude and
 * y to be the value of the latitude.</li>
 * </ul>
 *
 * <p>The singularity of the mapping (parametrization) is at φ = 0 (y = -r) and φ = π (y = r):</p>
 * <ul>
 *   <li>In this case, an entire line at the top (or bottom) boundary of the texture is mapped onto a single point.</li>
 *   <li> In {@link https://en.wikipedia.org/wiki/Geographic_coordinate_system geographic coordinate system},
 *   φ is measured from the positive y axis (North), not the z axis, as it is usual in math books.
 *   <li>Therefore, we will use North-Counterclockwise Convention.</li>
 *   <li>The 'clockwise from north' convention is used in navigation and mapping.</li>
 *   <li>________________________________________________</li>
 *   <li> atan2(y, x) (East-Counterclockwise Convention)</li>
 *   <li> atan2(x, y) (North-Clockwise Convention)</li>
 *   <li> atan2(-x,-y) (South-Clockwise Convention)</li>
 *   <li>________________________________________________</li>
 *   <li> cos(φ-90) = sin(φ)</li>
 *   <li> sin(φ-90) = -cos(φ)</li>
 *   <li> x = r cos(θ) sin(φ) </li>
 *   <li> y = −r cos (φ) </li>
 *   <li> z = -r sin(θ) sin(φ) </li>
 *   <li> z/x = −(r sin(θ) sin(φ)) / (r cos(θ) sin(φ)) = -sin(θ) / cos(θ) = −tanθ </li>
 *   <li> θ = atan(−z/x) </li>
 *   <li> φ = acos(−y/r) </li>
 * </ul>
 * Note that this definition provides a logical extension of the usual polar coordinates notation,<br>
 * with θ remaining the angle in the zx-plane and φ becoming the angle out of that plane.
 *
 *  @param {vec3} p a point on the sphere.
 *  @return {Object<s:Number, t:Number>} point p in spherical coordinates:
 *  <ul>
 *     <li>const [x, y, z] = p</li>
 *     <li>r = 1 = √(x² + y² + z²)</li>
 *     <li>s = θ = atan2(-z, x) / 2π + 0.5</li>
 *     <li>t = φ = acos(-y/r) / π</li>
 *     <li>tg(-θ) = -tg(θ) = tan (z/x)
 *     <li>arctan(-θ) = -arctan(θ) = atan2(z, x)
 *  </ul>
 *
 * Since the positive angular direction is CCW,
 * we can not use North-Clockwise Convention,
 * because the image would be rendered mirrored.
 * <ul>
 *  <li>border ≡ antimeridian
 *  <li>atan2(-z, x) (border at -x axis of the image - wrap left to right) (correct form) or </li>
 *  <li>atan2(z, -x) (border at x axis of the image - wrap right to left). </li>
 *  <li>atan2(z, x) (border at x axis of the image - mirrored). </li>
 * </ul>
 *
 * @see {@link https://people.computing.clemson.edu/~dhouse/courses/405/notes/texture-maps.pdf#page=3 Texture Mapping}
 * @see {@link https://en.wikipedia.org/wiki/Spherical_coordinate_system Spherical coordinate system}
 * @see {@link https://en.wikipedia.org/wiki/Parametrization_(geometry) Parametrization (geometry)}
 * @see {@link https://math.libretexts.org/Courses/Monroe_Community_College/MTH_212_Calculus_III/Chapter_11%3A_Vectors_and_the_Geometry_of_Space/11.7%3A_Cylindrical_and_Spherical_Coordinates Cylindrical and Spherical Coordinates}
 * @see {@link https://pro.arcgis.com/en/pro-app/latest/help/mapping/properties/coordinate-systems-and-projections.htm Coordinate systems}
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 Math.atan2()}
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos Math.acos()}
 * @see {@link https://en.wikipedia.org/wiki/Atan2 atan2}
 * @see <img src="../images/spherical-projection.png" width = "256">
 * @see <img src="../images/Spherical2.png" width="356">
 *      <img src="../images/Declination.jpg" width="175">
 */
export function cartesian2Spherical(p) {
  const [x, y, z] = p;

  // acos ∈ [0,pi] ⇒ phi ∈ [0,1]
  // acos (-y) = π - acos (y)
  const phi = Math.acos(-y) / Math.PI;

  // atan2 ∈ [-pi,pi] ⇒ theta ∈ [-0.5, 0.5]
  let theta = Math.atan2(-z, x) / (2 * Math.PI);

  // theta ∈ [0, 1]
  theta += 0.5;

  return {
    s: clamp(theta, 0.0, 1.0),
    t: clamp(phi, 0.0, 1.0),
  };
}

/**
 * <p>Return a point on the unit sphere given their
 * {@link https://people.computing.clemson.edu/~dhouse/courses/405/notes/texture-maps.pdf#page=3 spherical coordinates}: (θ, φ, r=1).</p>
 * It is assumed that:
 * <ul>
 *  <li>the two systems have the same origin,</li>
 *  <li>the spherical reference plane is the Cartesian xz plane, </li>
 *  <li>φ is inclination from the y direction, and</li>
 *  <li>the azimuth is measured from the Cartesian x axis, so that the x axis has θ = 0° (prime meridian).</li>
 *  <li>x = p[0] = r cos(θ) * sin(φ)</li>
 *  <li>z = p[2] = -r sin(θ) * sin(φ)</li>
 *  <li>y = p[1] = -r cos(φ)</li>
 * </ul>
 *
 * @param {Number} s azimuth angle θ, 0 ≤ θ ≤ 2π.
 * @param {Number} t zenith angle φ, 0 ≤ φ ≤ π.
 * @param {Number} r radius distance, r ≥ 0.
 * @returns {vec3} cartesian point onto the unit sphere.
 * @see {@link https://mathworld.wolfram.com/SphericalCoordinates.html spherical coordinates}
 * @see <img src="../images/spherical-projection.png" width="256">
 */
export function spherical2Cartesian(s, t, r = 1) {
  const x = r * Math.cos(s) * Math.sin(t);
  const z = -r * Math.sin(s) * Math.sin(t);
  const y = -r * Math.cos(t);
  return vec3.fromValues(x, y, z);
}

/**
 * <p>Convert a 2D point in spherical coordinates to a 2D point in
 * {@link https://en.wikipedia.org/wiki/Mercator_projection Mercator coordinates}.</p>
 * <p>The Mercator projection is a map projection that was widely used for navigation,
 * since {@link https://www.atractor.pt/mat/loxodromica/mercator_loxodromica-_en.html loxodromes}
 * are straight lines (although great circles are curved).</p>
 * The following {@link https://mathworld.wolfram.com/MercatorProjection.html equations}
 * place the x-axis of the projection on the equator,
 * and the y-axis at longitude θ<sub>0</sub>, where θ is the longitude and φ is the latitude:
 * <ul>
 *    <li>x =	θ - θ<sub>0</sub>, 0 ≤ θ - θ<sub>0</sub> ≤ 2π</li>
 *    <li>y =	ln [tan (π/4 + φ/2)], -π/2 ≤ φ ≤ π/2 → -π ≤ y ≤ π </li>
 * </ul>
 * The poles extent to infinity. Therefore, to create a square image,
 * the maximum latitude occurs at y = π, namely:
 * <ul>
 *    <li>φ<sub>max</sub> = 2 atan (e<sup>π</sup>) - π /2 = 85.051129°</li>
 * </ul>
 * As a consequence, we clamp the latitude to &#91;-85°,85°&#93; to avoid any artifact.
 * @param {Number} s longitude (horizontal angle) θ, 0 ≤ θ ≤ 1.
 * @param {Number} t latitude (vertical angle) φ, 0 ≤ φ ≤ 1.
 * @return {Object<x:Number, y:Number>} mercator coordinates in [0,1].
 * @see {@link https://stackoverflow.com/questions/59907996/shader-that-transforms-a-mercator-projection-to-equirectangular mercator projection to equirectangular}
 * @see {@link https://paulbourke.net/panorama/webmerc2sphere/ Converting Web Mercator projection to equirectangular}
 * @see <img src="../images/Cylindrical_Projection_basics2.svg">
 */
export function spherical2Mercator(s, t) {
  // st (uv) to equirectangular
  const lon = s * 2.0 * Math.PI; // [0, 2pi]
  let lat = (t - 0.5) * Math.PI; // [-pi/2, pi/2]
  lat = clamp(lat, radians(-85.0), radians(85.0));

  // equirectangular to mercator
  let x = lon;
  let y = Math.log(Math.tan(Math.PI / 4.0 + lat / 2.0)); // [-pi, pi]

  // bring x,y into [0,1] range
  x = s;
  y = (y + Math.PI) / (2.0 * Math.PI);

  return {
    x: x,
    y: y,
  };
}

/**
 * Convert a 2D point (x=long, y=lat) in {@link https://en.wikipedia.org/wiki/Mercator_projection mercator coordinates}
 * to a 2D point (θ, φ) in {@link https://paulbourke.net/geometry/transformationprojection/ spherical coordinates}.
 * <ul>
 *    <li>θ =	x + θ<sub>0</sub>, 0 ≤ x + θ<sub>0</sub> ≤ 2π</li>
 *    <li>φ =	2 atan (exp (y)) - π/2, -π ≤ y ≤ π → -85.051129° ≤ φ ≤ 85.051129° </li>
 * </ul>
 * @param {Number} x longitude in [0,1].
 * @param {Number} y latitude in [0,1].
 * @returns {Object<x:Number, y:Number>} spherical coordinates in [0,1].
 * @see <img src="../images/Cylindrical_Projection_basics2.svg">
 */
export function mercator2Spherical(x, y) {
  const lat = y * 2 * Math.PI - Math.PI; // [-pi, pi]
  let t = 2 * Math.atan(Math.exp(lat)) - Math.PI / 2; // [-pi/2, pi/2]
  t = t / Math.PI + 0.5; // [0, 1]
  return {
    s: x,
    t: t,
  };
}

/**
 * Set Mercator vertex coordinates.
 * @param {module:polyhedron~polyData} obj model data.
 */
export function setMercatorCoordinates(obj) {
  obj.vertexMercatorCoords = new Float32Array(obj.vertexTextureCoords.length);
  for (let i = 0; i < obj.vertexTextureCoords.length; i += 2) {
    const s = obj.vertexTextureCoords[i];
    const t = obj.vertexTextureCoords[i + 1];
    const { x, y } = spherical2Mercator(s, t);
    obj.vertexMercatorCoords[i] = x;
    obj.vertexMercatorCoords[i + 1] = y;
  }
}

/**
 * Rotate u texture coordinate by a given angle.
 * @param {module:polyhedron~polyData} obj model data.
 * @param {Number} degrees rotation angle.
 */
export function rotateUTexture(obj, degrees) {
  const du = degrees / 360 + 1;
  const uv = obj.vertexTextureCoords;
  for (let i = 0; i < uv.length; i += 2) {
    uv[i] += du;
    if (uv[i] > 1) uv[i] -= 1;
  }
}

/**
 * Return an array with n points on a parallel given its
 * {@link https://www.britannica.com/science/latitude latitude}.
 * @param {Number} [latitude=0] distance north or south of the Equator: [-90°,90°].
 * @param {Number} [n={@link nsegments}] number of points.
 * @return {Float32Array} points on the parallel.
 */
export function pointsOnParallel(latitude = 0, n = nsegments) {
  const ds = (Math.PI * 2) / (n - 1);
  const arr = new Float32Array(3 * n);
  let phi = clamp(latitude, -90, 90) + 90;
  phi = radians(phi);
  for (let i = 0, j = 0; i < n; ++i, j += 3) {
    const p = spherical2Cartesian(i * ds, phi, 1.01);
    arr[j] = p[0];
    arr[j + 1] = p[1];
    arr[j + 2] = p[2];
  }
  return arr;
}

/**
 * Return an array with n points on the equator.
 * @param {Number} [n={@link nsegments}] number of points.
 * @return {Float32Array} points on the equator.
 */
export function pointsOnEquator(n = nsegments) {
  return pointsOnParallel(0, n);
}

/**
 * Return an array with n points on the prime meridian.
 * @param {Number} [n={@link nsegments}] number of points.
 * @return {Float32Array} points on the prime meridian.
 */
export function pointsOnPrimeMeridian(n = nsegments) {
  return pointsOnMeridian(0, n, false);
}

/**
 * Return an array with n points on the anti meridian.
 * @param {Number} [n={@link nsegments}] number of points.
 * @return {Float32Array} points on the anti meridian.
 */
export function pointsOnAntiMeridian(n = nsegments) {
  return pointsOnMeridian(180, n, false);
}

/**
 * Return an array with n points on a meridian given its
 * {@link https://en.wikipedia.org/wiki/Longitude longitude}.
 * @param {Number} [longitude=0] distance east or west of the prime meridian: [-180°,180°]
 * @param {Number} [n={@link nsegments}] number of points.
 * @param {Boolean} [anti=false] whether to draw the antimeridian also.
 * @return {Float32Array} points on the meridian.
 */
export function pointsOnMeridian(longitude = 0, n = nsegments, anti = false) {
  let j = 0;
  let ds = Math.PI / (n - 1);
  if (anti) ds *= 2;
  const arr = new Float32Array(3 * n);
  let theta = clamp(longitude, -180, 180);
  theta = radians(theta);
  for (let i = 0; i < n; ++i, j += 3) {
    const p = spherical2Cartesian(theta, i * ds, 1.01);
    arr[j] = p[0];
    arr[j + 1] = p[1];
    arr[j + 2] = p[2];
  }
  return arr;
}

/**
 * <p>Class for creating the model of a sphere by continuously subdividing a
 * {@link https://en.wikipedia.org/wiki/Regular_polyhedron convex regular polyhedron}.</p>
 *
 * {@link https://www.esri.com/arcgis-blog/products/arcgis-pro/mapping/mercator-its-not-hip-to-be-square Mercator coordinates}
 * are created and returned as a {@link module:polyhedron~polyData polyData}'s property, vertexMercatorCoords, and
 * {@link https://threejs.org/docs/#api/en/geometries/PolyhedronGeometry Three.js polyhedra}
 * texture coordinates are rotated by 180°, because their original coordinates
 * reversed the places of the prime and anti meridians.
 * @see <img src="../images/simple-cylindrical-projection-earth-map-globe-mercator.jpg" width="512">
 */
export class Polyhedron {
  /**
   * @constructs Polyhedron
   * @param {Boolean} fix whether to fix uv coordinates.
   */
  constructor(fix = false) {
    /**
     * <p>Whether texture coordinates should be fixed.</p>
     *
     * @deprecated in face of {@link https://bgolus.medium.com/distinctive-derivative-differences-cce38d36797b Tarini's}
     * method executed in the fragment shader.</p>
     * @type {Boolean}
     */
    this.fix = fix;
    /**
     * Name (type) of the subdivided polyhedron.
     * @type {String}
     */
    this.name = "";

    /**
     * Return the number of triangles at a given subdivision level.
     * @param {Number} n level of detail.
     * @returns {Number} number of triangles: nfaces * 4<sup>n</sup>.
     */
    this.ntriHWS = (n) => this.nfaces * 4 ** Math.min(n, this.maxSubdivisions);

    /**
     * Return the subdivision level given a number of triangles.
     * @param {Number} t number of triangles.
     * @returns {Number} level of detail: log₄(t / nfaces).
     */
    this.levelHWS = (t) => Math.log(t / this.nfaces) / Math.log(4);

    /**
     * Return the number of triangles at a given subdivision level.
     * @param {Number} n level of detail.
     * @returns {Number} number of triangles: nfaces * (n² + 2n + 1).
     */
    this.ntri = (n) => {
      n = Math.min(n, this.maxSubdivisions);
      return this.nfaces * (n * n + 2 * n + 1);
    };

    /**
     * Return the subdivision level given a number of triangles.
     * @param {Number} t number of triangles.
     * @returns {Number} level of detail n: n² + 2n + 1 - t = 0.
     */
    this.level = (t) => {
      const a = 1;
      const b = 2;
      const c = 1 - t / this.nfaces;
      const delta = b * b - 4 * a * c;
      const root = Math.sqrt(delta) / (2 * a);
      return Math.ceil(root) - 1;
    };
  }

  /**
   * Start with empty buffers.
   */
  resetBuffers() {
    /**
     * Number of vertices.
     * @type {Number}
     */
    this.index = 0;

    /**
     * Vertex coordinate array.
     * @type {Array<Number>}
     */
    this.pointsArray = [];

    /**
     * Vertex normal array.
     * @type {Array<Number>}
     */
    this.normalsArray = [];

    /**
     * Index array (triangular face indices).
     * @type {Array<Number>}
     */
    this.pointsIndices = [];

    /**
     * Vertex texture coordinate array.
     * @type {Array<Number>}
     */
    this.texCoords = [];

    /**
     * Vertex mercator texture coordinate array.
     * @type {Array<Number>}
     */
    this.mercCoords = [];
  }

  /**
   * <p>Adds a new triangle.</p>
   * Mercator texture coordinates are also set.
   * @param {vec3} a first vertex.
   * @param {vec3} b second vertex.
   * @param {vec3} c third vertex.
   */
  triangle(a, b, c) {
    this.pointsArray.push(...a, ...b, ...c);

    this.pointsIndices.push(this.index, this.index + 1, this.index + 2);

    const sc = [
      cartesian2Spherical(a),
      cartesian2Spherical(b),
      cartesian2Spherical(c),
    ];

    if (this.fix) this.fixUVCoordinates(sc);

    for (const uv of sc) {
      const { s, t } = uv;
      this.texCoords.push(s, t);

      const { x, y } = spherical2Mercator(s, t);
      this.mercCoords.push(x, y);
    }

    // normals are vectors
    this.normalsArray.push(...a, ...b, ...c);

    this.index += 3;
  }

  /**
   * Recursively subdivides a triangle.
   * @param {vec3} a first vertex.
   * @param {vec3} b second vertex.
   * @param {vec3} c third vertex.
   * @param {Number} count subdivision counter.
   */
  divideTriangle(a, b, c, count) {
    if (count > 0) {
      const ab = vec3.create();
      const ac = vec3.create();
      const bc = vec3.create();
      vec3.scale(ab, vec3.add(ab, a, b), 0.5);
      vec3.scale(ac, vec3.add(ac, a, c), 0.5);
      vec3.scale(bc, vec3.add(bc, b, c), 0.5);

      // project the new points onto the unit sphere
      vec3.normalize(ab, ab);
      vec3.normalize(ac, ac);
      vec3.normalize(bc, bc);

      this.divideTriangle(a, ab, ac, count - 1);
      this.divideTriangle(ab, b, bc, count - 1);
      this.divideTriangle(bc, c, ac, count - 1);
      this.divideTriangle(ab, bc, ac, count - 1);
    } else {
      this.triangle(a, b, c);
    }
  }

  /**
   * <p>Subdivides an initial {@link module:polyhedron~initialTet tetrahedron}.</p>
   * <p>WebGL's vertex index buffers are limited to 16-bit (0-65535) right now:
   * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array Uint16Array}</p>
   * Generates:
   * <ul>
   *  <li> 4 * 4<sup>n</sup> triangles</li>
   *  <li> 4 * 3 * 4<sup>n</sup> vertices</li>
   *  <li> maximum level = 6 (16384 triangles)</li>
   *  <li> 4 * 3 * 4**7 = 196608 vertices → buffer overflow</li>
   * </ul>
   * @param {Object} poly tetrahedron.
   * @property {Array<vec3>} poly.vtx=initialTet.cube vertices of initial tetrahedron.
   * @property {Number} poly.n=limit.tet_hws number of subdivisions.
   * @returns {module:polyhedron~polyData}
   */
  tetrahedronHWS({ vtx = initialTet.cube, n = limit.tet_hws }) {
    this.name = "tetrahedronHWS";
    /**
     * Initial number of triangles.
     * @type {Number}
     */
    this.nfaces = 4;
    /**
     * Maximum number of subdivisions.
     * @type {Number}
     */
    this.maxSubdivisions = limit.tet_hws;

    const [a, b, c, d] = vtx;
    this.resetBuffers();

    n = Math.min(limit.tet_hws, n);

    this.divideTriangle(c, b, a, n);
    this.divideTriangle(b, c, d, n);
    this.divideTriangle(b, d, a, n);
    this.divideTriangle(d, c, a, n);

    return {
      vertexPositions: new Float32Array(this.pointsArray),
      vertexNormals: new Float32Array(this.normalsArray),
      vertexTextureCoords: new Float32Array(this.texCoords),
      vertexMercatorCoords: new Float32Array(this.mercCoords),
      indices: new Uint16Array(this.pointsIndices),
      maxSubdivisions: this.maxSubdivisions,
      name: this.name,
      nfaces: this.nfaces,
      ntri: this.ntriHWS,
      level: this.levelHWS,
    };
  }

  /**
   * <p>Subdivides an initial {@link module:polyhedron~initialOcta octahedron}.</p>
   * <p>WebGL's vertex index buffers are limited to 16-bit (0-65535) right now:
   * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array Uint16Array}</p>
   * Generates:
   * <ul>
   *  <li> 8 * 4<sup>n</sup> triangles</li>
   *  <li> 8 * 3 * 4<sup>n</sup> vertices</li>
   *  <li> maximum level = 5 (8192 triangles)</li>
   *  <li> 8 * 3 * 4**6 = 98304 vertices → buffer overflow</li>
   * </ul>
   * @param {Object} poly octahedron.
   * @property {Array<vec3>} poly.vtx=initialOcta vertices of initial octahedron.
   * @property {Number} poly.n=limit.oct_hws number of subdivisions.
   * @returns {module:polyhedron~polyData}
   */
  octahedronHWS({ vtx = initialOcta, n = limit.oct_hws }) {
    this.name = "octahedronHWS";
    this.nfaces = 8;
    this.maxSubdivisions = limit.oct_hws;

    const [a, b, c, d, e, f] = vtx;
    this.resetBuffers();

    n = Math.min(limit.oct_hws, n);

    this.divideTriangle(b, c, e, n);
    this.divideTriangle(f, c, b, n);
    this.divideTriangle(b, e, d, n);
    this.divideTriangle(f, b, d, n);
    this.divideTriangle(c, a, e, n);
    this.divideTriangle(c, f, a, n);
    this.divideTriangle(a, d, e, n);
    this.divideTriangle(a, f, d, n);

    return {
      vertexPositions: new Float32Array(this.pointsArray),
      vertexNormals: new Float32Array(this.normalsArray),
      vertexTextureCoords: new Float32Array(this.texCoords),
      vertexMercatorCoords: new Float32Array(this.mercCoords),
      indices: new Uint16Array(this.pointsIndices),
      maxSubdivisions: this.maxSubdivisions,
      nfaces: this.nfaces,
      name: this.name,
      ntri: this.ntriHWS,
      level: this.levelHWS,
    };
  }

  /**
   * <p>Subdivides an initial {@link module:polyhedron~initialIco icosahedron}.</p>
   * <p>WebGL's vertex index buffers are limited to 16-bit (0-65535) right now:
   * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array Uint16Array}</p>
   * Generates:
   * <ul>
   *  <li> 20 * 4<sup>n</sup> triangles</li>
   *  <li> 20 * 3 * 4<sup>n</sup> vertices</li>
   *  <li> maximum level = 5 (20480 triangles)</li>
   *  <li> 20 * 3 * 4**6 = 245760 vertices → buffer overflow</li>
   * </ul>
   * @param {Object} poly icosahedron.
   * @property {Array<vec3>} poly.vtx=initialIco vertices of initial octahedron.
   * @property {Number} poly.n=limit.ico_hws number of subdivisions.
   * @returns {module:polyhedron~polyData}
   */
  icosahedronHWS({ vtx = initialIco, n = limit.ico_hws }) {
    this.name = "icosahedronHWS";
    this.nfaces = 20;
    this.maxSubdivisions = limit.ico_hws;

    // prettier-ignore
    const [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11] = vtx;

    this.resetBuffers();

    n = Math.min(limit.ico_hws, n);
    this.divideTriangle(v1, v2, v6, n);
    this.divideTriangle(v1, v7, v2, n);
    this.divideTriangle(v3, v4, v5, n);
    this.divideTriangle(v4, v3, v8, n);
    this.divideTriangle(v6, v5, v11, n);
    this.divideTriangle(v5, v6, v10, n);
    this.divideTriangle(v9, v10, v2, n);
    this.divideTriangle(v10, v9, v3, n);
    this.divideTriangle(v7, v8, v9, n);
    this.divideTriangle(v8, v7, v0, n);
    this.divideTriangle(v11, v0, v1, n);
    this.divideTriangle(v0, v11, v4, n);
    this.divideTriangle(v6, v2, v10, n);
    this.divideTriangle(v1, v6, v11, n);
    this.divideTriangle(v3, v5, v10, n);
    this.divideTriangle(v5, v4, v11, n);
    this.divideTriangle(v2, v7, v9, n);
    this.divideTriangle(v7, v1, v0, n);
    this.divideTriangle(v3, v9, v8, n);
    this.divideTriangle(v4, v8, v0, n);

    return {
      vertexPositions: new Float32Array(this.pointsArray),
      vertexNormals: new Float32Array(this.normalsArray),
      vertexTextureCoords: new Float32Array(this.texCoords),
      vertexMercatorCoords: new Float32Array(this.mercCoords),
      indices: new Uint16Array(this.pointsIndices),
      maxSubdivisions: this.maxSubdivisions,
      nfaces: this.nfaces,
      name: this.name,
      ntri: this.ntriHWS,
      level: this.levelHWS,
    };
  }

  /**
   * <p>Subdivides an initial {@link module:polyhedron~initialDod dodecahedron}.</p>
   * <p>WebGL's vertex index buffers are limited to 16-bit (0-65535) right now:
   * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array Uint16Array}</p>
   * Generates:
   * <ul>
   *  <li> 36 * 4<sup>n</sup> triangles</li>
   *  <li> 36 * 3 * 4<sup>n</sup> vertices</li>
   *  <li> maximum level = 4 (9216 triangles)</li>
   *  <li> 36 * 3 * 4**5 = 110592 vertices → buffer overflow</li>
   * </ul>
   * @param {Object} poly dodecahedron.
   * @property {Array<vec3>} poly.vtx=initialDod vertices of initial dodecahedron.
   * @property {Number} poly.n=limit.dod_hws number of subdivisions.
   * @returns {module:polyhedron~polyData}
   */
  dodecahedronHWS({ vtx = initialDod, n = limit.dod_hws }) {
    this.name = "dodecahedronHWS";
    this.nfaces = 36;
    this.maxSubdivisions = limit.dod_hws;

    // prettier-ignore
    const [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19] = vtx;

    this.resetBuffers();

    n = Math.min(limit.ico_hws, n);
    this.divideTriangle(v19, v18, v4, n);
    this.divideTriangle(v19, v4, v3, n);
    this.divideTriangle(v19, v3, v13, n);

    this.divideTriangle(v19, v13, v12, n);
    this.divideTriangle(v19, v12, v16, n);
    this.divideTriangle(v19, v16, v15, n);

    this.divideTriangle(v11, v16, v12, n);
    this.divideTriangle(v11, v12, v9, n);
    this.divideTriangle(v11, v9, v8, n);

    this.divideTriangle(v5, v0, v4, n);
    this.divideTriangle(v5, v4, v18, n);
    this.divideTriangle(v5, v18, v17, n);

    this.divideTriangle(v14, v10, v6, n);
    this.divideTriangle(v14, v6, v5, n);
    this.divideTriangle(v14, v5, v17, n);

    this.divideTriangle(v14, v17, v18, n);
    this.divideTriangle(v14, v18, v19, n);
    this.divideTriangle(v14, v19, v15, n);

    this.divideTriangle(v14, v15, v16, n);
    this.divideTriangle(v14, v16, v11, n);
    this.divideTriangle(v14, v11, v10, n);

    this.divideTriangle(v12, v13, v3, n);
    this.divideTriangle(v12, v3, v2, n);
    this.divideTriangle(v12, v2, v9, n);

    this.divideTriangle(v8, v7, v6, n);
    this.divideTriangle(v8, v6, v10, n);
    this.divideTriangle(v8, v10, v11, n);

    this.divideTriangle(v1, v7, v8, n);
    this.divideTriangle(v1, v8, v9, n);
    this.divideTriangle(v1, v9, v2, n);

    this.divideTriangle(v1, v0, v5, n);
    this.divideTriangle(v1, v5, v6, n);
    this.divideTriangle(v1, v6, v7, n);

    this.divideTriangle(v0, v1, v2, n);
    this.divideTriangle(v0, v2, v3, n);
    this.divideTriangle(v0, v3, v4, n);

    return {
      vertexPositions: new Float32Array(this.pointsArray),
      vertexNormals: new Float32Array(this.normalsArray),
      vertexTextureCoords: new Float32Array(this.texCoords),
      vertexMercatorCoords: new Float32Array(this.mercCoords),
      indices: new Uint16Array(this.pointsIndices),
      maxSubdivisions: this.maxSubdivisions,
      nfaces: this.nfaces,
      name: this.name,
      ntri: this.ntriHWS,
      level: this.levelHWS,
    };
  }

  /**
   * <p>Subdivides an initial
   * {@link https://threejs.org/docs/#api/en/geometries/TetrahedronGeometry tetrahedron}.</p>
   * Generates:
   * <ul>
   *  <li> 4(n² + 2n + 1) </li>
   *  <li> n = 0: 4 triangles, 4 vertices</li>
   *  <li> n = 1: 16 triangles, 10 vertices</li>
   *  <li> n = 2: 36 triangles, 20 vertices</li>
   *  <li> n = 3: 64 triangles, 34 vertices</li>
   *  <li> n = 4: 100 triangles, 52 vertices</li>
   *  <li> n = 5: 144 triangles, 74 vertices</li>
   * </ul>
   * @param {Object} poly tetrahedron.
   * @property {Number} poly.radius=1 radius for three.js.
   * @property {Number} poly.n=limit.tet number of subdivisions.
   * @returns {module:polyhedron~polyData}
   * @see {@link https://github.com/mrdoob/three.js/blob/master/src/geometries/TetrahedronGeometry.js TetrahedronGeometry.js}
   */
  tetrahedron({ radius = 1, n = limit.tet }) {
    this.name = "tetrahedron";
    this.nfaces = 4;
    this.maxSubdivisions = limit.tet;

    n = Math.min(limit.tet, n);

    const obj = getModelData(new THREE.TetrahedronGeometry(radius, n));

    // rotate texture by 180°
    rotateUTexture(obj, 180);

    setMercatorCoordinates(obj);

    obj.maxSubdivisions = this.maxSubdivisions;
    obj.nfaces = this.nfaces;
    obj.name = this.name;
    obj.ntri = this.ntri;
    obj.level = this.level;

    return obj;
  }

  /**
   * <p>Subdivides an initial
   * {@link https://threejs.org/docs/#api/en/geometries/OctahedronGeometry octhedron}.</p>
   * Generates:
   * <ul>
   *  <li> 8(n² + 2n + 1) </li>
   *  <li> n = 0: 8 triangles, 6 vertices</li>
   *  <li> n = 1: 32 triangles, 18 vertices</li>
   *  <li> n = 2: 72 triangles, 38 vertices</li>
   *  <li> n = 3: 192 triangles, 66 vertices</li>
   *  <li> n = 4: 200 triangles, 102 vertices</li>
   *  <li> n = 5: 288 triangles, 146 vertices</li>
   * </ul>
   * @param {Object} poly octahedron.
   * @property {Number} poly.radius=1 radius for three.js.
   * @property {Number} poly.n=limit.oct number of subdivisions.
   * @returns {module:polyhedron~polyData}
   * @see {@link https://github.com/mrdoob/three.js/blob/master/src/geometries/OctahedronGeometry.js OctahedronGeometry.js}
   */
  octahedron({ radius = 1, n = limit.oct }) {
    this.name = "octahedron";
    this.nfaces = 8;
    this.maxSubdivisions = limit.oct;

    n = Math.min(limit.oct, n);

    const obj = getModelData(new THREE.OctahedronGeometry(radius, n));

    // rotate texture by 180°
    rotateUTexture(obj, 180);

    setMercatorCoordinates(obj);

    obj.maxSubdivisions = this.maxSubdivisions;
    obj.nfaces = this.nfaces;
    obj.name = this.name;
    obj.ntri = this.ntri;
    obj.level = this.level;

    return obj;
  }

  /**
   * <p>Subdivides an initial
   * {@link https://threejs.org/docs/#api/en/geometries/DodecahedronGeometry dodecahedron}.</p>
   * Generates:
   * <ul>
   *  <li> 36(n² + 2n + 1) </li>
   *  <li> n = 0: 36 triangles, 20 vertices</li>
   *  <li> n = 1: 144 triangles, 74 vertices</li>
   *  <li> n = 2: 324 triangles, 164 vertices</li>
   *  <li> n = 3: 576 triangles, 290 vertices</li>
   *  <li> n = 4: 900 triangles, 452 vertices</li>
   *  <li> n = 5: 1296 triangles, 650 vertices</li>
   * </ul>
   * @param {Object} poly dodecahedron.
   * @property {Number} poly.radius=1 radius of the dodecahedron.
   * @property {Number} poly.n=limit.dod number of subdivisions.
   * @returns {module:polyhedron~polyData}
   * @see {@link https://github.com/mrdoob/three.js/blob/master/src/geometries/DodecahedronGeometry.js DodecahedronGeometry.js}
   */
  dodecahedron({ radius = 1, n = limit.dod }) {
    this.name = "dodecahedron";
    this.nfaces = 36;
    this.maxSubdivisions = limit.dod;

    n = Math.min(limit.dod, n);

    const obj = getModelData(new THREE.DodecahedronGeometry(radius, n));

    // rotate texture by 180°
    rotateUTexture(obj, 180);

    setMercatorCoordinates(obj);

    obj.maxSubdivisions = this.maxSubdivisions;
    obj.nfaces = this.nfaces;
    obj.name = this.name;
    obj.ntri = this.ntri;
    obj.level = this.level;

    return obj;
  }

  /**
   * <p>Subdivides an initial
   * {@link https://threejs.org/docs/#api/en/geometries/IcosahedronGeometry icosahedron}.</p>
   * Generates:
   * <ul>
   *  <li> 20(n² + 2n + 1) </li>
   *  <li> n = 0: 20 triangles, 12 vertices</li>
   *  <li> n = 1: 80 triangles, 42 vertices</li>
   *  <li> n = 2: 180 triangles, 92 vertices</li>
   *  <li> n = 3: 320 triangles, 162 vertices</li>
   *  <li> n = 4: 500 triangles, 252 vertices</li>
   *  <li> n = 5: 720 triangles, 362 vertices</li>
   * </ul>
   * @param {Object} poly icosahedron.
   * @property {Number} poly.radius=1 radius of the icosahedron.
   * @property {Number} poly.n=limit.ico number of subdivisions.
   * @returns {module:polyhedron~polyData}
   * @see {@link https://github.com/mrdoob/three.js/blob/master/src/geometries/IcosahedronGeometry.js IcosahedronGeometry.js}
   */
  icosahedron({ radius = 1, n = limit.ico }) {
    this.name = "icosahedron";
    this.nfaces = 20;
    this.maxSubdivisions = limit.ico;

    n = Math.min(limit.ico, n);

    const obj = getModelData(new THREE.IcosahedronGeometry(radius, n));

    // rotate texture by 180°
    rotateUTexture(obj, 180);

    setMercatorCoordinates(obj);

    obj.maxSubdivisions = this.maxSubdivisions;
    obj.nfaces = this.nfaces;
    obj.name = this.name;
    obj.ntri = this.ntri;
    obj.level = this.level;

    return obj;
  }

  /**
   * <p>Equirectangular mapping (also called latitude/longitude or spherical coordinates) is non-linear.<br>
   * That means normal UV mapping can only approximate it - quite badly at the poles, in fact.</p>
   *
   * <p>To fix this, we can calculate our own texture coordinate per fragment, <br>
   * by using the direction to the fragment being drawn, resulting in a perfect match.</p>
   *
   * As a last resource, we can try to adjust uv texture coordinates,
   * when two vertices of a triangle are on one side, <br>
   * and the third on the other side of the discontinuity created,
   * when the 0 coordinate is stitched together with the 1 coordinate.
   *
   * @deprecated in face of {@link https://bgolus.medium.com/distinctive-derivative-differences-cce38d36797b Tarini's}
   * method executed in the fragment shader.</p>
   *
   * @param {Array<Object<{s:Number, t:Number}>>} sc triangle given by its spherical coordinates.
   * @see {@link https://gamedev.stackexchange.com/questions/148167/correcting-projection-of-360-content-onto-a-sphere-distortion-at-the-poles/148178#148178 Per-Fragment Equirectangular}
   */
  fixUVCoordinates(sc) {
    const zero = 0.2;
    const onem = 1 - zero;
    const onep = 1 + zero;
    const twom = 2 - zero;
    const twop = 2 + zero;

    const s = sc[0].s + sc[1].s + sc[2].s;
    const t = sc[0].t + sc[1].t + sc[2].t;
    if (s > onem && s < onep) {
      // two zeros
      if (sc[0].s > onem || sc[1].s > onem || sc[2].s > onem)
        sc[0].s = sc[1].s = sc[2].s = 0;
    } else if (s > twom && s < twop) {
      // two ones
      if (sc[0].s < zero || sc[1].s < zero || sc[2].s < zero)
        sc[0].s = sc[1].s = sc[2].s = 1;
    }
    if (t > onem && t < onep) {
      if (sc[0].t > onem || sc[1].t > onem || sc[2].t > onem)
        sc[0].t = sc[1].t = sc[2].t = 0;
    } else if (t > twom && t < twop) {
      if (sc[0].t < zero || sc[1].t < zero || sc[2].t < zero)
        sc[0].t = sc[1].t = sc[2].t = 1;
    }
  }
}