import { GeometryUtils } from './geometry-utils.js';
import { GeneratedGeometry } from './generated-geometry.js';
import { MATH_COMMON_VALUES, MATH_VECTOR3_INDEXES } from '../constants/math.js';
import { ECMASCRIPT_TYPEOF_RESULTS } from '../constants/ecmascript-types.js';
import {
DEFAULT_VERTEX_COLOR,
GEOMETRY_SIZES,
GEOMETRY_GRID,
GEOMETRY_UV,
GEOMETRY_LAYOUT,
GEOMETRY_COLOR_INDEXES
} from '../constants/geometry.js';
import {
BOX_FACES,
BOX_DEFAULTS,
BOX_LAYOUT,
BOX_LIMITS
} from '../constants/box-geometry.js';
/**
* Options used by `BoxGeometry` constructor.
*
* `colors` supports:
* - Uniform RGB (`length === 3`)
* - Per-face RGB (`length === 18`)
* - Per-vertex RGB (`length === vertexCount * 3`)
*
* Segment parameters must be `integers >= 1`.
*
* @typedef {Object} BoxGeometryOptions
* @property {number} [size = 1.0] - Convenience cube size (applies to width, height or depth).
* @property {number} [width = size] - Box width along the X axis.
* @property {number} [height = size] - Box height along the Y axis.
* @property {number} [depth = size] - Box depth along the Z axis.
* @property {number} [widthSegments = 1] - Subdivisions along the X axis.
* @property {number} [heightSegments = 1] - Subdivisions along the Y axis.
* @property {number} [depthSegments = 1] - Subdivisions along the Z axis.
* @property {Float32Array} [colors] - Color specification buffer.
*/
/**
* Internal geometry buffers produced by `BoxGeometry`.
*
* @typedef {Object} BoxGeometryData
* @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.
*/
/**
* Internal face grid definition for `BoxGeometry`.
*
* @typedef {Object} BoxFaceDefinition
* @property {number[]} axisU - U axis direction.
* @property {number[]} axisV - V axis direction.
* @property {number[]} normal - Face normal.
* @property {number} fixed - Fixed coordinate value for the remaining axis.
* @property {number} sizeU - Face size along U.
* @property {number} sizeV - Face size along V.
* @property {number} segmentsU - Segment count along U `>= 1`.
* @property {number} segmentsV - Segment count along V `>= 1`.
*/
/**
* Segmented box geometry (cube, when `width = height = depth`).
* Generates positions, normals, UVs and both solid and wireframe indices.
*/
export class BoxGeometry extends GeneratedGeometry {
/**
* Normalizes constructor input to a `BoxGeometryOptions` object.
*
* @param {BoxGeometryOptions | number} optionsOrSize - Options object or numeric size.
* @returns {Required<BoxGeometryOptions>} - Normalized options.
* @private
*/
static #normalizeOptions(optionsOrSize) {
if (typeof optionsOrSize === ECMASCRIPT_TYPEOF_RESULTS.NUMBER) {
return {
size : optionsOrSize,
width : optionsOrSize,
height : optionsOrSize,
depth : optionsOrSize,
widthSegments : BOX_DEFAULTS.SEGMENT_COUNT,
heightSegments : BOX_DEFAULTS.SEGMENT_COUNT,
depthSegments : BOX_DEFAULTS.SEGMENT_COUNT,
colors : DEFAULT_VERTEX_COLOR
};
}
if (optionsOrSize === null || typeof optionsOrSize !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT) {
throw new TypeError('BoxGeometry expects options as an object or a number.');
}
const {
size = BOX_DEFAULTS.SIZE,
width = size,
height = size,
depth = size,
widthSegments = BOX_DEFAULTS.SEGMENT_COUNT,
heightSegments = BOX_DEFAULTS.SEGMENT_COUNT,
depthSegments = BOX_DEFAULTS.SEGMENT_COUNT,
colors = DEFAULT_VERTEX_COLOR
} = optionsOrSize;
if (typeof width !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER ||
typeof height !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER ||
typeof depth !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER) {
throw new TypeError('BoxGeometry expects width, height or depth as numbers.');
}
if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(depth)) {
throw new RangeError('BoxGeometry expects finite width, height or depth.');
}
if (!(colors instanceof Float32Array)) {
throw new TypeError('BoxGeometry expects colors as a Float32Array.');
}
return {
size,
width,
height,
depth,
widthSegments : GeometryUtils.normalizeSegmentCount(widthSegments, 'widthSegments', BOX_LIMITS.MIN_SEGMENT_COUNT, 'BoxGeometry'),
heightSegments : GeometryUtils.normalizeSegmentCount(heightSegments, 'heightSegments', BOX_LIMITS.MIN_SEGMENT_COUNT, 'BoxGeometry'),
depthSegments : GeometryUtils.normalizeSegmentCount(depthSegments, 'depthSegments', BOX_LIMITS.MIN_SEGMENT_COUNT, 'BoxGeometry'),
colors
};
}
/**
* Generates vertex and index buffers from construction options.
*
* @param {BoxGeometryOptions | number} [options] - Geometry options.
* @returns {BoxGeometryData} - Generated CPU buffers.
* @protected
*/
static createGeometryData(options = {}) {
const normalized = BoxGeometry.#normalizeOptions(options);
const faces = BoxGeometry.#createFaces(normalized);
const positions = [];
const normals = [];
const uvs = [];
const faceVertexCounts = [];
const indicesSolid = [];
let vertexOffset = MATH_COMMON_VALUES.ZERO;
for (let faceIndex = MATH_COMMON_VALUES.ZERO; faceIndex < faces.length; faceIndex += MATH_COMMON_VALUES.UNIT) {
const face = faces[faceIndex];
const localVertexCount = BoxGeometry.#appendFaceGrid(
positions,
normals,
uvs,
indicesSolid,
vertexOffset,
face
);
faceVertexCounts.push(localVertexCount);
vertexOffset += localVertexCount;
}
const vertexCount = vertexOffset;
const colors = BoxGeometry.#createColors(normalized.colors, vertexCount, faceVertexCounts);
const indicesSolidTyped = GeometryUtils.createIndexArray(vertexCount, indicesSolid);
const indicesWireframe = GeometryUtils.createWireframeIndicesFromSolidIndices(vertexCount, indicesSolidTyped);
return {
positions : new Float32Array(positions),
normals : new Float32Array(normals),
uvs : new Float32Array(uvs),
colors,
indicesSolid : indicesSolidTyped,
indicesWireframe
};
}
/**
* Appends a single face grid to the output buffers.
*
* @param {number[]} positions - Output positions.
* @param {number[]} normals - Output normals.
* @param {number[]} uvs - Output UVs.
* @param {number[]} indicesSolid - Output solid indices (triangles).
* @param {number} vertexOffset - Starting vertex index for this face.
* @param {BoxFaceDefinition} face - Face definition.
* @returns {number} - Number of vertices appended for this face.
* @private
*/
static #appendFaceGrid(positions, normals, uvs, indicesSolid, vertexOffset, face) {
const centerOffset = GEOMETRY_GRID.CENTER_OFFSET;
const xComponentIndex = MATH_VECTOR3_INDEXES.X;
const yComponentIndex = MATH_VECTOR3_INDEXES.Y;
const zComponentIndex = MATH_VECTOR3_INDEXES.Z;
const segmentsU = face.segmentsU;
const segmentsV = face.segmentsV;
const uVertexCount = segmentsU + GEOMETRY_GRID.VERTEX_INCREMENT;
const vVertexCount = segmentsV + GEOMETRY_GRID.VERTEX_INCREMENT;
for (let vIndex = MATH_COMMON_VALUES.ZERO; vIndex < vVertexCount; vIndex += MATH_COMMON_VALUES.UNIT) {
const vNormalized = vIndex / segmentsV;
const vLocalOffset = (vNormalized - centerOffset) * face.sizeV;
for (let uIndex = MATH_COMMON_VALUES.ZERO; uIndex < uVertexCount; uIndex += MATH_COMMON_VALUES.UNIT) {
const uNormalized = uIndex / segmentsU;
const uLocalOffset = (uNormalized - centerOffset) * face.sizeU;
const positionX =
(face.axisU[xComponentIndex] * uLocalOffset) +
(face.axisV[xComponentIndex] * vLocalOffset) +
(face.normal[xComponentIndex] * face.fixed);
const positionY =
(face.axisU[yComponentIndex] * uLocalOffset) +
(face.axisV[yComponentIndex] * vLocalOffset) +
(face.normal[yComponentIndex] * face.fixed);
const positionZ =
(face.axisU[zComponentIndex] * uLocalOffset) +
(face.axisV[zComponentIndex] * vLocalOffset) +
(face.normal[zComponentIndex] * face.fixed);
positions.push(positionX, positionY, positionZ);
normals.push(face.normal[xComponentIndex], face.normal[yComponentIndex], face.normal[zComponentIndex]);
uvs.push(uNormalized, GEOMETRY_UV.V_FLIP_BASE - vNormalized);
}
}
GeometryUtils.appendGridTriangleIndices(indicesSolid, segmentsU, segmentsV, vertexOffset, true);
return uVertexCount * vVertexCount;
}
/**
* Creates a per-vertex color buffer for the final vertex count.
*
* @param {Float32Array} colorsSpec - Color specification.
* @param {number} vertexCount - Total vertex count.
* @param {number[]} faceVertexCounts - Vertex count for each face, in face order.
* @returns {Float32Array} - Per-vertex RGB buffer.
* @private
*/
static #createColors(colorsSpec, vertexCount, faceVertexCounts) {
const colorComponentCount = GEOMETRY_LAYOUT.COLOR_COMPONENT_COUNT;
const redIndex = GEOMETRY_COLOR_INDEXES.RED;
const greenIndex = GEOMETRY_COLOR_INDEXES.GREEN;
const blueIndex = GEOMETRY_COLOR_INDEXES.BLUE;
// Repeat each face color for all vertices belonging to that face
if (colorsSpec.length === BOX_LAYOUT.COLORS_PER_FACE_LENGTH) {
const colorBuffer = new Float32Array(vertexCount * colorComponentCount);
let vertexBase = MATH_COMMON_VALUES.ZERO;
for (let faceIndex = MATH_COMMON_VALUES.ZERO; faceIndex < BOX_LAYOUT.FACE_COUNT; faceIndex += MATH_COMMON_VALUES.UNIT) {
const faceVertexCount = faceVertexCounts[faceIndex];
const faceColorBase = faceIndex * colorComponentCount;
const red = colorsSpec[faceColorBase + redIndex];
const green = colorsSpec[faceColorBase + greenIndex];
const blue = colorsSpec[faceColorBase + blueIndex];
for (let i = MATH_COMMON_VALUES.ZERO; i < faceVertexCount; i += MATH_COMMON_VALUES.UNIT) {
const destinationComponentOffset = (vertexBase + i) * colorComponentCount;
colorBuffer[destinationComponentOffset + redIndex] = red;
colorBuffer[destinationComponentOffset + greenIndex] = green;
colorBuffer[destinationComponentOffset + blueIndex] = blue;
}
vertexBase += faceVertexCount;
}
return colorBuffer;
}
// Expand a uniform color or reuse the supplied per-vertex colors
return GeometryUtils.createColorsFromSpec(vertexCount, colorsSpec);
}
/**
* Describes the six outward-facing box grids in their existing face order.
*
* @param {Required<BoxGeometryOptions>} normalized - Normalized geometry options.
* @returns {BoxFaceDefinition[]}
* @private
*/
static #createFaces(normalized) {
const faces = [];
for (const definition of BOX_FACES) {
faces.push({
axisU : definition.axisU,
axisV : definition.axisV,
normal : definition.normal,
fixed : normalized[definition.fixedDimension] / GEOMETRY_SIZES.HALF_SIZE_DIVISOR,
sizeU : normalized[definition.sizeU],
sizeV : normalized[definition.sizeV],
segmentsU : normalized[definition.segmentsU],
segmentsV : normalized[definition.segmentsV]
});
}
return faces;
}
}