Source: geometry/geometry.js

/**
 * Attribute location used by vec3 position.
 * Must match shader `layout(location = X)` declaration.
 *
 * @type {number}
 */
const POSITION_ATTRIBUTE_LOCATION = 0;

/**
 * Number of float components per vertex position.
 *
 * @type {number}
 */
const POSITION_COMPONENT_COUNT = 3;

/**
 * Attribute location used by vertex color.
 * Must match shader `layout(location = X)` declaration.
 *
 * @type {number}
 */
const COLOR_ATTRIBUTE_LOCATION = 1;

/**
 * Number of float components per vertex color.
 *
 * @type {number}
 */
const COLOR_COMPONENT_COUNT = 3;

/**
 * Attribute location used by the UV coordinates.
 * Must match shader `layout(location = X)` declaration.
 *
 * @type {number}
 */
const UV_ATTRIBUTE_LOCATION = 2;

/**
 * Number of float components per UV coordinate.
 *
 * @type {number}
 */
const UV_COMPONENT_COUNT = 2;

/**
 * Attribute location used by the normals.
 * Must match shader `layout(location = X)` declaration.
 *
 * @type {number}
 */
const NORMAL_ATTRIBUTE_LOCATION = 3;

/**
 * Number of float components per vertex normal.
 *
 * @type {number}
 */
const NORMAL_COMPONENT_COUNT = 3;

/**
 * Flag passed to `vertexAttribPointer()` method.
 * When false, attribute values are used as-is (no normalization).
 *
 * @type {boolean}
 */
const ATTRIBUTE_NORMALIZED = false;

/**
 * Stride parameter for `vertexAttribPointer()`, when the attribute data is tightly packed.
 * Zero means: compute stride automatically from attribute size and type.
 *
 * @type {number}
 */
const ATTRIBUTE_NO_STRIDE = 0;

/**
 * Offset parameter for `vertexAttribPointer()` for attributes starting at the beginning of the buffer.
 *
 * @type {number}
 */
const ATTRIBUTE_NO_OFFSET = 0;

/**
 * Modulo result expected for correct component alignment.
 *
 * @type {number}
 */
const MODULO_ALIGNED_VALUE = 0;

/**
 * Component count for AABB min/max vectors.
 *
 * @type {number}
 */
const BOUNDING_BOX_COMPONENT_COUNT = 3;

/**
 * Starting index for vertex data traversal.
 *
 * @type {number}
 */
const POSITION_START_INDEX = 0;

/**
 * Offset for the X component in a position triplet.
 *
 * @type {number}
 */
const POSITION_X_OFFSET = 0;

/**
 * Offset for the Y component in a position triplet.
 *
 * @type {number}
 */
const POSITION_Y_OFFSET = 1;

/**
 * Offset for the Z component in a position triplet.
 *
 * @type {number}
 */
const POSITION_Z_OFFSET = 2;

/**
 * Default bounding box component value for empty geometry.
 *
 * @type {number}
 */
const EMPTY_BOUND_COMPONENT = 0.0;

/**
 * Initial value used for bounding box minimum computations.
 *
 * @type {number}
 */
const BOUND_MIN_INIT = Number.POSITIVE_INFINITY;

/**
 * Initial value used for bounding box maximum computations.
 *
 * @type {number}
 */
const BOUND_MAX_INIT = Number.NEGATIVE_INFINITY;

/**
 * Error message for invalid bounding box positions buffer.
 *
 * @type {string}
 */
const ERROR_BOUNDING_BOX_POSITIONS_TYPE = '`Geometry.#writeBoundingBox` expects positions as `Float32Array`.';

/**
 * Error message for invalid bounding box minimum buffer.
 *
 * @type {string}
 */
const ERROR_BOUNDING_BOX_MIN_TYPE = '`Geometry.#writeBoundingBox` expects `outMin` as `Float32Array(3)`.';

/**
 * Error message for invalid bounding box maximum buffer.
 *
 * @type {string}
 */
const ERROR_BOUNDING_BOX_MAX_TYPE = '`Geometry.#writeBoundingBox` expects `outMax` as `Float32Array(3)`.';

/**
 * Number of indices, that form a single triangle.
 *
 * @type {number}
 */
const TRIANGLE_INDEX_COMPONENT_COUNT = 3;

/**
 * Number of indices, that form a single line segment.
 *
 * @type {number}
 */
const LINE_INDEX_COMPONENT_COUNT = 2;

/**
 * Primitive type name of the triangle meshes.
 *
 * @type {string}
 */
export const PRIMITIVE_TRIANGLES = 'triangles';

/**
 * Primitive type name of the independent line segments.
 *
 * @type {string}
 */
export const PRIMITIVE_LINES = 'lines';

/**
 * Primitive type name of the connected line strip.
 *
 * @type {string}
 */
export const PRIMITIVE_LINE_STRIP = 'line_strip';

/**
 * Primitive type name of the closed line loop.
 *
 * @type {string}
 */
export const PRIMITIVE_LINE_LOOP = 'line_loop';

/**
 * Primitive type name of the point sprites.
 *
 * @type {string}
 */
export const PRIMITIVE_POINTS = 'points';

/**
 * Default solid primitive, used by geometry (triangles).
 *
 * @type {string}
 */
const DEFAULT_SOLID_PRIMITIVE = PRIMITIVE_TRIANGLES;

/**
 * Default wireframe primitive, used by geometry (lines).
 *
 * @type {string}
 */
const DEFAULT_WIREFRAME_PRIMITIVE = PRIMITIVE_LINES;

/**
 * Minimum index count for line strip/loop primitives.
 *
 * @type {number}
 */
const MIN_LINE_STRIP_INDEX_COUNT = 2;

/**
 * Supported primitive names.
 *
 * @type {Set<string>}
 */
const SUPPORTED_PRIMITIVES = new Set([
    PRIMITIVE_TRIANGLES,
    PRIMITIVE_LINES,
    PRIMITIVE_LINE_STRIP,
    PRIMITIVE_LINE_LOOP,
    PRIMITIVE_POINTS
]);

/**
 * Error message used for invalid primitive options.
 *
 * @type {string}
 */
const ERROR_INVALID_PRIMITIVE = '`Geometry` expects the primitive options to use known primitive constants.';

/**
 * Geometry primitive override options.
 *
 * @typedef {Object} GeometryPrimitiveOptions
 * @property {string} [solidPrimitive=PRIMITIVE_TRIANGLES] - Solid primitive type.
 * @property {string} [wireframePrimitive=PRIMITIVE_LINES] - Wireframe primitive type.
 */

/**
 * Geometry represents a set of `vertex buffers + index buffers`, grouped under a VAO.
 */
export class Geometry {

    /**
     * WebGL2 rendering context used to create and manage GPU resources.
     *
     * @type {WebGL2RenderingContext}
     * @private
     */
    #webglContext;

    /**
     * Vertex Array Object (VAO) that stores vertex attribute bindings for this geometry.
     *
     * @type {WebGLVertexArrayObject}
     * @private
     */
    #vertexArrayObject;

    /**
     * GPU buffer that stores vertex positions.
     *
     * @type {WebGLBuffer}
     * @private
     */
    #positionBuffer;

    /**
     * Optional GPU buffer, that stores vertex colors.
     * Used by materials that read `a_color` attribute.
     *
     * @type {WebGLBuffer | null}
     * @private
     */
    #colorBuffer;

    /**
     * Optional GPU buffer that stores texture coordinates.
     * Used by textured materials, that read `a_uv` attribute.
     *
     * @type {WebGLBuffer | null}
     * @private
     */
    #uvBuffer;

    /**
     * Optional GPU buffer, that stores the vertex normals.
     * Used by lit materials, that read `a_normal` attribute.
     *
     * @type {WebGLBuffer | null}
     * @private
     */
    #normalBuffer;

    /**
     * Index buffer for solid rendering mode (triangles).
     *
     * @type {WebGLBuffer}
     * @private
     */
    #indexBufferSolid;

    /**
     * Index buffer for wireframe rendering mode (lines).
     *
     * @type {WebGLBuffer}
     * @private
     */
    #indexBufferWireframe;

    /**
     * Number of indices in the solid index buffer.
     *
     * @type {number}
     * @private
     */
    #solidIndexCount;

    /**
     * Number of indices in the wireframe index buffer.
     *
     * @type {number}
     * @private
     */
    #wireframeIndexCount;

    /**
     * Index component type used for solid rendering.
     *
     * @type {number}
     * @private
     */
    #solidIndexComponentType;

    /**
     * Index component type used for wireframe rendering.
     *
     * @type {number}
     * @private
     */
    #wireframeIndexComponentType;

    /**
     * Local-space AABB minimum.
     *
     * @type {Float32Array}
     * @private
     */
    #boundingBoxMin;

    /**
     * Local-space AABB maximum.
     *
     * @type {Float32Array}
     * @private
     */
    #boundingBoxMax;

    /**
     * Indicates whether this geometry instance has been disposed.
     * Disposed geometries must not be used for rendering.
     *
     * @type {boolean}
     * @private
     */
    #isDisposed = false;

    /**
     * Solid primitive type, used for rendering (triangles, lines, points, etc...).
     *
     * @type {string}
     * @private
     */
    #solidPrimitive;

    /**
     * Wireframe primitive type, used for rendering (lines, points, etc...).
     *
     * @type {string}
     * @private
     */
    #wireframePrimitive;

    /**
     * @param {WebGL2RenderingContext} webglContext        - WebGL2 rendering context used to create and manage the GPU resources.
     * @param {Float32Array} positions                     - [x, y, z] triples.
     * @param {Float32Array | null} colors                 - [red, green, blue] triples or null.
     * @param {Uint16Array | Uint32Array} indicesSolid     - Indices for solid triangles.
     * @param {Uint16Array | Uint32Array} indicesWireframe - Indices for wireframe lines.
     * @param {Float32Array | null} [uvs = null]           - [u, v] pairs or null.
     * @param {Float32Array | null} [normals = null]       - [x, y, z] triples or null.
     * @param {GeometryPrimitiveOptions | null} [options]  - Primitive overrides.
     */
    constructor(
        webglContext,
        positions,
        colors,
        indicesSolid,
        indicesWireframe,
        uvs     = null,
        normals = null,
        options = null
    ) {
        if (!(webglContext instanceof WebGL2RenderingContext)) {
            throw new TypeError('`Geometry` expects a `WebGL2RenderingContext`.');
        }

        if (!(positions instanceof Float32Array)) {
            throw new TypeError('`Geometry` expects positions as `Float32Array`.');
        }

        if (colors !== null && !(colors instanceof Float32Array)) {
            throw new TypeError('`Geometry` expects colors as `Float32Array` or null.');
        }

        if (uvs !== null && !(uvs instanceof Float32Array)) {
            throw new TypeError('`Geometry` expects uvs as `Float32Array` or null.');
        }

        if (normals !== null && !(normals instanceof Float32Array)) {
            throw new TypeError('`Geometry` expects normals as `Float32Array` or null.');
        }

        if (!Geometry.#isSupportedIndexArray(indicesSolid) || !Geometry.#isSupportedIndexArray(indicesWireframe)) {
            throw new TypeError('`Geometry` expects indices as `Uint16Array` or `Uint32Array`.');
        }

        if (options !== null && (typeof options !== 'object' || Array.isArray(options))) {
            throw new TypeError('`Geometry` expects `options` as a plain object or null.');
        }

        const {
            solidPrimitive     = DEFAULT_SOLID_PRIMITIVE,
            wireframePrimitive = DEFAULT_WIREFRAME_PRIMITIVE
        } = options || {};

        Geometry.#assertPrimitiveName(solidPrimitive);
        Geometry.#assertPrimitiveName(wireframePrimitive);
        this.#validateAttributeSizes(positions, colors, uvs, normals);
        this.#validateIndexSizes(indicesSolid, indicesWireframe, solidPrimitive, wireframePrimitive);

        this.#webglContext                = webglContext;
        this.#solidIndexCount             = indicesSolid.length;
        this.#wireframeIndexCount         = indicesWireframe.length;
        this.#solidIndexComponentType     = Geometry.#resolveIndexComponentType(webglContext, indicesSolid);
        this.#wireframeIndexComponentType = Geometry.#resolveIndexComponentType(webglContext, indicesWireframe);
        this.#vertexArrayObject           = this.#createVertexArrayObject();
        this.#positionBuffer              = this.#createStaticArrayBuffer(positions);
        this.#colorBuffer                 = colors ? this.#createStaticArrayBuffer(colors) : null;
        this.#uvBuffer                    = uvs ? this.#createStaticArrayBuffer(uvs) : null;
        this.#normalBuffer                = normals ? this.#createStaticArrayBuffer(normals) : null;
        this.#indexBufferSolid            = this.#createIndexBuffer(indicesSolid);
        this.#indexBufferWireframe        = this.#createIndexBuffer(indicesWireframe);
        this.#boundingBoxMin              = new Float32Array(BOUNDING_BOX_COMPONENT_COUNT);
        this.#boundingBoxMax              = new Float32Array(BOUNDING_BOX_COMPONENT_COUNT);
        this.#solidPrimitive              = solidPrimitive;
        this.#wireframePrimitive          = wireframePrimitive;
        Geometry.#writeBoundingBox(positions, this.#boundingBoxMin, this.#boundingBoxMax);
        this.#configureVertexArray();
    }

    /**
     * Binds the VAO of this geometry.
     */
    bind() {
        this.#assertNotDisposed();
        this.#webglContext.bindVertexArray(this.#vertexArrayObject);
    }

    /**
     * Binds the appropriate index buffer depending on the wireframe flag.
     *
     * @param {boolean} wireframe - Flag indicating whether the geometry should be drawn in wireframe mode.
     */
    bindIndexBuffer(wireframe) {
        this.#assertNotDisposed();
        const buffer = wireframe ? this.#indexBufferWireframe : this.#indexBufferSolid;
        this.#webglContext.bindBuffer(this.#webglContext.ELEMENT_ARRAY_BUFFER, buffer);
    }

    /**
     * Returns the index count depending on the wireframe flag.
     *
     * @param {boolean} wireframe - Flag indicating whether the geometry should be drawn in wireframe mode.
     * @returns {number}
     */
    getIndexCount(wireframe) {
        this.#assertNotDisposed();
        return wireframe ? this.#wireframeIndexCount : this.#solidIndexCount;
    }

    /**
     * Returns index component type constant used by `drawElements()`.
     * This depends on whether the index buffer is `Uint16Array` or `Uint32Array`.
     *
     * @param {boolean} wireframe - When true, returns wireframe index component type.
     * @returns {number}          - WebGL component type constant.
     */
    getIndexComponentType(wireframe) {
        this.#assertNotDisposed();
        return wireframe ? this.#wireframeIndexComponentType : this.#solidIndexComponentType;
    }

    /**
     * Returns the primitive type for solid or wireframe rendering.
     *
     * @param {boolean} wireframe - When true, returns wireframe primitive type.
     * @returns {string}
     */
    getPrimitive(wireframe) {
        this.#assertNotDisposed();
        return wireframe ? this.#wireframePrimitive : this.#solidPrimitive;
    }

    /**
     * Returns local-space AABB minimum.
     *
     * @returns {Float32Array}
     */
    getBoundingBoxMin() {
        this.#assertNotDisposed();
        return this.#boundingBoxMin;
    }

    /**
     * Returns local-space AABB maximum.
     *
     * @returns {Float32Array}
     */
    getBoundingBoxMax() {
        this.#assertNotDisposed();
        return this.#boundingBoxMax;
    }

    /**
     * Releases all GPU resources owned by this geometry (VAO and buffers).
     * After calling dispose, this geometry instance must not be used for rendering.
     */
    dispose() {
        if (this.#isDisposed) {
            return;
        }

        const webglContext = this.#webglContext;
        webglContext.deleteBuffer(this.#positionBuffer);

        if (this.#colorBuffer) {
            webglContext.deleteBuffer(this.#colorBuffer);
            this.#colorBuffer = null;
        }

        if (this.#uvBuffer) {
            webglContext.deleteBuffer(this.#uvBuffer);
            this.#uvBuffer = null;
        }

        if (this.#normalBuffer) {
            webglContext.deleteBuffer(this.#normalBuffer);
            this.#normalBuffer = null;
        }

        webglContext.deleteBuffer(this.#indexBufferSolid);
        webglContext.deleteBuffer(this.#indexBufferWireframe);
        webglContext.deleteVertexArray(this.#vertexArrayObject);
        this.#isDisposed = true;
    }

    /**
     * Creates a vertex array object (VAO).
     *
     * @returns {WebGLVertexArrayObject}
     * @private
     */
    #createVertexArrayObject() {
        const vao = this.#webglContext.createVertexArray();

        if (!vao) {
            throw new Error('Failed to create vertex array object (VAO).');
        }

        return vao;
    }

    /**
     * Creates a static `ARRAY_BUFFER` and uploads the given data.
     *
     * @param {Float32Array} data - Vertex attribute data stored as a flat array of numeric components.
     * @returns {WebGLBuffer}
     * @private
     */
    #createStaticArrayBuffer(data) {
        const buffer = this.#webglContext.createBuffer();

        if (!buffer) {
            throw new Error('Failed to create `ARRAY_BUFFER`.');
        }

        this.#webglContext.bindBuffer(this.#webglContext.ARRAY_BUFFER, buffer);
        this.#webglContext.bufferData(this.#webglContext.ARRAY_BUFFER, data, this.#webglContext.STATIC_DRAW);
        return buffer;
    }

    /**
     * Creates an `ELEMENT_ARRAY_BUFFER` and uploads the given index data.
     *
     * @param {Uint16Array | Uint32Array} indices - Index data referencing vertices in the associated vertex buffers.
     * @returns {WebGLBuffer}
     * @private
     */
    #createIndexBuffer(indices) {
        const buffer = this.#webglContext.createBuffer();

        if (!buffer) {
            throw new Error('Failed to create `ELEMENT_ARRAY_BUFFER`.');
        }

        if (!Geometry.#isSupportedIndexArray(indices)) {
            throw new TypeError('`Geometry` expects indices as `Uint16Array` or `Uint32Array`.');
        }

        this.#webglContext.bindBuffer(this.#webglContext.ELEMENT_ARRAY_BUFFER, buffer);
        this.#webglContext.bufferData(this.#webglContext.ELEMENT_ARRAY_BUFFER, indices, this.#webglContext.STATIC_DRAW);
        return buffer;
    }

    /**
     * Checks whether an index array type is supported by `Geometry`.
     *
     * @param {unknown} indices - Value to test.
     * @returns {boolean}       - True if indices is `Uint16Array` or `Uint32Array`.
     * @private
     */
    static #isSupportedIndexArray(indices) {
        return (indices instanceof Uint16Array) || (indices instanceof Uint32Array);
    }

    /**
     * Resolves WebGL index component type constant for the given index array.
     *
     * @param {WebGL2RenderingContext} webglContext - WebGL2 context, that provides constants.
     * @param {Uint16Array | Uint32Array} indices   - Index buffer array.
     * @returns {number}                            - `UNSIGNED_SHORT` or `UNSIGNED_INT`.
     * @private
     */
    static #resolveIndexComponentType(webglContext, indices) {
        return (indices instanceof Uint32Array) ? webglContext.UNSIGNED_INT : webglContext.UNSIGNED_SHORT;
    }

    /**
     * Validates vertex attribute array sizes (positions, colors, uvs, normals).
     *
     * @param {Float32Array} positions      - Flat array of positions: `[x, y, z] * vertexCount`.
     * @param {Float32Array | null} colors  - Optional flat array of colors: `[red, green, blue] * vertexCount`.
     * @param {Float32Array | null} uvs     - Optional flat array of UVs: `[u, v] * vertexCount`.
     * @param {Float32Array | null} normals - Optional flat array of normals: `[x, y, z] * vertexCount`.
     * @private
     */
    #validateAttributeSizes(positions, colors, uvs, normals) {
        if ((positions.length % POSITION_COMPONENT_COUNT) !== MODULO_ALIGNED_VALUE) {
            throw new Error('Geometry positions length must be a multiple of `POSITION_COMPONENT_COUNT`.');
        }

        const vertexCount = positions.length / POSITION_COMPONENT_COUNT;

        if (colors !== null) {
            if ((colors.length % COLOR_COMPONENT_COUNT) !== MODULO_ALIGNED_VALUE) {
                throw new Error('Geometry colors length must be a multiple of `COLOR_COMPONENT_COUNT`.');
            }

            const colorVertexCount = colors.length / COLOR_COMPONENT_COUNT;

            if (colorVertexCount !== vertexCount) {
                throw new Error('Geometry colors vertex count must match positions vertex count.');
            }
        }

        if (uvs !== null) {
            if ((uvs.length % UV_COMPONENT_COUNT) !== MODULO_ALIGNED_VALUE) {
                throw new Error('Geometry uvs length must be a multiple of `UV_COMPONENT_COUNT`.');
            }

            const uvVertexCount = uvs.length / UV_COMPONENT_COUNT;

            if (uvVertexCount !== vertexCount) {
                throw new Error('Geometry uvs vertex count must match positions vertex count.');
            }
        }

        if (normals !== null) {
            if ((normals.length % NORMAL_COMPONENT_COUNT) !== MODULO_ALIGNED_VALUE) {
                throw new Error('Geometry normals length must be a multiple of `NORMAL_COMPONENT_COUNT`.');
            }

            const normalVertexCount = normals.length / NORMAL_COMPONENT_COUNT;

            if (normalVertexCount !== vertexCount) {
                throw new Error('Geometry normals vertex count must match positions vertex count.');
            }
        }
    }

    /**
     * Validates basic index array structure (triangles + lines).
     *
     * @param {Uint16Array | Uint32Array} indicesSolid     - Triangle index buffer data (3 indices per triangle).
     * @param {Uint16Array | Uint32Array} indicesWireframe - Line index buffer data (2 indices per line segment).
     * @private
     */
    #validateIndexSizes(indicesSolid, indicesWireframe, solidPrimitive, wireframePrimitive) {
        Geometry.#validateIndexSizeForPrimitive(indicesSolid, solidPrimitive, 'solid');
        Geometry.#validateIndexSizeForPrimitive(indicesWireframe, wireframePrimitive, 'wireframe');
    }

    /**
     * Validates the index buffer length based on the primitive type.
     *
     * @param {Uint16Array | Uint32Array} indices - Index buffer.
     * @param {string} primitive                  - Primitive type name.
     * @param {string} label                      - Buffer label for error messages.
     * @private
     */
    static #validateIndexSizeForPrimitive(indices, primitive, label) {
        switch (primitive) {
            case PRIMITIVE_TRIANGLES:
                if ((indices.length % TRIANGLE_INDEX_COMPONENT_COUNT) !== MODULO_ALIGNED_VALUE) {
                    throw new Error(`Geometry ${label} indices length must be a multiple of TRIANGLE_INDEX_COMPONENT_COUNT.`);
                }

                return;

            case PRIMITIVE_LINES:
                if ((indices.length % LINE_INDEX_COMPONENT_COUNT) !== MODULO_ALIGNED_VALUE) {
                    throw new Error(`Geometry ${label} indices length must be a multiple of LINE_INDEX_COMPONENT_COUNT.`);
                }

                return;

            case PRIMITIVE_LINE_STRIP:
            case PRIMITIVE_LINE_LOOP:
                if (indices.length < MIN_LINE_STRIP_INDEX_COUNT) {
                    throw new Error(`Geometry ${label} indices length must be at least ${MIN_LINE_STRIP_INDEX_COUNT}.`);
                }

                return;

            case PRIMITIVE_POINTS:
                return;

            default:
                throw new Error(ERROR_INVALID_PRIMITIVE);
        }
    }

    /**
     * Validates primitive name.
     *
     * @param {string} value - Primitive name.
     * @private
     */
    static #assertPrimitiveName(value) {
        if (typeof value !== 'string' || !SUPPORTED_PRIMITIVES.has(value)) {
            throw new TypeError(ERROR_INVALID_PRIMITIVE);
        }
    }

    /**
     * Writes local AABB bounds into the provided buffers.
     *
     * @param {Float32Array} positions - Flat vertex positions [x, y, z].
     * @param {Float32Array} outMin    - Output min buffer.
     * @param {Float32Array} outMax    - Output max buffer.
     * @private
     */
    static #writeBoundingBox(positions, outMin, outMax) {
        if (!(positions instanceof Float32Array)) {
            throw new TypeError(ERROR_BOUNDING_BOX_POSITIONS_TYPE);
        }

        if (!(outMin instanceof Float32Array) || outMin.length !== BOUNDING_BOX_COMPONENT_COUNT) {
            throw new TypeError(ERROR_BOUNDING_BOX_MIN_TYPE);
        }

        if (!(outMax instanceof Float32Array) || outMax.length !== BOUNDING_BOX_COMPONENT_COUNT) {
            throw new TypeError(ERROR_BOUNDING_BOX_MAX_TYPE);
        }

        // Handle the empty geometry, write `empty` bounds and exit early:
        if (positions.length === POSITION_START_INDEX) {
            outMin[POSITION_X_OFFSET] = EMPTY_BOUND_COMPONENT;
            outMin[POSITION_Y_OFFSET] = EMPTY_BOUND_COMPONENT;
            outMin[POSITION_Z_OFFSET] = EMPTY_BOUND_COMPONENT;
            outMax[POSITION_X_OFFSET] = EMPTY_BOUND_COMPONENT;
            outMax[POSITION_Y_OFFSET] = EMPTY_BOUND_COMPONENT;
            outMax[POSITION_Z_OFFSET] = EMPTY_BOUND_COMPONENT;
            return;
        }

        // Initialize min/max accumulators for the AABB computation:
        let minX = BOUND_MIN_INIT;
        let minY = BOUND_MIN_INIT;
        let minZ = BOUND_MIN_INIT;
        let maxX = BOUND_MAX_INIT;
        let maxY = BOUND_MAX_INIT;
        let maxZ = BOUND_MAX_INIT;

        for (let index = POSITION_START_INDEX; index < positions.length; index += POSITION_COMPONENT_COUNT) {
            const x = positions[index + POSITION_X_OFFSET];
            const y = positions[index + POSITION_Y_OFFSET];
            const z = positions[index + POSITION_Z_OFFSET];

            if (x < minX) {
                minX = x;
            }

            if (y < minY) {
                minY = y;
            }

            if (z < minZ) {
                minZ = z;
            }

            if (x > maxX) {
                maxX = x;
            }

            if (y > maxY) {
                maxY = y;
            }

            if (z > maxZ) {
                maxZ = z;
            }
        }

        // Write the computed AABB bounds to the output buffers:
        outMin[POSITION_X_OFFSET] = minX;
        outMin[POSITION_Y_OFFSET] = minY;
        outMin[POSITION_Z_OFFSET] = minZ;
        outMax[POSITION_X_OFFSET] = maxX;
        outMax[POSITION_Y_OFFSET] = maxY;
        outMax[POSITION_Z_OFFSET] = maxZ;
    }

    /**
     * Configures the VAO with position (and optional color/uv) attribute pointers.
     *
     * @private
     */
    #configureVertexArray() {
        const webglContext = this.#webglContext;

        webglContext.bindVertexArray(this.#vertexArrayObject);

        // Positions:
        webglContext.bindBuffer(webglContext.ARRAY_BUFFER, this.#positionBuffer);
        webglContext.enableVertexAttribArray(POSITION_ATTRIBUTE_LOCATION);
        webglContext.vertexAttribPointer(
            POSITION_ATTRIBUTE_LOCATION,
            POSITION_COMPONENT_COUNT,
            webglContext.FLOAT,
            ATTRIBUTE_NORMALIZED,
            ATTRIBUTE_NO_STRIDE,
            ATTRIBUTE_NO_OFFSET
        );

        // Colors (optional):
        if (this.#colorBuffer) {
            webglContext.bindBuffer(webglContext.ARRAY_BUFFER, this.#colorBuffer);
            webglContext.enableVertexAttribArray(COLOR_ATTRIBUTE_LOCATION);
            webglContext.vertexAttribPointer(
                COLOR_ATTRIBUTE_LOCATION,
                COLOR_COMPONENT_COUNT,
                webglContext.FLOAT,
                ATTRIBUTE_NORMALIZED,
                ATTRIBUTE_NO_STRIDE,
                ATTRIBUTE_NO_OFFSET
            );
        }

        // UVs (optional):
        if (this.#uvBuffer) {
            webglContext.bindBuffer(webglContext.ARRAY_BUFFER, this.#uvBuffer);
            webglContext.enableVertexAttribArray(UV_ATTRIBUTE_LOCATION);
            webglContext.vertexAttribPointer(
                UV_ATTRIBUTE_LOCATION,
                UV_COMPONENT_COUNT,
                webglContext.FLOAT,
                ATTRIBUTE_NORMALIZED,
                ATTRIBUTE_NO_STRIDE,
                ATTRIBUTE_NO_OFFSET
            );
        }

        // Normals (optional):
        if (this.#normalBuffer) {
            webglContext.bindBuffer(webglContext.ARRAY_BUFFER, this.#normalBuffer);
            webglContext.enableVertexAttribArray(NORMAL_ATTRIBUTE_LOCATION);
            webglContext.vertexAttribPointer(
                NORMAL_ATTRIBUTE_LOCATION,
                NORMAL_COMPONENT_COUNT,
                webglContext.FLOAT,
                ATTRIBUTE_NORMALIZED,
                ATTRIBUTE_NO_STRIDE,
                ATTRIBUTE_NO_OFFSET
            );
        }

        // Bind the default index buffer (solid) to the VAO:
        webglContext.bindBuffer(webglContext.ELEMENT_ARRAY_BUFFER, this.#indexBufferSolid);
        webglContext.bindVertexArray(null);
    }

    /**
     * @private
     */
    #assertNotDisposed() {
        if (this.#isDisposed) {
            throw new Error('Geometry has been disposed and can no longer be used.');
        }
    }
}