Home Reference Source

src/volume/HermiteDataHelper.js

import {
	BufferAttribute,
	BufferGeometry,
	Group,
	LineBasicMaterial,
	LineSegments,
	Points,
	PointsMaterial,
	Vector3,
	VertexColors
} from "three";

/**
 * The material value for air.
 *
 * @type {Material}
 * @private
 */

let air = 0;

/**
 * A Hermite data helper.
 */

export class HermiteDataHelper extends Group {

	/**
	 * Constructs a new Hermite data helper.
	 *
	 * @param {Vector3} cellPosition - The position of the volume data cell.
	 * @param {Number} cellSize - The size of the volume data cell.
	 * @param {HermiteData} data - The volume data. Must be uncompressed.
	 * @param {Boolean} [useMaterialIndices=false] - Whether points should be created for solid material indices.
	 * @param {Boolean} [useEdgeData=true] - Whether edges with intersection points and normals should be created.
	 */

	constructor(cellPosition = null, cellSize = 1, data = null, useMaterialIndices = false, useEdgeData = true) {

		super();

		/**
		 * The name of this object.
		 */

		this.name = "HermiteDataHelper";

		/**
		 * The position of the volume data cell.
		 *
		 * @type {Vector3}
		 */

		this.cellPosition = cellPosition;

		/**
		 * The size of the volume data cell.
		 *
		 * @type {Number}
		 */

		this.cellSize = cellSize;

		/**
		 * The volume data.
		 *
		 * @type {HermiteData}
		 */

		this.data = data;

		/**
		 * The material of the grid points.
		 *
		 * @type {PointsMaterial}
		 */

		this.pointsMaterial = new PointsMaterial({
			vertexColors: VertexColors,
			sizeAttenuation: true,
			size: 0.05
		});

		// Create groups for grid points, edges and normals.
		this.add(new Group());
		this.add(new Group());
		this.add(new Group());

		this.gridPoints.name = "GridPoints";
		this.edges.name = "Edges";
		this.normals.name = "Normals";

		try {

			// Attempt to update right away.
			this.update(useMaterialIndices, useEdgeData);

		} catch(e) {

			// Don't complain on error.

		}

	}

	/**
	 * The grid points.
	 *
	 * @type {Group}
	 */

	get gridPoints() {

		return this.children[0];

	}

	/**
	 * The edges.
	 *
	 * @type {Group}
	 */

	get edges() {

		return this.children[1];

	}

	/**
	 * The normals.
	 *
	 * @type {Group}
	 */

	get normals() {

		return this.children[2];

	}

	/**
	 * Checks if the current position, size and data is valid.
	 *
	 * @private
	 * @return {Error} An error, or null if the data is valid.
	 */

	validate() {

		let error = null;

		if(this.cellPosition === null) {

			error = new Error("The cell position is not defined");

		} else if(this.cellSize <= 0) {

			error = new Error("Invalid cell size: " + this.cellSize);

		} else if(this.data === null) {

			error = new Error("No data");

		} else {

			if(this.data.empty) {

				error = new Error("The provided data is empty");

			}

			if(this.data.compressed) {

				error = new Error("The provided data must be uncompressed");

			}

		}

		return error;

	}

	/**
	 * Sets the cell position, size and data.
	 *
	 * @param {Vector3} cellPosition - The position of the volume data cell.
	 * @param {Number} cellSize - The size of the volume data cell.
	 * @param {HermiteData} data - The volume data. Must be uncompressed.
	 * @return {HermiteDataHelper} This helper.
	 */

	set(cellPosition, cellSize, data) {

		this.cellPosition = cellPosition;
		this.cellSize = cellSize;
		this.data = data;

		return this;

	}

	/**
	 * Creates the helper geometry.
	 *
	 * @throws {Error} Throws an error if the current cell position, cell size or data is invalid.
	 * @param {Boolean} [useMaterialIndices=false] - Whether points should be created for solid material indices.
	 * @param {Boolean} [useEdgeData=true] - Whether edges with intersection points and normals should be created.
	 * @return {HermiteDataHelper} This helper.
	 */

	update(useMaterialIndices = false, useEdgeData = true) {

		const data = this.data;
		const error = this.validate();

		// Remove existing geometry.
		this.dispose();

		if(error !== null) {

			throw error;

		} else {

			if(useMaterialIndices) {

				this.createPoints(data);

			}

			if(useEdgeData && data.edgeData !== null) {

				this.createEdges(data);

			}

		}

		return this;

	}

	/**
	 * Creates points for solid material indices.
	 *
	 * @private
	 * @param {HermiteData} data - Volume data.
	 */

	createPoints(data) {

		const materialIndices = data.materialIndices;
		const n = Math.cbrt(materialIndices.length) - 1;
		const s = this.cellSize;

		const base = this.cellPosition;
		const offset = new Vector3();
		const position = new Vector3();

		const color = new Float32Array([0.0, 0.0, 0.0]);

		const geometry = new BufferGeometry();
		const vertexCount = data.materials;
		const positions = new Float32Array(vertexCount * 3);
		const colors = new Float32Array(vertexCount * 3);

		let x, y, z;
		let i, j;

		for(i = 0, j = 0, z = 0; z <= n; ++z) {

			offset.z = z * s / n;

			for(y = 0; y <= n; ++y) {

				offset.y = y * s / n;

				for(x = 0; x <= n; ++x) {

					offset.x = x * s / n;

					if(materialIndices[i++] !== air) {

						position.addVectors(base, offset);

						positions[j] = position.x; colors[j++] = color[0];
						positions[j] = position.y; colors[j++] = color[1];
						positions[j] = position.z; colors[j++] = color[2];

					}

				}

			}

		}

		geometry.setAttribute("position", new BufferAttribute(positions, 3));
		geometry.setAttribute("color", new BufferAttribute(colors, 3));

		this.gridPoints.add(new Points(geometry, this.pointsMaterial));

	}

	/**
	 * Creates edges with intersection points and normals.
	 *
	 * @private
	 * @param {HermiteData} data - Volume data.
	 */

	createEdges(data) {

		const edgeData = data.edgeData;
		const n = Math.cbrt(data.materialIndices.length) - 1;
		const s = this.cellSize;

		const normalA = new Vector3();
		const normalB = new Vector3();

		const edgeIterators = [
			edgeData.edgesX(this.cellPosition, this.cellSize),
			edgeData.edgesY(this.cellPosition, this.cellSize),
			edgeData.edgesZ(this.cellPosition, this.cellSize)
		];

		const axisColors = [
			new Float32Array([0.6, 0.0, 0.0]),
			new Float32Array([0.0, 0.6, 0.0]),
			new Float32Array([0.0, 0.0, 0.6])
		];

		const normalColor = new Float32Array([0.0, 1.0, 1.0]);

		const lineSegmentsMaterial = new LineBasicMaterial({
			vertexColors: VertexColors
		});

		let edgePositions, edgeColors;
		let normalPositions, normalColors;
		let vertexCount, edgeColor, geometry, edges, edge;

		let d, i, j;

		for(i = 0, j = 0, d = 0; d < 3; ++d, i = 0, j = 0) {

			edgeColor = axisColors[d];
			edges = edgeIterators[d];

			// Are there any edges for this dimension?
			if(edges.lengths.length > 0) {

				// There can only be one lengths entry per iterator.
				vertexCount = edges.lengths[0] * 2;

				edgePositions = new Float32Array(vertexCount * 3);
				edgeColors = new Float32Array(vertexCount * 3);
				normalPositions = new Float32Array(vertexCount * 3);
				normalColors = new Float32Array(vertexCount * 3);

				for(edge of edges) {

					// Edge.
					edgePositions[i] = edge.a.x; edgeColors[i++] = edgeColor[0];
					edgePositions[i] = edge.a.y; edgeColors[i++] = edgeColor[1];
					edgePositions[i] = edge.a.z; edgeColors[i++] = edgeColor[2];

					edgePositions[i] = edge.b.x; edgeColors[i++] = edgeColor[0];
					edgePositions[i] = edge.b.y; edgeColors[i++] = edgeColor[1];
					edgePositions[i] = edge.b.z; edgeColors[i++] = edgeColor[2];

					// Normal at Zero Crossing.
					edge.computeZeroCrossingPosition(normalA);
					normalB.copy(normalA).addScaledVector(edge.n, 0.25 * s / n);

					normalPositions[j] = normalA.x; normalColors[j++] = normalColor[0];
					normalPositions[j] = normalA.y; normalColors[j++] = normalColor[1];
					normalPositions[j] = normalA.z; normalColors[j++] = normalColor[2];

					normalPositions[j] = normalB.x; normalColors[j++] = normalColor[0];
					normalPositions[j] = normalB.y; normalColors[j++] = normalColor[1];
					normalPositions[j] = normalB.z; normalColors[j++] = normalColor[2];

				}

				geometry = new BufferGeometry();
				geometry.setAttribute("position", new BufferAttribute(edgePositions, 3));
				geometry.setAttribute("color", new BufferAttribute(edgeColors, 3));

				this.edges.add(new LineSegments(geometry, lineSegmentsMaterial));

				geometry = new BufferGeometry();
				geometry.setAttribute("position", new BufferAttribute(normalPositions, 3));
				geometry.setAttribute("color", new BufferAttribute(normalColors, 3));

				this.normals.add(new LineSegments(geometry, lineSegmentsMaterial));

			}

		}

	}

	/**
	 * Destroys the current helper geometry.
	 */

	dispose() {

		let child, children;
		let i, l;

		for(i = 0, l = this.children.length; i < l; ++i) {

			child = this.children[i];
			children = child.children;

			while(children.length > 0) {

				children[0].geometry.dispose();
				children[0].material.dispose();
				child.remove(children[0]);

			}

		}

	}

	/**
	 * Sets the material value for grid points that represent air.
	 *
	 * @type {Material}
	 */

	static set air(value) {

		air = value;

	}

}