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 { TORUS_DEFAULTS, TORUS_LIMITS } from '../constants/torus-geometry.js';
import {
DEFAULT_VERTEX_COLOR,
GEOMETRY_SIZES,
GEOMETRY_GRID,
GEOMETRY_LAYOUT,
GEOMETRY_ANGLES,
GEOMETRY_UV_INDEXES,
GEOMETRY_UV
} from '../constants/geometry.js';
/**
* Torus geometry options.
*
* `width` and `height` are interpreted as:
* - `width` = major diameter (center ring diameter)
* - `height` = tube diameter
*
* Segment parameters must be integers `>= 3`.
*
* `colors` supports:
* - Uniform RGB `length === 3`
* - Per-vertex RGB `length === vertexCount * 3`
*
* @typedef {Object} TorusGeometryOptions
* @property {number} [width = 1.5] - Major diameter.
* @property {number} [height = 0.5] - Tube diameter.
* @property {number} [tubularSegments = 32] - Segments around the ring `>= 3`.
* @property {number} [radialSegments = 16] - Segments around the tube `>= 3`.
* @property {Float32Array} [colors] - Color specification buffer.
*/
/**
* Internal geometry buffers produced by `TorusGeometry`.
*
* @typedef {Object} TorusGeometryData
* @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.
*/
/**
* Segmented torus geometry.
*/
export class TorusGeometry extends GeneratedGeometry {
/**
* Normalizes constructor input to a `TorusGeometryOptions` object.
*
* @param {TorusGeometryOptions} options - Options object.
* @returns {Required<TorusGeometryOptions>} - Normalized options.
* @private
*/
static #normalizeOptions(options) {
if (options === null || typeof options !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT) {
throw new TypeError('TorusGeometry expects options as an object.');
}
const {
width = TORUS_DEFAULTS.MAJOR_DIAMETER,
height = TORUS_DEFAULTS.TUBE_DIAMETER,
tubularSegments = TORUS_DEFAULTS.TUBULAR_SEGMENTS,
radialSegments = TORUS_DEFAULTS.RADIAL_SEGMENTS,
colors = DEFAULT_VERTEX_COLOR
} = options;
if (typeof width !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER || typeof height !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER) {
throw new TypeError('TorusGeometry expects width/height as numbers.');
}
if (!Number.isFinite(width) || !Number.isFinite(height)) {
throw new RangeError('TorusGeometry expects finite width/height.');
}
if (!(colors instanceof Float32Array)) {
throw new TypeError('TorusGeometry expects colors as a Float32Array.');
}
return {
width,
height,
tubularSegments : GeometryUtils.normalizeSegmentCount(tubularSegments, 'tubularSegments', TORUS_LIMITS.MIN_SEGMENT_COUNT, 'TorusGeometry'),
radialSegments : GeometryUtils.normalizeSegmentCount(radialSegments, 'radialSegments', TORUS_LIMITS.MIN_SEGMENT_COUNT, 'TorusGeometry'),
colors
};
}
/**
* Generates vertex and index buffers from construction options.
*
* @param {TorusGeometryOptions} [options] - Geometry options.
* @returns {TorusGeometryData} - Generated CPU buffers.
* @protected
*/
static createGeometryData(options = {}) {
const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
const uvComponentCount = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
const normalized = TorusGeometry.#normalizeOptions(options);
const tubularSegments = normalized.tubularSegments;
const radialSegments = normalized.radialSegments;
const tubularVertexCount = tubularSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
const radialVertexCount = radialSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
const vertexCount = tubularVertexCount * radialVertexCount;
const positions = new Float32Array(vertexCount * positionComponentCount);
const normals = new Float32Array(vertexCount * positionComponentCount);
const uvs = new Float32Array(vertexCount * uvComponentCount);
TorusGeometry.#writeVertices(positions, normals, uvs, normalized);
const indicesSolidList = [];
GeometryUtils.appendGridTriangleIndices(indicesSolidList, tubularSegments, radialSegments);
const indicesSolid = GeometryUtils.createIndexArray(vertexCount, indicesSolidList);
const indicesWireframe = GeometryUtils.createWireframeIndicesFromSolidIndices(vertexCount, indicesSolid);
const colors = GeometryUtils.createColorsFromSpec(vertexCount, normalized.colors);
return {
positions,
normals,
uvs,
colors,
indicesSolid,
indicesWireframe
};
}
/**
* Writes the grid positions, normals and texture coordinates.
*
* @param {Float32Array} positions - Output positions.
* @param {Float32Array} normals - Output normals.
* @param {Float32Array} uvs - Output texture coordinates.
* @param {Required<TorusGeometryOptions>} normalized - Normalized geometry options.
* @private
*/
static #writeVertices(positions, normals, uvs, normalized) {
const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
const uvComponentCount = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
const fullTurn = GEOMETRY_ANGLES.FULL_TURN;
const xComponentIndex = MATH_VECTOR3_INDEXES.X;
const yComponentIndex = MATH_VECTOR3_INDEXES.Y;
const zComponentIndex = MATH_VECTOR3_INDEXES.Z;
const majorRadius = normalized.width / GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
const tubeRadius = normalized.height / GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
const tubularSegments = normalized.tubularSegments;
const radialSegments = normalized.radialSegments;
const tubularVertexCount = tubularSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
const radialVertexCount = radialSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
let vertexIndex = MATH_COMMON_VALUES.ZERO;
for (let radialIndex = MATH_COMMON_VALUES.ZERO; radialIndex < radialVertexCount; radialIndex += MATH_COMMON_VALUES.UNIT) {
const vNormalized = radialIndex / radialSegments;
const phi = vNormalized * fullTurn;
const cosPhi = Math.cos(phi);
const sinPhi = Math.sin(phi);
for (let tubularIndex = MATH_COMMON_VALUES.ZERO; tubularIndex < tubularVertexCount; tubularIndex += MATH_COMMON_VALUES.UNIT) {
const uNormalized = tubularIndex / tubularSegments;
const theta = uNormalized * fullTurn;
const cosTheta = Math.cos(theta);
const sinTheta = Math.sin(theta);
const ringRadius = majorRadius + (tubeRadius * cosPhi);
const positionX = ringRadius * cosTheta;
const positionY = tubeRadius * sinPhi;
const positionZ = ringRadius * sinTheta;
const positionBase = vertexIndex * positionComponentCount;
positions[positionBase + xComponentIndex] = positionX;
positions[positionBase + yComponentIndex] = positionY;
positions[positionBase + zComponentIndex] = positionZ;
normals[positionBase + xComponentIndex] = cosTheta * cosPhi;
normals[positionBase + yComponentIndex] = sinPhi;
normals[positionBase + zComponentIndex] = sinTheta * cosPhi;
const uvBase = vertexIndex * uvComponentCount;
uvs[uvBase + GEOMETRY_UV_INDEXES.U] = uNormalized;
uvs[uvBase + GEOMETRY_UV_INDEXES.V] = GEOMETRY_UV.V_FLIP_BASE - vNormalized;
vertexIndex += MATH_COMMON_VALUES.UNIT;
}
}
}
}