import * as GeometryConstants from '../constants/geometry.js';
import * as GeometryUtilsConstants from '../constants/geometry-utils.js';
import { ECMASCRIPT_TYPEOF_RESULTS } from '../constants/ecmascript-types.js';
/**
* Utility class for geometry color and index buffers.
*
* Provides static methods for:
* - expanding a uniform RGB color into a per-vertex buffer
* - choosing between 16-bit and 32-bit index buffers
* - generating sequential vertex indexes
* - extracting unique wireframe edges from triangle indexes
*/
export class GeometryUtils {
/**
* Creates a per-vertex RGB buffer from a uniform color or an existing color buffer.
* A positive `expectedPerVertexLength` overrides the length derived from `vertexCount`.
*
* @param {number} vertexCount - Total vertex count.
* @param {Float32Array} colors - Uniform RGB color or per-vertex colors.
* @param {number} [expectedPerVertexLength = 0] - Optional explicit expected buffer length.
* @returns {Float32Array} - A new uniform-color buffer or the supplied per-vertex buffer.
* @throws {TypeError} When colors have an unsupported type or length.
*/
static createColorsFromSpec(
vertexCount,
colors,
expectedPerVertexLength = GeometryUtilsConstants.GEOMETRY_COLORS.AUTO_LENGTH
) {
if (!(colors instanceof Float32Array)) {
throw new TypeError('GeometryUtils.createColorsFromSpec expects colors as a Float32Array.');
}
const colorConstants = GeometryUtilsConstants.GEOMETRY_COLORS;
const colorIndexes = GeometryConstants.GEOMETRY_COLOR_INDEXES;
const indexConstants = GeometryUtilsConstants.GEOMETRY_INDICES;
const perVertexLength = expectedPerVertexLength > colorConstants.AUTO_LENGTH
? expectedPerVertexLength
: (vertexCount * colorConstants.COMPONENT_COUNT);
// Repeat the same RGB color for every vertex in a new buffer
if (colors.length === colorConstants.COMPONENT_COUNT) {
const colorBuffer = new Float32Array(perVertexLength);
for (
let index = indexConstants.FIRST_VERTEX_INDEX;
index < vertexCount;
index += indexConstants.SEQUENTIAL_INDEX_INCREMENT
) {
const baseIndex = index * colorConstants.COMPONENT_COUNT;
colorBuffer[baseIndex + colorIndexes.RED] = colors[colorIndexes.RED];
colorBuffer[baseIndex + colorIndexes.GREEN] = colors[colorIndexes.GREEN];
colorBuffer[baseIndex + colorIndexes.BLUE] = colors[colorIndexes.BLUE];
}
return colorBuffer;
}
// Reuse the supplied per-vertex buffer without copying its components
if (colors.length === perVertexLength) {
return colors;
}
/* eslint-disable indent */
throw new TypeError(
'GeometryUtils.createColorsFromSpec expects colors length to be {uniform} (uniform) or {vertex} (per-vertex).'
.replace('{uniform}', String(colorConstants.COMPONENT_COUNT))
.replace('{vertex}' , String(perVertexLength))
);
/* eslint-enable indent */
}
/**
* Creates an index buffer using the smallest type required by the vertex count.
* The largest vertex index is `vertexCount - 1`.
*
* @param {number} vertexCount - Total vertex count.
* @param {number[]} indices - Index list.
* @returns {Uint16Array | Uint32Array} - A new typed index buffer.
* @throws {TypeError} When indices are not an ordinary array.
*/
static createIndexArray(vertexCount, indices) {
if (!Array.isArray(indices)) {
throw new TypeError('GeometryUtils.createIndexArray expects indices as an array of numbers.');
}
const indexConstants = GeometryUtilsConstants.GEOMETRY_INDICES;
const requiresUint32 =
(vertexCount - indexConstants.VERTEX_COUNT_TO_MAX_INDEX_OFFSET)
> indexConstants.MAX_UINT16_INDEX_VALUE;
if (requiresUint32) {
return new Uint32Array(indices);
}
return new Uint16Array(indices);
}
/**
* Creates a sequential index buffer without an intermediate JavaScript array.
* The buffer contains every vertex index, from zero through `vertexCount - 1`.
*
* @param {number} vertexCount - Total vertex count.
* @returns {Uint16Array | Uint32Array} - A new sequential index buffer.
* @throws {TypeError} When `vertexCount` is not a finite number.
* @throws {RangeError} When `vertexCount` is negative or non-integer.
*/
static createSequentialIndexArray(vertexCount) {
if (typeof vertexCount !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(vertexCount)) {
throw new TypeError('GeometryUtils.createSequentialIndexArray expects vertexCount as a finite number.');
}
const indexConstants = GeometryUtilsConstants.GEOMETRY_INDICES;
if (!Number.isInteger(vertexCount) || vertexCount < indexConstants.MIN_VERTEX_COUNT) {
throw new RangeError('GeometryUtils.createSequentialIndexArray expects vertexCount as a non-negative integer.');
}
if (vertexCount === indexConstants.MIN_VERTEX_COUNT) {
return new Uint16Array(indexConstants.MIN_VERTEX_COUNT);
}
const requiresUint32 =
(vertexCount - indexConstants.VERTEX_COUNT_TO_MAX_INDEX_OFFSET)
> indexConstants.MAX_UINT16_INDEX_VALUE;
const indexArray = requiresUint32 ? new Uint32Array(vertexCount) : new Uint16Array(vertexCount);
for (
let index = indexConstants.FIRST_VERTEX_INDEX;
index < vertexCount;
index += indexConstants.SEQUENTIAL_INDEX_INCREMENT
) {
indexArray[index] = index;
}
return indexArray;
}
/**
* Creates a wireframe line index buffer from a solid triangle index buffer.
* Shared edges are included once, regardless of their direction in each triangle.
*
* @param {number} vertexCount - Total vertex count.
* @param {Uint16Array | Uint32Array} triangleIndices - Solid triangle indexes.
* @returns {Uint16Array | Uint32Array} - Wireframe indexes, with two indexes per edge.
* @throws {TypeError} When triangle indices have an unsupported type.
*/
static createWireframeIndicesFromSolidIndices(vertexCount, triangleIndices) {
if (!(triangleIndices instanceof Uint16Array) && !(triangleIndices instanceof Uint32Array)) {
throw new TypeError('GeometryUtils.createWireframeIndicesFromSolidIndices expects indices as Uint16Array or Uint32Array.');
}
const indexConstants = GeometryUtilsConstants.GEOMETRY_INDICES;
const triangleIndexes = GeometryConstants.GEOMETRY_TRIANGLE_INDEXES;
const edgeSet = new Set();
const lines = [];
for (
let index = indexConstants.FIRST_VERTEX_INDEX;
index < triangleIndices.length;
index += indexConstants.TRIANGLE_INDEX_STRIDE
) {
const firstVertexIndex = triangleIndices[index + triangleIndexes.FIRST];
const secondVertexIndex = triangleIndices[index + triangleIndexes.SECOND];
const thirdVertexIndex = triangleIndices[index + triangleIndexes.THIRD];
GeometryUtils.#addEdge(edgeSet, lines, firstVertexIndex , secondVertexIndex);
GeometryUtils.#addEdge(edgeSet, lines, secondVertexIndex , thirdVertexIndex);
GeometryUtils.#addEdge(edgeSet, lines, thirdVertexIndex , firstVertexIndex);
}
return GeometryUtils.createIndexArray(vertexCount, lines);
}
/**
* Floors a finite segment count and checks the minimum required by a geometry.
*
* @param {number} value - Requested segment count.
* @param {string} optionName - Option name included in errors.
* @param {number} minimumValue - Minimum supported count.
* @param {string} geometryName - Geometry class name included in errors.
* @returns {number} - Validated integer segment count.
* @throws {TypeError} When the value is not a finite number.
* @throws {RangeError} When the rounded count is below the minimum.
*/
static normalizeSegmentCount(value, optionName, minimumValue, geometryName) {
if (typeof value !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || !Number.isFinite(value)) {
throw new TypeError(`${geometryName} expects ${optionName} as a finite number.`);
}
const segmentCount = Math.floor(value);
if (segmentCount < minimumValue) {
throw new RangeError(`${geometryName} expects ${optionName} to be >= ${minimumValue}.`);
}
return segmentCount;
}
/**
* Adds an undirected edge to the line list, if it has not already been added.
* Stores the smaller vertex index first so reversed edges have the same key.
*
* @param {Set<string>} edgeSet - Keys of edges already added to the buffer.
* @param {number[]} lines - Output line index list.
* @param {number} indexA - First vertex index.
* @param {number} indexB - Second vertex index.
* @private
*/
static #addEdge(edgeSet, lines, indexA, indexB) {
const minVertexIndex = Math.min(indexA, indexB);
const maxVertexIndex = Math.max(indexA, indexB);
const edgeKey = String(minVertexIndex)
+ GeometryUtilsConstants.GEOMETRY_EDGE_KEY_SEPARATOR
+ String(maxVertexIndex);
if (edgeSet.has(edgeKey)) {
return;
}
edgeSet.add(edgeKey);
lines.push(minVertexIndex, maxVertexIndex);
}
/**
* Appends triangles for a rectangular vertex grid in row order.
* Dimensions and offsets come from normalized geometry options.
*
* @param {number[]} indices - Output triangle indexes.
* @param {number} columnSegments - Number of cells per row.
* @param {number} rowSegments - Number of cell rows.
* @param {number} [vertexOffset = 0] - First vertex in the grid.
* @param {boolean} [reverse = false] - Whether to reverse each triangle's winding.
*/
static appendGridTriangleIndices(
indices,
columnSegments,
rowSegments,
vertexOffset = GeometryUtilsConstants.GEOMETRY_INDICES.FIRST_VERTEX_INDEX,
reverse = false
) {
const firstIndex = GeometryUtilsConstants.GEOMETRY_INDICES.FIRST_VERTEX_INDEX;
const increment = GeometryUtilsConstants.GEOMETRY_INDICES.SEQUENTIAL_INDEX_INCREMENT;
const rowVertexCount = columnSegments + GeometryConstants.GEOMETRY_GRID.VERTEX_INCREMENT;
const nextOffset = GeometryConstants.GEOMETRY_GRID.NEXT_VERTEX_OFFSET;
for (let rowIndex = firstIndex; rowIndex < rowSegments; rowIndex += increment) {
for (let columnIndex = firstIndex; columnIndex < columnSegments; columnIndex += increment) {
const topLeft = vertexOffset + (rowIndex * rowVertexCount) + columnIndex;
const topRight = topLeft + nextOffset;
const bottomLeft = topLeft + rowVertexCount;
const bottomRight = bottomLeft + nextOffset;
if (reverse) {
indices.push(topLeft, topRight, bottomLeft);
indices.push(topRight, bottomRight, bottomLeft);
} else {
indices.push(topLeft, bottomLeft, topRight);
indices.push(topRight, bottomLeft, bottomRight);
}
}
}
}
}