src/octree/voxel/SparseVoxelOctree.js
import { Vector3 } from "three";
import { Octree, layout, edges } from "sparse-octree";
import { QEFData, QEFSolver } from "../../math";
import { HermiteData, Material, Voxel } from "../../volume";
import { VoxelCell } from "./VoxelCell";
/**
 * Creates intermediate voxel cells down to the leaf octant that is described
 * by the given local grid coordinates and returns it.
 *
 * @private
 * @param {VoxelCell} cell - The root octant.
 * @param {Number} n - The grid resolution.
 * @param {Number} x - A local grid point X-coordinate.
 * @param {Number} y - A local grid point Y-coordinate.
 * @param {Number} z - A local grid point Z-coordinate.
 * @return {VoxelCell} A leaf voxel cell.
 */
function getCell(cell, n, x, y, z) {
	let i = 0;
	for(n = n >> 1; n > 0; n >>= 1, i = 0) {
		// YZ.
		if(x >= n) {
			i += 4; x -= n;
		}
		// XZ.
		if(y >= n) {
			i += 2; y -= n;
		}
		// XY.
		if(z >= n) {
			i += 1; z -= n;
		}
		if(cell.children === null) {
			cell.split();
		}
		cell = cell.children[i];
	}
	return cell;
}
/**
 * Creates a voxel and builds a material configuration code from the materials
 * in the voxel corners.
 *
 * @private
 * @param {Number} n - The grid resolution.
 * @param {Number} x - A local grid point X-coordinate.
 * @param {Number} y - A local grid point Y-coordinate.
 * @param {Number} z - A local grid point Z-coordinate.
 * @param {Uint8Array} materialIndices - The material indices.
 * @return {Voxel} A voxel.
 */
function createVoxel(n, x, y, z, materialIndices) {
	const m = n + 1;
	const mm = m * m;
	const voxel = new Voxel();
	let materials, edgeCount;
	let material, offset, index;
	let c1, c2, m1, m2;
	let i;
	// Pack the material information of the eight corners into a single byte.
	for(materials = 0, i = 0; i < 8; ++i) {
		// Translate the coordinates into a one-dimensional grid point index.
		offset = layout[i];
		index = (z + offset[2]) * mm + (y + offset[1]) * m + (x + offset[0]);
		// Convert the identified material index into a binary value.
		material = Math.min(materialIndices[index], Material.SOLID);
		// Store the value in bit i.
		materials |= (material << i);
	}
	// Find out how many edges intersect with the implicit surface.
	for(edgeCount = 0, i = 0; i < 12; ++i) {
		c1 = edges[i][0];
		c2 = edges[i][1];
		m1 = (materials >> c1) & 1;
		m2 = (materials >> c2) & 1;
		// Check if there is a material change on the edge.
		if(m1 !== m2) {
			++edgeCount;
		}
	}
	voxel.materials = materials;
	voxel.edgeCount = edgeCount;
	voxel.qefData = new QEFData();
	return voxel;
}
/**
 * A sparse, cubic voxel octree.
 */
export class SparseVoxelOctree extends Octree {
	/**
	 * Constructs a new voxel octree.
	 *
	 * @param {HermiteData} data - A set of volume data.
	 * @param {Vector3} [min] - The lower bounds of this octree.
	 * @param {Number} [size=1] - The size of this octree.
	 */
	constructor(data, min = new Vector3(), size = 1) {
		super();
		/**
		 * The root octant.
		 *
		 * @type {VoxelCell}
		 */
		this.root = new VoxelCell(min, size);
		/**
		 * The amount of voxels in this octree.
		 *
		 * @type {Number}
		 */
		this.voxelCount = 0;
		if(data !== null && data.edgeData !== null) {
			this.construct(data);
		}
		if(VoxelCell.errorThreshold >= 0) {
			this.simplify();
		}
	}
	/**
	 * Attempts to simplify the octree by clustering voxels.
	 *
	 * @private
	 */
	simplify() {
		this.voxelCount -= this.root.collapse();
	}
	/**
	 * Constructs voxel cells from volume data.
	 *
	 * @private
	 * @param {HermiteData} data - The volume data.
	 */
	construct(data) {
		const n = HermiteData.resolution;
		const edgeData = data.edgeData;
		const materialIndices = data.materialIndices;
		const qefSolver = new QEFSolver();
		const intersection = new Vector3();
		const edgeIterators = [
			edgeData.edgesX(this.min, this.root.size),
			edgeData.edgesY(this.min, this.root.size),
			edgeData.edgesZ(this.min, this.root.size)
		];
		const sequences = [
			new Uint8Array([0, 1, 2, 3]),
			new Uint8Array([0, 1, 4, 5]),
			new Uint8Array([0, 2, 4, 6])
		];
		let voxelCount = 0;
		let edges, edge;
		let sequence, offset;
		let cell, voxel;
		let x, y, z;
		let d, i;
		// Process edges X -> Y -> Z.
		for(d = 0; d < 3; ++d) {
			sequence = sequences[d];
			edges = edgeIterators[d];
			for(edge of edges) {
				edge.computeZeroCrossingPosition(intersection);
				// Each edge can belong to up to four voxel cells.
				for(i = 0; i < 4; ++i) {
					// Rotate around the edge.
					offset = layout[sequence[i]];
					x = edge.coordinates.x - offset[0];
					y = edge.coordinates.y - offset[1];
					z = edge.coordinates.z - offset[2];
					// Check if the adjusted coordinates still lie inside the grid bounds.
					if(x >= 0 && y >= 0 && z >= 0 && x < n && y < n && z < n) {
						cell = getCell(this.root, n, x, y, z);
						if(cell.voxel === null) {
							// The existence of the edge guarantees that the voxel contains the surface.
							cell.voxel = createVoxel(n, x, y, z, materialIndices);
							++voxelCount;
						}
						// Add the edge data to the voxel.
						voxel = cell.voxel;
						voxel.normal.add(edge.n);
						voxel.qefData.add(intersection, edge.n);
						if(voxel.qefData.numPoints === voxel.edgeCount) {
							// Finalise the voxel by solving the accumulated data.
							qefSolver.setData(voxel.qefData).solve(voxel.position);
							if(!cell.contains(voxel.position)) {
								voxel.position.copy(qefSolver.massPoint);
							}
							voxel.normal.normalize();
						}
					}
				}
			}
		}
		this.voxelCount = voxelCount;
	}
}