Source: geometry/geometry.js

import { MATH_VECTOR3_INDEXES }      from '../constants/math.js';
import { ECMASCRIPT_TYPEOF_RESULTS } from '../constants/ecmascript-types.js';

export {
    PRIMITIVE_TRIANGLES,
    PRIMITIVE_LINES,
    PRIMITIVE_LINE_STRIP,
    PRIMITIVE_LINE_LOOP,
    PRIMITIVE_POINTS
} from '../constants/geometry.js';

import {
    PRIMITIVE_TRIANGLES,
    PRIMITIVE_LINES,
    PRIMITIVE_LINE_STRIP,
    PRIMITIVE_LINE_LOOP,
    PRIMITIVE_POINTS,
    SUPPORTED_PRIMITIVES,
    GEOMETRY_PRIMITIVE_DEFAULTS,
    GEOMETRY_LAYOUT,
    GEOMETRY_BUFFER_LAYOUT,
    GEOMETRY_LIMITS,
    GEOMETRY_ERRORS
} from '../constants/geometry.js';

/**
 * 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 !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT || Array.isArray(options))) {
            throw new TypeError('Geometry expects options as a plain object or null.');
        }

        const {
            solidPrimitive     = GEOMETRY_PRIMITIVE_DEFAULTS.SOLID_PRIMITIVE,
            wireframePrimitive = GEOMETRY_PRIMITIVE_DEFAULTS.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(GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT);
        this.#boundingBoxMax              = new Float32Array(GEOMETRY_LAYOUT.POSITION_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.');
        }

        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 % GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT) !== GEOMETRY_BUFFER_LAYOUT.MODULO_ALIGNED_VALUE) {
            throw new Error('Geometry positions length must be a multiple of POSITION_COMPONENT_COUNT.');
        }

        const vertexCount = positions.length / GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;

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

            const colorVertexCount = colors.length / GEOMETRY_LAYOUT.COLOR_COMPONENT_COUNT;

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

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

            const uvVertexCount = uvs.length / GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;

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

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

            const normalVertexCount = normals.length / GEOMETRY_LAYOUT.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 % GEOMETRY_LAYOUT.TRIANGLE_INDEX_COUNT) !== GEOMETRY_BUFFER_LAYOUT.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 % GEOMETRY_LAYOUT.LINE_INDEX_COUNT) !== GEOMETRY_BUFFER_LAYOUT.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 < GEOMETRY_LIMITS.MIN_LINE_STRIP_INDEX_COUNT) {
                    throw new Error(`Geometry ${label} indices length must be at least ${GEOMETRY_LIMITS.MIN_LINE_STRIP_INDEX_COUNT}.`);
                }

                return;

            case PRIMITIVE_POINTS:
                return;

            default:
                throw new Error(GEOMETRY_ERRORS.INVALID_PRIMITIVE);
        }
    }

    /**
     * Validates primitive name.
     *
     * @param {string} value - Primitive name.
     * @private
     */
    static #assertPrimitiveName(value) {
        if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.STRING || !SUPPORTED_PRIMITIVES.has(value)) {
            throw new TypeError(GEOMETRY_ERRORS.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) {
        // Buffer types are validated by the constructor; output buffers are allocated internally.
        // Handle the empty geometry, write the empty bounds and exit early
        if (positions.length === GEOMETRY_BUFFER_LAYOUT.POSITION_START_INDEX) {
            outMin[MATH_VECTOR3_INDEXES.X] = GEOMETRY_BUFFER_LAYOUT.EMPTY_BOUND_COMPONENT;
            outMin[MATH_VECTOR3_INDEXES.Y] = GEOMETRY_BUFFER_LAYOUT.EMPTY_BOUND_COMPONENT;
            outMin[MATH_VECTOR3_INDEXES.Z] = GEOMETRY_BUFFER_LAYOUT.EMPTY_BOUND_COMPONENT;
            outMax[MATH_VECTOR3_INDEXES.X] = GEOMETRY_BUFFER_LAYOUT.EMPTY_BOUND_COMPONENT;
            outMax[MATH_VECTOR3_INDEXES.Y] = GEOMETRY_BUFFER_LAYOUT.EMPTY_BOUND_COMPONENT;
            outMax[MATH_VECTOR3_INDEXES.Z] = GEOMETRY_BUFFER_LAYOUT.EMPTY_BOUND_COMPONENT;
            return;
        }

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

        for (let index = GEOMETRY_BUFFER_LAYOUT.POSITION_START_INDEX; index < positions.length; index += GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT) {
            const x = positions[index + MATH_VECTOR3_INDEXES.X];
            const y = positions[index + MATH_VECTOR3_INDEXES.Y];
            const z = positions[index + MATH_VECTOR3_INDEXES.Z];

            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[MATH_VECTOR3_INDEXES.X] = minX;
        outMin[MATH_VECTOR3_INDEXES.Y] = minY;
        outMin[MATH_VECTOR3_INDEXES.Z] = minZ;
        outMax[MATH_VECTOR3_INDEXES.X] = maxX;
        outMax[MATH_VECTOR3_INDEXES.Y] = maxY;
        outMax[MATH_VECTOR3_INDEXES.Z] = maxZ;
    }

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

        this.#configureAttribute(
            this.#positionBuffer,
            GEOMETRY_BUFFER_LAYOUT.POSITION_ATTRIBUTE_LOCATION,
            GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT
        );

        if (this.#colorBuffer) {
            this.#configureAttribute(
                this.#colorBuffer,
                GEOMETRY_BUFFER_LAYOUT.COLOR_ATTRIBUTE_LOCATION,
                GEOMETRY_LAYOUT.COLOR_COMPONENT_COUNT
            );
        }

        if (this.#uvBuffer) {
            this.#configureAttribute(
                this.#uvBuffer,
                GEOMETRY_BUFFER_LAYOUT.UV_ATTRIBUTE_LOCATION,
                GEOMETRY_LAYOUT.UV_COMPONENT_COUNT
            );
        }

        if (this.#normalBuffer) {
            this.#configureAttribute(
                this.#normalBuffer,
                GEOMETRY_BUFFER_LAYOUT.NORMAL_ATTRIBUTE_LOCATION,
                GEOMETRY_LAYOUT.NORMAL_COMPONENT_COUNT
            );
        }

        webglContext.bindBuffer(webglContext.ELEMENT_ARRAY_BUFFER, this.#indexBufferSolid);
        webglContext.bindVertexArray(null);
    }

    /**
     * Connects one vertex buffer to its attribute location in the current VAO.
     *
     * @param {WebGLBuffer} buffer    - Attribute buffer.
     * @param {number} location       - Shader attribute location.
     * @param {number} componentCount - Components per vertex.
     * @private
     */
    #configureAttribute(buffer, location, componentCount) {
        const webglContext = this.#webglContext;
        webglContext.bindBuffer(webglContext.ARRAY_BUFFER, buffer);
        webglContext.enableVertexAttribArray(location);
        webglContext.vertexAttribPointer(
            location,
            componentCount,
            webglContext.FLOAT,
            GEOMETRY_BUFFER_LAYOUT.ATTRIBUTE_NORMALIZED,
            GEOMETRY_BUFFER_LAYOUT.ATTRIBUTE_NO_STRIDE,
            GEOMETRY_BUFFER_LAYOUT.ATTRIBUTE_NO_OFFSET
        );
    }

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