Source: builders/MeshBuilder.js

import { MathUtils } from '../math/MathUtils';

/**
 * @class
 * The mesh builder.
 * @memberof THING
 */
class MeshBuilder {

	/**
	 * @typedef {Object} MeshResult
	 * @property {Array<Number>} position The position.
	 * @property {Array<Number>} normal The normal.
	 * @property {Array<Number>} uv The uv.
	 * @property {Array<Number>} index The index of position.
	 */

	/**
	 * Create plane.
	 * @private
	 */
	static createPlane(width = 1, height = 1, widthSegments = 1, heightSegments = 1) {
		const plane = {};
		plane.position = [];
		plane.normal = [];
		plane.uv = [];
		plane.index = [];

		const segmentWidth = width / widthSegments;
		const segmentHeight = height / heightSegments;

		for (let j = 0; j <= heightSegments; j++) {
			const yPos = j * segmentHeight - height / 2;
			for (let i = 0; i <= widthSegments; i++) {
				const xPos = i * segmentWidth - width / 2;

				plane.position.push(xPos, yPos, 0);
				plane.normal.push(0, 0, 1);
				plane.uv.push(i / widthSegments, j / heightSegments);
			}
		}

		for (let j = 0; j < heightSegments; j++) {
			for (let i = 0; i < widthSegments; i++) {
				const a = i + (widthSegments + 1) * j;
				const b = i + (widthSegments + 1) * (j + 1);
				const c = a + 1;
				const d = b + 1;

				plane.index.push(a, b, c);
				plane.index.push(b, d, c);
			}
		}

		return plane;
	}

	/**
	 * Create circle.
	 * @param {Object} options The options.
	 * @property {Number} options.radius The radius.
	 * @property {Number} options.segments The number of disc segments.
	 * @property {Number} options.startRad The starting angle.
	 * @returns {MeshResult}
	 * @example
	 * let cirecle = THING.MeshBuilder.createCircle();
	 * // @expect(cirecle.index.length == 192 );
	 */
	static createCircle({ radius = 1, segments = 64, startRad = 0 } = {}) {
		let circle = {};
		circle.position = [];
		circle.normal = [];
		circle.uv = [];
		circle.index = [];

		circle.position.push(0, 0, 0);
		circle.normal.push(0, 0, 1);
		circle.uv.push(0.5, 0.5);

		for (let s = 0, i = 3; s <= segments; s++, i += 3) {
			const segment = startRad + s / segments * MathUtils.PI * 2;
			circle.position.push(radius * MathUtils.cos(segment), radius * MathUtils.sin(segment), 0);
			circle.normal.push(0, 0, 1);
			circle.uv.push((circle.position[i] / radius + 1) / 2, (circle.position[i + 1] / radius + 1) / 2);
		}
		for (let i = 1; i <= segments; i++) {
			circle.index.push(i, i + 1, 0);
		}

		return circle;
	}

	/**
	 * Create cylinder.
	 * @param {Object} options The options.
	 * @property {Number} options.radiusTop The top radius.
	 * @property {Number} options.radiusBottom The bottom radius.
	 * @property {Number} options.height The height.
	 * @property {Number} options.radialSegments The number of divisions.
	 * @property {Number} options.heightSegments The number of height divisions.
	 * @property {Boolean} options.openEnded The calculate the top and bottom surfaces.
	 * @property {Number} options.thetaStart The starting angle.
	 * @property {Number} options.thetaLength The end angle.
	 * @property {Number} options.poslength The offset.
	 * @returns {MeshResult}
	 */
	static createCylinder({ radiusTop = 1, radiusBottom = 1, height = 2, radialSegments = 64, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = MathUtils.PI * 2, poslength = 0 } = {}) {
		let cylinder = {};
		cylinder.position = [];
		cylinder.normal = [];
		cylinder.uv = [];
		cylinder.index = [];

		radialSegments = MathUtils.floor(radialSegments);
		heightSegments = MathUtils.floor(heightSegments);

		let indexTemp = 0;
		const indexArray = [];
		const halfHeight = height / 2;

		generateTorso();

		if (openEnded === false) {
			if (radiusTop > 0) generateCap(true);
			if (radiusBottom > 0) generateCap(false);
		}

		function generateTorso() {
			const slope = (radiusBottom - radiusTop) / height;
			for (let y = 0; y <= heightSegments; y++) {
				const indexRow = [];
				const v = y / heightSegments;
				const radius = v * (radiusBottom - radiusTop) + radiusTop;

				for (let x = 0; x <= radialSegments; x++) {
					const u = x / radialSegments;
					const theta = u * thetaLength + thetaStart;
					const sinTheta = MathUtils.sin(theta);
					const cosTheta = MathUtils.cos(theta);
					const normalNor = MathUtils.normalizeVector([sinTheta, slope, cosTheta]);

					cylinder.position.push(radius * sinTheta, -v * height + halfHeight, radius * cosTheta);
					cylinder.normal.push(normalNor[0], normalNor[1], normalNor[2]);
					cylinder.uv.push(u, 1 - v);
					indexRow.push(indexTemp++);
				}

				indexArray.push(indexRow);
			}

			for (let x = 0; x < radialSegments; x++) {
				for (let y = 0; y < heightSegments; y++) {
					const a = indexArray[y][x];
					const b = indexArray[y + 1][x];
					const c = indexArray[y + 1][x + 1];
					const d = indexArray[y][x + 1];

					cylinder.index.push(a + poslength, b + poslength, d + poslength);
					cylinder.index.push(b + poslength, c + poslength, d + poslength);
				}
			}

		}

		function generateCap(top) {
			const centerIndexStart = indexTemp;
			const radius = (top === true) ? radiusTop : radiusBottom;
			const sign = (top === true) ? 1 : -1;

			for (let x = 1; x <= radialSegments; x++) {
				cylinder.position.push(0, halfHeight * sign, 0);
				cylinder.normal.push(0, sign, 0);
				cylinder.uv.push(0.5, 0.5);
				indexTemp++;
			}
			const centerIndexEnd = indexTemp;

			for (let x = 0; x <= radialSegments; x++) {
				const u = x / radialSegments;
				const theta = u * thetaLength + thetaStart;
				const cosTheta = MathUtils.cos(theta);
				const sinTheta = MathUtils.sin(theta);

				cylinder.position.push(radius * sinTheta, halfHeight * sign, radius * cosTheta);
				cylinder.normal.push(0, sign, 0);
				cylinder.uv.push((sinTheta * 0.5) + 0.5, -(cosTheta * 0.5 * sign) + 0.5);
				indexTemp++;
			}

			for (let x = 0; x < radialSegments; x++) {
				const c = centerIndexStart + x;
				const i = centerIndexEnd + x;

				if (top === true) {
					cylinder.index.push(i + poslength, i + 1 + poslength, c + poslength);
				}
				else {
					cylinder.index.push(i + 1 + poslength, i + poslength, c + poslength);
				}
			}
		}

		return cylinder;
	}

	/**
	 * Create torus.
	 * @param {Object} options The options.
	 * @property {Number} options.radius The inner radius of ring.
	 * @property {Number} options.tube The width.
	 * @property {Number} options.radialSegments The number of tangent circle segments.
	 * @property {Number} options.tubularSegments The number of ring segments.
	 * @property {Number} options.arc The display range.
	 * @returns {MeshResult}
	 */
	static createTorus({ radius = 0.8, tube = 0.2, radialSegments = 64, tubularSegments = 64, arc = MathUtils.PI * 2 } = {}) {
		let torus = {};
		torus.position = [];
		torus.normal = [];
		torus.uv = [];
		torus.index = [];

		radialSegments = MathUtils.floor(radialSegments);
		tubularSegments = MathUtils.floor(tubularSegments);

		let centerTemp = [];
		let vertexTemp = [];
		let normalTemp = [];

		for (let j = 0; j <= radialSegments; j++) {
			for (let i = 0; i <= tubularSegments; i++) {
				const u = i / tubularSegments * arc;
				const v = j / radialSegments * MathUtils.PI * 2;

				vertexTemp = [
					(radius + tube * MathUtils.cos(v)) * MathUtils.cos(u),
					(radius + tube * MathUtils.cos(v)) * MathUtils.sin(u),
					tube * MathUtils.sin(v)
				];
				torus.position.push(
					(radius + tube * MathUtils.cos(v)) * MathUtils.cos(u),
					(radius + tube * MathUtils.cos(v)) * MathUtils.sin(u),
					tube * MathUtils.sin(v)
				);
				centerTemp = [radius * MathUtils.cos(u), radius * MathUtils.sin(u), 0];
				normalTemp = MathUtils.normalizeVector(MathUtils.subVector(vertexTemp, centerTemp));
				torus.normal.push(normalTemp[0], normalTemp[1], normalTemp[2]);
				torus.uv.push(i / tubularSegments);
				torus.uv.push(j / radialSegments);
			}
		}

		for (let j = 1; j <= radialSegments; j++) {
			for (let i = 1; i <= tubularSegments; i++) {
				const a = (tubularSegments + 1) * j + i - 1;
				const b = (tubularSegments + 1) * (j - 1) + i - 1;
				const c = (tubularSegments + 1) * (j - 1) + i;
				const d = (tubularSegments + 1) * j + i;

				torus.index.push(a, b, d);
				torus.index.push(b, c, d);
			}
		}

		return torus;
	}

	/**
	 * Create capsule.
	 * @param {Object} options The options.
	 * @property {Number} options.radius The semicircle radius.
	 * @property {Number} options.cylinderHeight The column height.
	 * @property {Number} options.widthSegments The widthSegments.
	 * @property {Number} options.heightSegments The heightSegments.
	 * @returns {MeshResult}
	 */
	static createCapsule({ radius = 0.5, cylinderHeight = 1, widthSegments = 64, heightSegments = 64 } = {}) {
		let capsule = {};
		capsule.position = [];
		capsule.normal = [];
		capsule.uv = [];
		capsule.index = [];

		let upSphere = mathSphere(radius, widthSegments, heightSegments, 0, -MathUtils.PI * 2, -MathUtils.PI / 2, MathUtils.PI, cylinderHeight / 2);
		let downSphere = mathSphere(radius, widthSegments, heightSegments, 0, -MathUtils.PI * 2, MathUtils.PI / 2, MathUtils.PI, - cylinderHeight / 2, upSphere.position.length / 3);
		let cylinder = this.createCylinder({
			radiusTop: radius, radiusBottom: radius, height: cylinderHeight, radialSegments: heightSegments, heightSegments: 1,
			openEnded: true, thetaStart: -MathUtils.PI / 4 * 2, thetaLength: MathUtils.PI * 2, poslength: upSphere.position.length / 3 + downSphere.position.length / 3
		})

		capsule.position = [
			...upSphere.position,
			...downSphere.position,
			...cylinder.position
		];
		capsule.normal = [
			...upSphere.normal,
			...downSphere.normal,
			...cylinder.normal
		];
		capsule.uv = [
			...upSphere.uv,
			...downSphere.uv,
			...cylinder.uv
		];
		capsule.index = [
			...upSphere.index,
			...downSphere.index,
			...cylinder.index
		];

		function mathSphere(radius = 10, widthSegments = 8, heightSegments = 8, phiStart = 0, phiLength = MathUtils.PI * 2,
			thetaStart = MathUtils.PI, thetaLength = MathUtils.PI, heightOffset = 0, poslength = 0) {
			let sphere = {};
			sphere.position = [];
			sphere.normal = [];
			sphere.uv = [];
			sphere.index = [];

			widthSegments = MathUtils.max(3, MathUtils.floor(widthSegments));
			heightSegments = MathUtils.max(2, MathUtils.floor(heightSegments));

			const thetaEnd = MathUtils.min(thetaStart + thetaLength, MathUtils.PI);
			let indexTemp = 0;
			const grid = [];

			for (let iy = 0; iy <= heightSegments; iy++) {
				const verticesRow = [];
				const v = iy / heightSegments;
				let uOffset = 0;
				if (iy == 0 && thetaStart == 0) {
					uOffset = 0.5 / widthSegments;
				}
				else if (iy == heightSegments && thetaEnd == MathUtils.PI) {
					uOffset = -0.5 / widthSegments;
				}
				for (let ix = 0; ix <= widthSegments; ix++) {
					const u = ix / widthSegments;
					sphere.position.push(
						-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
						radius * MathUtils.cos(thetaStart + v * thetaLength) + heightOffset,
						radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
					);
					let normali = [
						-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
						radius * MathUtils.cos(thetaStart + v * thetaLength),
						radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
					]
					normali = MathUtils.normalizeVector(normali);
					sphere.normal.push(normali[0], normali[1], normali[2]);
					if (thetaStart > 0) { sphere.uv.push(-u, -v); }
					else { sphere.uv.push(-u, v); }
					verticesRow.push(indexTemp++);
				}
				grid.push(verticesRow);
			}

			for (let iy = 0; iy < heightSegments / 2; iy++) {
				for (let ix = 0; ix < widthSegments; ix++) {
					const a = grid[iy][ix + 1];
					const b = grid[iy][ix];
					const c = grid[iy + 1][ix];
					const d = grid[iy + 1][ix + 1];
					if (thetaStart > 0) {
						sphere.index.push(a + poslength, d + poslength, b + poslength);
						sphere.index.push(b + poslength, d + poslength, c + poslength);
					} else {
						sphere.index.push(b + poslength, c + poslength, d + poslength);
						sphere.index.push(b + poslength, d + poslength, a + poslength);
					}
				}
			}

			return sphere;
		}

		return capsule;
	}

	static createSphere({ radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = MathUtils.PI * 2,
		thetaStart = 0, thetaLength = MathUtils.PI } = {}) {
		let sphere = {};
		sphere.position = [];
		sphere.normal = [];
		sphere.uv = [];
		sphere.index = [];

		widthSegments = MathUtils.max(3, MathUtils.floor(widthSegments));
		heightSegments = MathUtils.max(2, MathUtils.floor(heightSegments));

		const thetaEnd = MathUtils.min(thetaStart + thetaLength, MathUtils.PI);
		let indexTemp = 0;
		const grid = [];

		for (let iy = 0; iy <= heightSegments; iy++) {
			const verticesRow = [];
			const v = iy / heightSegments;
			let uOffset = 0;
			if (iy == 0 && thetaStart == 0) {
				uOffset = 0.5 / widthSegments;
			}
			else if (iy == heightSegments && thetaEnd == MathUtils.PI) {
				uOffset = -0.5 / widthSegments;
			}
			for (let ix = 0; ix <= widthSegments; ix++) {
				const u = ix / widthSegments;

				let vertex = [
					-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
					radius * MathUtils.cos(thetaStart + v * thetaLength),
					radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
				];

				sphere.position.push(...vertex);

				let normali = MathUtils.normalizeVector(vertex);
				sphere.normal.push(...normali);

				sphere.uv.push(u + uOffset, 1 - v);

				verticesRow.push(indexTemp++);
			}
			grid.push(verticesRow);
		}

		for (let iy = 0; iy < heightSegments; iy++) {
			for (let ix = 0; ix < widthSegments; ix++) {
				const a = grid[iy][ix + 1];
				const b = grid[iy][ix];
				const c = grid[iy + 1][ix];
				const d = grid[iy + 1][ix + 1];

				if (iy !== 0 || thetaStart > 0) sphere.index.push(a, b, d);
				if (iy !== heightSegments - 1 || thetaEnd < Math.PI) sphere.index.push(b, c, d);
			}
		}

		return sphere;
	}

	static createRing({ innerRadius = 0.5, outerRadius = 1, thetaSegments = 32, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2 } = {}) {
		let ring = {};
		ring.position = [];
		ring.normal = [];
		ring.uv = [];
		ring.index = [];

		thetaSegments = MathUtils.max(3, MathUtils.floor(thetaSegments));
		phiSegments = MathUtils.max(2, MathUtils.floor(phiSegments));

		let radius = innerRadius;
		const radiusStep = ((outerRadius - innerRadius) / phiSegments);

		for (let j = 0; j <= phiSegments; j++) {

			for (let i = 0; i <= thetaSegments; i++) {

				// values are generate from the inside of the ring to the outside

				const segment = thetaStart + i / thetaSegments * thetaLength;

				// vertex

				const vertex = [
					radius * MathUtils.cos(segment),
					radius * MathUtils.sin(segment),
					0
				];

				ring.position.push(...vertex);

				// normal

				ring.normal.push(0, 0, 1);

				// uv

				const uv = [
					(vertex[0] / outerRadius + 1) / 2,
					(vertex[1] / outerRadius + 1) / 2
				];

				ring.uv.push(...uv);

			}

			// increase the radius for next row of vertices

			radius += radiusStep;

		}

		// indices

		for (let j = 0; j < phiSegments; j++) {

			const thetaSegmentLevel = j * (thetaSegments + 1);

			for (let i = 0; i < thetaSegments; i++) {

				const segment = i + thetaSegmentLevel;

				const a = segment;
				const b = segment + thetaSegments + 1;
				const c = segment + thetaSegments + 2;
				const d = segment + 1;

				// faces

				ring.index.push(a, b, d);
				ring.index.push(b, c, d);

			}

		}

		return ring;
	}

}

export { MeshBuilder };