Source: geometry/cone-geometry.js

import { GeometryUtils }                            from './geometry-utils.js';
import { GeneratedGeometry }                        from './generated-geometry.js';
import { MATH_VECTOR3_INDEXES, MATH_COMMON_VALUES } from '../constants/math.js';
import { ECMASCRIPT_TYPEOF_RESULTS }                from '../constants/ecmascript-types.js';

import {
    GEOMETRY_DEFAULTS,
    DEFAULT_VERTEX_COLOR,
    GEOMETRY_SIZES,
    GEOMETRY_GRID,
    GEOMETRY_LAYOUT,
    GEOMETRY_UV,
    GEOMETRY_ANGLES,
    GEOMETRY_UV_INDEXES
} from '../constants/geometry.js';

import {
    CONE_DEFAULTS,
    CONE_LIMITS,
    CONE_NORMALS,
    CONE_LAYOUT
} from '../constants/cone-geometry.js';

/**
 * Cone geometry options.
 *
 * `width` is the base diameter along X. `depth` (optional) is the base diameter along Z.
 * If `depth` is not provided, it defaults to `width` (circular base).
 *
 * Segment parameters must be integers:
 * - `radialSegments >= 3`
 * - `heightSegments >= 1`
 *
 * @typedef {Object} ConeGeometryOptions
 * @property {number} [width = 1.0]                        - Base diameter along X.
 * @property {number} [height = 1.5]                       - Cone height along Y.
 * @property {number} [depth = width]                      - Base diameter along Z.
 * @property {number} [radialSegments = 24]                - Segments around the base.
 * @property {number} [heightSegments = 1]                 - Segments along height.
 * @property {boolean} [capped = GEOMETRY_DEFAULTS.CAPPED] - Whether to generate a bottom cap.
 * @property {Float32Array} [colors]                       - Color specification buffer.
 */

/**
 * Internal geometry buffers produced by `ConeGeometry`.
 *
 * @typedef {Object} ConeGeometryData
 * @property {Float32Array} positions                     - Vertex positions (xyz).
 * @property {Float32Array} normals                       - Vertex normals (xyz).
 * @property {Float32Array} uvs                           - Vertex UVs (uv).
 * @property {Float32Array} colors                        - Vertex colors (rgb).
 * @property {Uint16Array | Uint32Array} indicesSolid     - Triangle index buffer.
 * @property {Uint16Array | Uint32Array} indicesWireframe - Line index buffer for wireframe.
 */

/**
 * Segmented cone geometry (with optional elliptical base and bottom cap).
 */
export class ConeGeometry extends GeneratedGeometry {

    /**
     * Normalizes constructor input to a `ConeGeometryOptions` object.
     *
     * @param {ConeGeometryOptions} options     - Options object.
     * @returns {Required<ConeGeometryOptions>} - Normalized options.
     * @private
     */
    static #normalizeOptions(options) {
        if (options === null || typeof options !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT) {
            throw new TypeError('ConeGeometry expects options as an object.');
        }

        const {
            width          = CONE_DEFAULTS.WIDTH,
            height         = CONE_DEFAULTS.HEIGHT,
            depth          = width,
            radialSegments = CONE_DEFAULTS.RADIAL_SEGMENTS,
            heightSegments = CONE_DEFAULTS.HEIGHT_SEGMENTS,
            capped         = GEOMETRY_DEFAULTS.CAPPED,
            colors         = DEFAULT_VERTEX_COLOR
        } = options;

        if (typeof width  !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER ||
            typeof height !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER ||
            typeof depth  !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER) {
            throw new TypeError('ConeGeometry expects width, height or depth as numbers.');
        }

        if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(depth)) {
            throw new RangeError('ConeGeometry expects finite width, height or depth.');
        }

        if (!(colors instanceof Float32Array)) {
            throw new TypeError('ConeGeometry expects colors as a Float32Array.');
        }

        return {
            width,
            height,
            depth,
            radialSegments : GeometryUtils.normalizeSegmentCount(radialSegments, 'radialSegments', CONE_LIMITS.MIN_RADIAL_SEGMENT_COUNT, 'ConeGeometry'),
            heightSegments : GeometryUtils.normalizeSegmentCount(heightSegments, 'heightSegments', CONE_LIMITS.MIN_HEIGHT_SEGMENT_COUNT, 'ConeGeometry'),
            capped         : Boolean(capped),
            colors
        };
    }

    /**
     * Generates vertex and index buffers from construction options.
     *
     * @param {ConeGeometryOptions} [options] - Geometry options.
     * @returns {ConeGeometryData}            - Generated CPU buffers.
     * @protected
     */
    static createGeometryData(options = {}) {
        const normalized      = ConeGeometry.#normalizeOptions(options);
        const ringVertexCount = normalized.radialSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
        const sideVertexCount = normalized.heightSegments * ringVertexCount;
        const capVertexCount  = normalized.capped ? (ringVertexCount + GEOMETRY_GRID.VERTEX_INCREMENT) : CONE_LAYOUT.ZERO_VERTEX_COUNT;
        const vertexCount     = sideVertexCount + GEOMETRY_GRID.VERTEX_INCREMENT + capVertexCount;
        const positions       = new Float32Array(vertexCount * GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT);
        const normals         = new Float32Array(vertexCount * GEOMETRY_LAYOUT.NORMAL_COMPONENT_COUNT);
        const uvs             = new Float32Array(vertexCount * GEOMETRY_LAYOUT.UV_COMPONENT_COUNT);
        const apexIndex       = ConeGeometry.#writeSides(positions, normals, uvs, normalized);
        const capCenterIndex  = apexIndex + GEOMETRY_GRID.VERTEX_INCREMENT;
        ConeGeometry.#writeApex(positions, normals, uvs, normalized, apexIndex);

        if (normalized.capped) {
            ConeGeometry.#writeCapCenter(positions, normals, uvs, normalized, capCenterIndex);
            ConeGeometry.#writeCapRing(positions, normals, uvs, normalized, capCenterIndex);
        }

        const indexList        = ConeGeometry.#createTriangleIndices(normalized, apexIndex, capCenterIndex);
        const indicesSolid     = GeometryUtils.createIndexArray(vertexCount, indexList);
        const indicesWireframe = GeometryUtils.createWireframeIndicesFromSolidIndices(vertexCount, indicesSolid);
        const colors           = GeometryUtils.createColorsFromSpec(vertexCount, normalized.colors);

        return {
            positions,
            normals,
            uvs,
            colors,
            indicesSolid,
            indicesWireframe
        };
    }

    /**
     * Computes inverse vector length `(1 / sqrt(x ^ 2 + y ^ 2 + z ^ 2))`.
     * Returns `0`, when the input vector is zero-length.
     *
     * @param {number} x - X component.
     * @param {number} y - Y component.
     * @param {number} z - Z component.
     * @returns {number} - Inverse length.
     * @private
     */
    static #inverseLength(x, y, z) {
        const length = Math.sqrt((x * x) + (y * y) + (z * z));

        if (length === MATH_COMMON_VALUES.ZERO) {
            return MATH_COMMON_VALUES.ZERO;
        }

        return MATH_COMMON_VALUES.UNIT / length;
    }

    /**
     * Writes the side rings and returns the apex vertex index.
     *
     * @param {Float32Array} positions                   - Output positions.
     * @param {Float32Array} normals                     - Output normals.
     * @param {Float32Array} uvs                         - Output texture coordinates.
     * @param {Required<ConeGeometryOptions>} normalized - Normalized geometry options.
     * @returns {number}
     * @private
     */
    static #writeSides(positions, normals, uvs, normalized) {
        const halfSizeDivisor        = GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
        const vertexIncrement        = GEOMETRY_GRID.VERTEX_INCREMENT;
        const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
        const uvComponentCount       = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
        const vFlipBase              = GEOMETRY_UV.V_FLIP_BASE;
        const fullTurn               = GEOMETRY_ANGLES.FULL_TURN;
        const xComponentIndex        = MATH_VECTOR3_INDEXES.X;
        const yComponentIndex        = MATH_VECTOR3_INDEXES.Y;
        const zComponentIndex        = MATH_VECTOR3_INDEXES.Z;
        const uIndex                 = GEOMETRY_UV_INDEXES.U;
        const vIndex                 = GEOMETRY_UV_INDEXES.V;
        const radiusX                = normalized.width / halfSizeDivisor;
        const radiusZ                = normalized.depth / halfSizeDivisor;
        const height                 = normalized.height;
        const radialSegments         = normalized.radialSegments;
        const heightSegments         = normalized.heightSegments;
        const ringVertexCount        = radialSegments + vertexIncrement;
        const sideRingCount          = heightSegments;
        let vertexIndex              = MATH_COMMON_VALUES.ZERO;

        for (let heightRingIndex = MATH_COMMON_VALUES.ZERO; heightRingIndex < sideRingCount; heightRingIndex += MATH_COMMON_VALUES.UNIT) {
            const heightNormalized = heightRingIndex / heightSegments;
            const radiusFactor     = vFlipBase - heightNormalized;
            const positionY        = ((-height / halfSizeDivisor) + (heightNormalized * height));
            const currentRadiusX   = radiusX * radiusFactor;
            const currentRadiusZ   = radiusZ * radiusFactor;

            for (let radialVertexIndex = MATH_COMMON_VALUES.ZERO; radialVertexIndex < ringVertexCount; radialVertexIndex += MATH_COMMON_VALUES.UNIT) {
                const uNormalized  = radialVertexIndex / radialSegments;
                const angleRadians = uNormalized * fullTurn;
                const cosTheta     = Math.cos(angleRadians);
                const sinTheta     = Math.sin(angleRadians);
                const positionX    = cosTheta * currentRadiusX;
                const positionZ    = sinTheta * currentRadiusZ;

                const positionBaseOffset = vertexIndex * positionComponentCount;
                positions[positionBaseOffset + xComponentIndex] = positionX;
                positions[positionBaseOffset + yComponentIndex] = positionY;
                positions[positionBaseOffset + zComponentIndex] = positionZ;

                const normalX0            = radiusZ * height * cosTheta;
                const normalY0            = radiusX * radiusZ;
                const normalZ0            = radiusX * height * sinTheta;
                const inverseNormalLength = ConeGeometry.#inverseLength(normalX0, normalY0, normalZ0);
                const uvBaseOffset        = vertexIndex * uvComponentCount;

                normals[positionBaseOffset + xComponentIndex] = normalX0 * inverseNormalLength;
                normals[positionBaseOffset + yComponentIndex] = normalY0 * inverseNormalLength;
                normals[positionBaseOffset + zComponentIndex] = normalZ0 * inverseNormalLength;
                uvs[uvBaseOffset + uIndex] = uNormalized;
                uvs[uvBaseOffset + vIndex] = vFlipBase - heightNormalized;
                vertexIndex += MATH_COMMON_VALUES.UNIT;
            }
        }

        return vertexIndex;
    }

    /**
     * Writes the apex position, upward normal and texture coordinates.
     *
     * @param {Float32Array} positions                   - Output positions.
     * @param {Float32Array} normals                     - Output normals.
     * @param {Float32Array} uvs                         - Output texture coordinates.
     * @param {Required<ConeGeometryOptions>} normalized - Normalized geometry options.
     * @param {number} apexIndex                         - Apex index.
     * @private
     */
    static #writeApex(positions, normals, uvs, normalized, apexIndex) {
        const halfSizeDivisor        = GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
        const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
        const uvComponentCount       = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
        const xComponentIndex        = MATH_VECTOR3_INDEXES.X;
        const yComponentIndex        = MATH_VECTOR3_INDEXES.Y;
        const zComponentIndex        = MATH_VECTOR3_INDEXES.Z;
        const uIndex                 = GEOMETRY_UV_INDEXES.U;
        const vIndex                 = GEOMETRY_UV_INDEXES.V;
        const xZero                  = CONE_NORMALS.X_ZERO;
        const zZero                  = CONE_NORMALS.Z_ZERO;
        const center                 = GEOMETRY_UV.CENTER;
        const height                 = normalized.height;
        const apexBaseOffset         = apexIndex * positionComponentCount;

        positions[apexBaseOffset + xComponentIndex] = CONE_LAYOUT.ORIGIN;
        positions[apexBaseOffset + yComponentIndex] = height / halfSizeDivisor;
        positions[apexBaseOffset + zComponentIndex] = CONE_LAYOUT.ORIGIN;
        normals[apexBaseOffset + xComponentIndex]   = xZero;
        normals[apexBaseOffset + yComponentIndex]   = CONE_NORMALS.Y_UP;
        normals[apexBaseOffset + zComponentIndex]   = zZero;

        const apexUvOffset = apexIndex * uvComponentCount;
        uvs[apexUvOffset + uIndex] = center;
        uvs[apexUvOffset + vIndex] = CONE_LAYOUT.ORIGIN;
    }

    /**
     * Writes the bottom cap center and its downward normal.
     *
     * @param {Float32Array} positions                   - Output positions.
     * @param {Float32Array} normals                     - Output normals.
     * @param {Float32Array} uvs                         - Output texture coordinates.
     * @param {Required<ConeGeometryOptions>} normalized - Normalized geometry options.
     * @param {number} capCenterIndex                    - Cap center index.
     * @private
     */
    static #writeCapCenter(positions, normals, uvs, normalized, capCenterIndex) {
        const halfSizeDivisor        = GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
        const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
        const uvComponentCount       = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
        const xComponentIndex        = MATH_VECTOR3_INDEXES.X;
        const yComponentIndex        = MATH_VECTOR3_INDEXES.Y;
        const zComponentIndex        = MATH_VECTOR3_INDEXES.Z;
        const uIndex                 = GEOMETRY_UV_INDEXES.U;
        const vIndex                 = GEOMETRY_UV_INDEXES.V;
        const xZero                  = CONE_NORMALS.X_ZERO;
        const zZero                  = CONE_NORMALS.Z_ZERO;
        const center                 = GEOMETRY_UV.CENTER;
        const yDown                  = CONE_NORMALS.Y_DOWN;
        const height                 = normalized.height;

        const capCenterBaseOffset = capCenterIndex * positionComponentCount;
        positions[capCenterBaseOffset + xComponentIndex] = CONE_LAYOUT.ORIGIN;
        positions[capCenterBaseOffset + yComponentIndex] = -height / halfSizeDivisor;
        positions[capCenterBaseOffset + zComponentIndex] = CONE_LAYOUT.ORIGIN;
        normals[capCenterBaseOffset + xComponentIndex]   = xZero;
        normals[capCenterBaseOffset + yComponentIndex]   = yDown;
        normals[capCenterBaseOffset + zComponentIndex]   = zZero;

        const capCenterUvOffset = capCenterIndex * uvComponentCount;
        uvs[capCenterUvOffset + uIndex] = center;
        uvs[capCenterUvOffset + vIndex] = center;
    }

    /**
     * Writes the bottom cap rim with radial texture coordinates.
     *
     * @param {Float32Array} positions                   - Output positions.
     * @param {Float32Array} normals                     - Output normals.
     * @param {Float32Array} uvs                         - Output texture coordinates.
     * @param {Required<ConeGeometryOptions>} normalized - Normalized geometry options.
     * @param {number} capCenterIndex                    - Cap center index.
     * @private
     */
    static #writeCapRing(positions, normals, uvs, normalized, capCenterIndex) {
        const halfSizeDivisor        = GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
        const vertexIncrement        = GEOMETRY_GRID.VERTEX_INCREMENT;
        const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
        const uvComponentCount       = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
        const fullTurn               = GEOMETRY_ANGLES.FULL_TURN;
        const xComponentIndex        = MATH_VECTOR3_INDEXES.X;
        const yComponentIndex        = MATH_VECTOR3_INDEXES.Y;
        const zComponentIndex        = MATH_VECTOR3_INDEXES.Z;
        const uIndex                 = GEOMETRY_UV_INDEXES.U;
        const vIndex                 = GEOMETRY_UV_INDEXES.V;
        const xZero                  = CONE_NORMALS.X_ZERO;
        const zZero                  = CONE_NORMALS.Z_ZERO;
        const center                 = GEOMETRY_UV.CENTER;
        const yDown                  = CONE_NORMALS.Y_DOWN;
        const doubleSizeMultiplier   = GEOMETRY_SIZES.DOUBLE_SIZE_MULTIPLIER;
        const radiusX                = normalized.width / halfSizeDivisor;
        const radiusZ                = normalized.depth / halfSizeDivisor;
        const height                 = normalized.height;
        const radialSegments         = normalized.radialSegments;
        const ringVertexCount        = radialSegments + vertexIncrement;
        let vertexIndex              = capCenterIndex + GEOMETRY_GRID.VERTEX_INCREMENT;

        for (let radialVertexIndex = MATH_COMMON_VALUES.ZERO; radialVertexIndex < ringVertexCount; radialVertexIndex += MATH_COMMON_VALUES.UNIT) {
            const uNormalized        = radialVertexIndex / radialSegments;
            const angleRadians       = uNormalized * fullTurn;
            const cosTheta           = Math.cos(angleRadians);
            const sinTheta           = Math.sin(angleRadians);
            const positionX          = cosTheta * radiusX;
            const positionZ          = sinTheta * radiusZ;
            const positionBaseOffset = vertexIndex * positionComponentCount;

            positions[positionBaseOffset + xComponentIndex] = positionX;
            positions[positionBaseOffset + yComponentIndex] = -height / halfSizeDivisor;
            positions[positionBaseOffset + zComponentIndex] = positionZ;
            normals[positionBaseOffset + xComponentIndex]   = xZero;
            normals[positionBaseOffset + yComponentIndex]   = yDown;
            normals[positionBaseOffset + zComponentIndex]   = zZero;

            const uvBaseOffset = vertexIndex * uvComponentCount;
            uvs[uvBaseOffset + uIndex] = (radiusX === MATH_COMMON_VALUES.ZERO) ? center : ((positionX / (radiusX * doubleSizeMultiplier)) + center);
            uvs[uvBaseOffset + vIndex] = (radiusZ === MATH_COMMON_VALUES.ZERO) ? center : ((positionZ / (radiusZ * doubleSizeMultiplier)) + center);
            vertexIndex += MATH_COMMON_VALUES.UNIT;
        }
    }

    /**
     * Connects the side rings, apex and optional bottom cap.
     *
     * @param {Required<ConeGeometryOptions>} normalized - Normalized geometry options.
     * @param {number} apexIndex                         - Apex index.
     * @param {number} capCenterIndex                    - Cap center index.
     * @returns {number[]}
     * @private
     */
    static #createTriangleIndices(normalized, apexIndex, capCenterIndex) {
        const vertexIncrement      = GEOMETRY_GRID.VERTEX_INCREMENT;
        const nextVertexOffset     = GEOMETRY_GRID.NEXT_VERTEX_OFFSET;
        const radialSegments       = normalized.radialSegments;
        const heightSegments       = normalized.heightSegments;
        const ringVertexCount      = radialSegments + vertexIncrement;
        const sideRingCount        = heightSegments;
        const hasCap               = normalized.capped;
        const solidTriangleIndices = [];

        GeometryUtils.appendGridTriangleIndices(solidTriangleIndices, radialSegments, sideRingCount - vertexIncrement);
        const topRingStartIndex = (sideRingCount - vertexIncrement) * ringVertexCount;

        for (let radialIndex = MATH_COMMON_VALUES.ZERO; radialIndex < radialSegments; radialIndex += MATH_COMMON_VALUES.UNIT) {
            const topLeftVertexIndex  = topRingStartIndex  + radialIndex;
            const topRightVertexIndex = topLeftVertexIndex + nextVertexOffset;
            solidTriangleIndices.push(topLeftVertexIndex, apexIndex, topRightVertexIndex);
        }

        if (hasCap) {
            ConeGeometry.#appendCapIndices(solidTriangleIndices, radialSegments, capCenterIndex);
        }

        return solidTriangleIndices;
    }

    /**
     * Appends the triangle fan around the bottom cap center.
     *
     * @param {number[]} solidTriangleIndices - Output triangle indexes.
     * @param {number} radialSegments         - Radial segments.
     * @param {number} capCenterIndex         - Cap center index.
     * @private
     */
    static #appendCapIndices(solidTriangleIndices, radialSegments, capCenterIndex) {
        const vertexIncrement   = GEOMETRY_GRID.VERTEX_INCREMENT;
        const capRingStartIndex = capCenterIndex + vertexIncrement;

        for (let radialIndex = MATH_COMMON_VALUES.ZERO; radialIndex < radialSegments; radialIndex += MATH_COMMON_VALUES.UNIT) {
            const capLeftVertexIndex  = capRingStartIndex + radialIndex;
            const capRightVertexIndex = capRingStartIndex + radialIndex + vertexIncrement;
            solidTriangleIndices.push(capCenterIndex, capRightVertexIndex, capLeftVertexIndex);
        }
    }
}