import { Geometry } from './geometry.js';
import {
DEFAULT_VERTEX_COLOR,
createColorsFromSpec,
createIndexArray,
createWireframeIndicesFromSolidIndices
} from './geometry-utils.js';
/**
* Default box edge size.
*
* @type {number}
*/
const DEFAULT_BOX_SIZE = 1.0;
/**
* Default segment count for each box axis.
*
* @type {number}
*/
const DEFAULT_SEGMENT_COUNT = 1;
/**
* Divisor used to compute the half-size from the full size.
*
* @type {number}
*/
const HALF_SIZE_DIVISOR = 2.0;
/**
* Number of float components per `vec3` (position/normal).
*
* @type {number}
*/
const VEC3_COMPONENT_COUNT = 3;
/**
* Number of box faces.
*
* @type {number}
*/
const BOX_FACE_COUNT = 6;
/**
* Expected length for per-face RGB colors (6 faces * 3).
*
* @type {number}
*/
const COLORS_PER_FACE_LENGTH = BOX_FACE_COUNT * VEC3_COMPONENT_COUNT;
/**
* Minimum segment count supported by segmented geometries.
*
* @type {number}
*/
const MIN_SEGMENT_COUNT = 1;
/**
* Adds one vertex per grid intersection, so vertex count along an axis is `segments + 1`.
*
* @type {number}
*/
const VERTICES_PER_SEGMENT_INCREMENT = 1;
/**
* Used to center face coordinates around the origin: `(t - 0.5) * size`.
*
* @type {number}
*/
const CENTER_T_OFFSET = 0.5;
/**
* UV/V coordinate is flipped to keep (0, 0) at top-left.
*
* @type {number}
*/
const UV_V_FLIP_BASE = 1.0;
/**
* When segments are positive, the divisor is never 0. This value is used as fallback.
*
* @type {number}
*/
const DEFAULT_T_VALUE = 0.0;
/**
* Sentinel segment count that would cause division by zero in normalization.
*
* @type {number}
*/
const ZERO_SEGMENT_COUNT = 0;
/**
* Offset to move from a vertex to the next vertex in the same row.
*
* @type {number}
*/
const NEXT_VERTEX_OFFSET = 1;
/**
* 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/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 Geometry {
/**
* @param {WebGL2RenderingContext} webglContext - WebGL2 rendering context.
* @param {BoxGeometryOptions | number} [optionsOrSize] - Options object or numeric size.
*/
constructor(webglContext, optionsOrSize = {}) {
const options = BoxGeometry.#normalizeOptions(optionsOrSize);
const data = BoxGeometry.#createGeometryData(options);
super(
webglContext,
data.positions,
data.colors,
data.indicesSolid,
data.indicesWireframe,
data.uvs,
data.normals
);
}
/**
* 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 === 'number') {
return {
size : optionsOrSize,
width : optionsOrSize,
height : optionsOrSize,
depth : optionsOrSize,
widthSegments : DEFAULT_SEGMENT_COUNT,
heightSegments : DEFAULT_SEGMENT_COUNT,
depthSegments : DEFAULT_SEGMENT_COUNT,
colors : DEFAULT_VERTEX_COLOR
};
}
if (optionsOrSize === null || typeof optionsOrSize !== 'object') {
throw new TypeError('`BoxGeometry` expects options as an object or a number.');
}
const {
size = DEFAULT_BOX_SIZE,
width = size,
height = size,
depth = size,
widthSegments = DEFAULT_SEGMENT_COUNT,
heightSegments = DEFAULT_SEGMENT_COUNT,
depthSegments = DEFAULT_SEGMENT_COUNT,
colors = DEFAULT_VERTEX_COLOR
} = optionsOrSize;
if (typeof width !== 'number' || typeof height !== 'number' || typeof depth !== 'number') {
throw new TypeError('`BoxGeometry` expects `width/height/depth` as numbers.');
}
if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(depth)) {
throw new RangeError('`BoxGeometry` expects finite `width/height/depth`.');
}
if (!(colors instanceof Float32Array)) {
throw new TypeError('`BoxGeometry` expects colors as a `Float32Array`.');
}
return {
size,
width,
height,
depth,
widthSegments : BoxGeometry.#normalizeSegmentCount(widthSegments , 'widthSegments'),
heightSegments : BoxGeometry.#normalizeSegmentCount(heightSegments , 'heightSegments'),
depthSegments : BoxGeometry.#normalizeSegmentCount(depthSegments , 'depthSegments'),
colors
};
}
/**
* Normalizes and validates a segment count parameter.
*
* @param {number} value - Segment count value.
* @param {string} optionName - Name of the option for error messages.
* @returns {number} - Normalized integer `>= 1`.
* @private
*/
static #normalizeSegmentCount(value, optionName) {
if (typeof value !== 'number' || !Number.isFinite(value)) {
throw new TypeError('`BoxGeometry` expects `{name}` as a finite number.'.replace('{name}', optionName));
}
const intValue = Math.floor(value);
if (intValue < MIN_SEGMENT_COUNT) {
/* eslint-disable indent */
throw new RangeError(
'`BoxGeometry` expects `{name}` to be `>= {min}`.'
.replace('{name}', optionName)
.replace('{min}', String(MIN_SEGMENT_COUNT))
);
/* eslint-enable indent */
}
return intValue;
}
/**
* Creates full geometry data for a segmented box.
*
* @param {Required<BoxGeometryOptions>} options - Normalized options.
* @returns {BoxGeometryData} - Geometry buffers.
*
* @private
*/
static #createGeometryData(options) {
const halfWidth = options.width / HALF_SIZE_DIVISOR;
const halfHeight = options.height / HALF_SIZE_DIVISOR;
const halfDepth = options.depth / HALF_SIZE_DIVISOR;
const positions = [];
const normals = [];
const uvs = [];
const faceVertexCounts = [];
const indicesSolid = [];
let vertexOffset = 0;
// Face definitions ensure `normal === normalize(cross(axisU, axisV))`.
const faces = [
// Front (+Z)
{
axisU : [ 1, 0, 0],
axisV : [ 0, 1, 0],
normal : [ 0, 0, 1],
fixed : halfDepth,
sizeU : options.width,
sizeV : options.height,
segmentsU : options.widthSegments,
segmentsV : options.heightSegments
},
// Back (-Z)
{
axisU : [-1, 0, 0],
axisV : [ 0, 1, 0],
normal : [ 0, 0,-1],
fixed : halfDepth,
sizeU : options.width,
sizeV : options.height,
segmentsU : options.widthSegments,
segmentsV : options.heightSegments
},
// Top (+Y)
{
axisU : [ 1, 0, 0],
axisV : [ 0, 0,-1],
normal : [ 0, 1, 0],
fixed : halfHeight,
sizeU : options.width,
sizeV : options.depth,
segmentsU : options.widthSegments,
segmentsV : options.depthSegments
},
// Bottom (-Y)
{
axisU : [ 1, 0, 0],
axisV : [ 0, 0, 1],
normal : [ 0,-1, 0],
fixed : halfHeight,
sizeU : options.width,
sizeV : options.depth,
segmentsU : options.widthSegments,
segmentsV : options.depthSegments
},
// Right (+X)
{
axisU : [ 0, 0,-1],
axisV : [ 0, 1, 0],
normal : [ 1, 0, 0],
fixed : halfWidth,
sizeU : options.depth,
sizeV : options.height,
segmentsU : options.depthSegments,
segmentsV : options.heightSegments
},
// Left (-X)
{
axisU : [ 0, 0, 1],
axisV : [ 0, 1, 0],
normal : [-1, 0, 0],
fixed : halfWidth,
sizeU : options.depth,
sizeV : options.height,
segmentsU : options.depthSegments,
segmentsV : options.heightSegments
}
];
for (let faceIndex = 0; faceIndex < faces.length; faceIndex += 1) {
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(options.colors, vertexCount, faceVertexCounts);
const indicesSolidTyped = createIndexArray(vertexCount, indicesSolid);
const indicesWireframe = 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 segmentsU = face.segmentsU;
const segmentsV = face.segmentsV;
const uVertexCount = segmentsU + VERTICES_PER_SEGMENT_INCREMENT;
const vVertexCount = segmentsV + VERTICES_PER_SEGMENT_INCREMENT;
for (let vIndex = 0; vIndex < vVertexCount; vIndex += 1) {
const vNormalized = (segmentsV === ZERO_SEGMENT_COUNT) ? DEFAULT_T_VALUE : (vIndex / segmentsV);
const vLocalOffset = (vNormalized - CENTER_T_OFFSET) * face.sizeV;
for (let uIndex = 0; uIndex < uVertexCount; uIndex += 1) {
const uNormalized = (segmentsU === ZERO_SEGMENT_COUNT) ? DEFAULT_T_VALUE : (uIndex / segmentsU);
const uLocalOffset = (uNormalized - CENTER_T_OFFSET) * face.sizeU;
const positionX =
(face.axisU[0] * uLocalOffset) +
(face.axisV[0] * vLocalOffset) +
(face.normal[0] * face.fixed);
const positionY =
(face.axisU[1] * uLocalOffset) +
(face.axisV[1] * vLocalOffset) +
(face.normal[1] * face.fixed);
const positionZ =
(face.axisU[2] * uLocalOffset) +
(face.axisV[2] * vLocalOffset) +
(face.normal[2] * face.fixed);
positions.push(positionX, positionY, positionZ);
normals.push(face.normal[0], face.normal[1], face.normal[2]);
uvs.push(uNormalized, UV_V_FLIP_BASE - vNormalized);
}
}
// Indices:
for (let vIndex = 0; vIndex < segmentsV; vIndex += 1) {
for (let uIndex = 0; uIndex < segmentsU; uIndex += 1) {
const topLeftVertexIndex = vertexOffset + (vIndex * uVertexCount) + uIndex;
const topRightVertexIndex = topLeftVertexIndex + NEXT_VERTEX_OFFSET;
const bottomLeftVertexIndex = topLeftVertexIndex + uVertexCount;
const bottomRightVertexIndex = bottomLeftVertexIndex + NEXT_VERTEX_OFFSET;
// indicesSolid.push(topLeftVertexIndex, bottomLeftVertexIndex, topRightVertexIndex);
// indicesSolid.push(topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
indicesSolid.push(topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex);
indicesSolid.push(topRightVertexIndex, bottomRightVertexIndex, bottomLeftVertexIndex);
}
}
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) {
// Per-face colors:
if (colorsSpec.length === COLORS_PER_FACE_LENGTH) {
const colorBuffer = new Float32Array(vertexCount * VEC3_COMPONENT_COUNT);
let vertexBase = 0;
for (let faceIndex = 0; faceIndex < BOX_FACE_COUNT; faceIndex += 1) {
const faceVertexCount = faceVertexCounts[faceIndex];
const faceColorBase = faceIndex * VEC3_COMPONENT_COUNT;
const red = colorsSpec[faceColorBase + 0];
const green = colorsSpec[faceColorBase + 1];
const blue = colorsSpec[faceColorBase + 2];
for (let i = 0; i < faceVertexCount; i += 1) {
const destinationComponentOffset = (vertexBase + i) * VEC3_COMPONENT_COUNT;
colorBuffer[destinationComponentOffset + 0] = red;
colorBuffer[destinationComponentOffset + 1] = green;
colorBuffer[destinationComponentOffset + 2] = blue;
}
vertexBase += faceVertexCount;
}
return colorBuffer;
}
// Uniform or per-vertex:
return createColorsFromSpec(vertexCount, colorsSpec);
}
}