src/volume/csg/ConstructiveSolidGeometry.js
import { Box3, Vector3 } from "three";
import { layout } from "sparse-octree";
import { Material } from "../Material";
import { EdgeData } from "../EdgeData";
import { HermiteData } from "../HermiteData";
import { Edge } from "../Edge";
import { OperationType } from "./OperationType";
import { Union } from "./Union";
import { Difference } from "./Difference";
import { Intersection } from "./Intersection";
/**
 * The world size of the current data cell.
 *
 * @type {Number}
 * @private
 */
let cellSize = 0;
/**
 * The lower bounds of the current data cell.
 *
 * @type {Vector3}
 * @private
 */
const cellPosition = new Vector3();
/**
 * Finds out which grid points lie inside the area of the given operation.
 *
 * @private
 * @param {Operation} operation - A CSG operation.
 * @return {Box3} The index bounds.
 */
function computeIndexBounds(operation) {
	const s = cellSize;
	const n = HermiteData.resolution;
	const min = new Vector3(0, 0, 0);
	const max = new Vector3(n, n, n);
	const cellBounds = new Box3(cellPosition, cellPosition.clone().addScalar(cellSize));
	const operationBounds = operation.getBoundingBox();
	if(operation.type !== OperationType.INTERSECTION) {
		if(operationBounds.intersectsBox(cellBounds)) {
			min.copy(operationBounds.min).max(cellBounds.min).sub(cellBounds.min);
			min.x = Math.ceil(min.x * n / s);
			min.y = Math.ceil(min.y * n / s);
			min.z = Math.ceil(min.z * n / s);
			max.copy(operationBounds.max).min(cellBounds.max).sub(cellBounds.min);
			max.x = Math.floor(max.x * n / s);
			max.y = Math.floor(max.y * n / s);
			max.z = Math.floor(max.z * n / s);
		} else {
			// The chunk is unaffected by this operation.
			min.set(n, n, n);
			max.set(0, 0, 0);
		}
	}
	return new Box3(min, max);
}
/**
 * Combines material indices.
 *
 * @private
 * @param {Operation} operation - A CSG operation.
 * @param {HermiteData} data0 - A target data set.
 * @param {HermiteData} data1 - A predominant data set.
 * @param {Box3} bounds - Grid iteration limits.
 */
function combineMaterialIndices(operation, data0, data1, bounds) {
	const n = HermiteData.resolution;
	const m = n + 1;
	const mm = m * m;
	const X = bounds.max.x;
	const Y = bounds.max.y;
	const Z = bounds.max.z;
	let x, y, z;
	for(z = bounds.min.z; z <= Z; ++z) {
		for(y = bounds.min.y; y <= Y; ++y) {
			for(x = bounds.min.x; x <= X; ++x) {
				operation.updateMaterialIndex((z * mm + y * m + x), data0, data1);
			}
		}
	}
}
/**
 * Generates material indices.
 *
 * @private
 * @param {DensityFunction} operation - A CSG operation.
 * @param {HermiteData} data - A target data set.
 * @param {Box3} bounds - Grid iteration limits.
 */
function generateMaterialIndices(operation, data, bounds) {
	const s = cellSize;
	const n = HermiteData.resolution;
	const m = n + 1;
	const mm = m * m;
	const materialIndices = data.materialIndices;
	const base = cellPosition;
	const offset = new Vector3();
	const position = new Vector3();
	const X = bounds.max.x;
	const Y = bounds.max.y;
	const Z = bounds.max.z;
	let materialIndex;
	let materials = 0;
	let x, y, z;
	for(z = bounds.min.z; z <= Z; ++z) {
		offset.z = z * s / n;
		for(y = bounds.min.y; y <= Y; ++y) {
			offset.y = y * s / n;
			for(x = bounds.min.x; x <= X; ++x) {
				offset.x = x * s / n;
				materialIndex = operation.generateMaterialIndex(position.addVectors(base, offset));
				if(materialIndex !== Material.AIR) {
					materialIndices[z * mm + y * m + x] = materialIndex;
					++materials;
				}
			}
		}
	}
	data.materials = materials;
}
/**
 * Combines edges.
 *
 * @private
 * @param {Operation} operation - A CSG operation.
 * @param {HermiteData} data0 - A target data set.
 * @param {HermiteData} data1 - A predominant data set.
 * @return {Object} The generated edge data.
 */
function combineEdges(operation, data0, data1) {
	const n = HermiteData.resolution;
	const m = n + 1;
	const mm = m * m;
	const indexOffsets = new Uint32Array([1, m, mm]);
	const materialIndices = data0.materialIndices;
	const edge1 = new Edge();
	const edge0 = new Edge();
	const edgeData1 = data1.edgeData;
	const edgeData0 = data0.edgeData;
	const lengths = new Uint32Array(3);
	const edgeCount = EdgeData.calculate1DEdgeCount(n);
	const edgeData = new EdgeData(
		n,
		Math.min(edgeCount, edgeData0.indices[0].length + edgeData1.indices[0].length),
		Math.min(edgeCount, edgeData0.indices[1].length + edgeData1.indices[1].length),
		Math.min(edgeCount, edgeData0.indices[2].length + edgeData1.indices[2].length)
	);
	let edges1, zeroCrossings1, normals1;
	let edges0, zeroCrossings0, normals0;
	let edges, zeroCrossings, normals;
	let indexOffset;
	let indexA1, indexB1;
	let indexA0, indexB0;
	let m1, m2;
	let edge;
	let c, d, i, j, il, jl;
	// Process the edges along the X-axis, then Y and finally Z.
	for(c = 0, d = 0; d < 3; c = 0, ++d) {
		edges1 = edgeData1.indices[d];
		edges0 = edgeData0.indices[d];
		edges = edgeData.indices[d];
		zeroCrossings1 = edgeData1.zeroCrossings[d];
		zeroCrossings0 = edgeData0.zeroCrossings[d];
		zeroCrossings = edgeData.zeroCrossings[d];
		normals1 = edgeData1.normals[d];
		normals0 = edgeData0.normals[d];
		normals = edgeData.normals[d];
		indexOffset = indexOffsets[d];
		il = edges1.length;
		jl = edges0.length;
		// Process all generated edges.
		for(i = 0, j = 0; i < il; ++i) {
			indexA1 = edges1[i];
			indexB1 = indexA1 + indexOffset;
			m1 = materialIndices[indexA1];
			m2 = materialIndices[indexB1];
			if(m1 !== m2 && (m1 === Material.AIR || m2 === Material.AIR)) {
				edge1.t = zeroCrossings1[i];
				edge1.n.x = normals1[i * 3];
				edge1.n.y = normals1[i * 3 + 1];
				edge1.n.z = normals1[i * 3 + 2];
				if(operation.type === OperationType.DIFFERENCE) {
					edge1.n.negate();
				}
				edge = edge1;
				// Process existing edges up to the generated edge.
				while(j < jl && edges0[j] <= indexA1) {
					indexA0 = edges0[j];
					indexB0 = indexA0 + indexOffset;
					edge0.t = zeroCrossings0[j];
					edge0.n.x = normals0[j * 3];
					edge0.n.y = normals0[j * 3 + 1];
					edge0.n.z = normals0[j * 3 + 2];
					m1 = materialIndices[indexA0];
					if(indexA0 < indexA1) {
						m2 = materialIndices[indexB0];
						if(m1 !== m2 && (m1 === Material.AIR || m2 === Material.AIR)) {
							// The edge exhibits a material change and there is no conflict.
							edges[c] = indexA0;
							zeroCrossings[c] = edge0.t;
							normals[c * 3] = edge0.n.x;
							normals[c * 3 + 1] = edge0.n.y;
							normals[c * 3 + 2] = edge0.n.z;
							++c;
						}
					} else {
						// Resolve the conflict.
						edge = operation.selectEdge(edge0, edge1, (m1 === Material.SOLID));
					}
					++j;
				}
				edges[c] = indexA1;
				zeroCrossings[c] = edge.t;
				normals[c * 3] = edge.n.x;
				normals[c * 3 + 1] = edge.n.y;
				normals[c * 3 + 2] = edge.n.z;
				++c;
			}
		}
		// Collect remaining edges.
		while(j < jl) {
			indexA0 = edges0[j];
			indexB0 = indexA0 + indexOffset;
			m1 = materialIndices[indexA0];
			m2 = materialIndices[indexB0];
			if(m1 !== m2 && (m1 === Material.AIR || m2 === Material.AIR)) {
				edges[c] = indexA0;
				zeroCrossings[c] = zeroCrossings0[j];
				normals[c * 3] = normals0[j * 3];
				normals[c * 3 + 1] = normals0[j * 3 + 1];
				normals[c * 3 + 2] = normals0[j * 3 + 2];
				++c;
			}
			++j;
		}
		lengths[d] = c;
	}
	return { edgeData, lengths };
}
/**
 * Generates edge data.
 *
 * @private
 * @param {DensityFunction} operation - A CSG operation.
 * @param {HermiteData} data - A target data set.
 * @param {Box3} bounds - Grid iteration limits.
 * @return {Object} The generated edge data.
 */
function generateEdges(operation, data, bounds) {
	const s = cellSize;
	const n = HermiteData.resolution;
	const m = n + 1;
	const mm = m * m;
	const indexOffsets = new Uint32Array([1, m, mm]);
	const materialIndices = data.materialIndices;
	const base = cellPosition;
	const offsetA = new Vector3();
	const offsetB = new Vector3();
	const edge = new Edge();
	const lengths = new Uint32Array(3);
	const edgeData = new EdgeData(n, EdgeData.calculate1DEdgeCount(n));
	let edges, zeroCrossings, normals, indexOffset;
	let indexA, indexB;
	let minX, minY, minZ;
	let maxX, maxY, maxZ;
	let c, d, a, axis;
	let x, y, z;
	// Process the edges along the X-axis, then Y and finally Z.
	for(a = 4, c = 0, d = 0; d < 3; a >>= 1, c = 0, ++d) {
		// X: [1, 0, 0] Y: [0, 1, 0] Z: [0, 0, 1].
		axis = layout[a];
		edges = edgeData.indices[d];
		zeroCrossings = edgeData.zeroCrossings[d];
		normals = edgeData.normals[d];
		indexOffset = indexOffsets[d];
		minX = bounds.min.x; maxX = bounds.max.x;
		minY = bounds.min.y; maxY = bounds.max.y;
		minZ = bounds.min.z; maxZ = bounds.max.z;
		/* Include edges that straddle the bounding box and avoid processing grid
		points at chunk borders. */
		switch(d) {
			case 0:
				minX = Math.max(minX - 1, 0);
				maxX = Math.min(maxX, n - 1);
				break;
			case 1:
				minY = Math.max(minY - 1, 0);
				maxY = Math.min(maxY, n - 1);
				break;
			case 2:
				minZ = Math.max(minZ - 1, 0);
				maxZ = Math.min(maxZ, n - 1);
				break;
		}
		for(z = minZ; z <= maxZ; ++z) {
			for(y = minY; y <= maxY; ++y) {
				for(x = minX; x <= maxX; ++x) {
					indexA = z * mm + y * m + x;
					indexB = indexA + indexOffset;
					// Check if the edge exhibits a material change.
					if(materialIndices[indexA] !== materialIndices[indexB]) {
						offsetA.set(
							x * s / n,
							y * s / n,
							z * s / n
						);
						offsetB.set(
							(x + axis[0]) * s / n,
							(y + axis[1]) * s / n,
							(z + axis[2]) * s / n
						);
						edge.a.addVectors(base, offsetA);
						edge.b.addVectors(base, offsetB);
						// Create and store the edge data.
						operation.generateEdge(edge);
						edges[c] = indexA;
						zeroCrossings[c] = edge.t;
						normals[c * 3] = edge.n.x;
						normals[c * 3 + 1] = edge.n.y;
						normals[c * 3 + 2] = edge.n.z;
						++c;
					}
				}
			}
		}
		lengths[d] = c;
	}
	return { edgeData, lengths };
}
/**
 * Either generates or combines volume data based on the operation type.
 *
 * @private
 * @param {Operation} operation - A CSG operation.
 * @param {HermiteData} data0 - A target data set. May be empty or full.
 * @param {HermiteData} [data1] - A predominant data set. Cannot be null.
 */
function update(operation, data0, data1) {
	const bounds = computeIndexBounds(operation);
	let result, edgeData, lengths, d;
	let done = false;
	// Grid points.
	if(operation.type === OperationType.DENSITY_FUNCTION) {
		generateMaterialIndices(operation, data0, bounds);
	} else if(data0.empty) {
		if(operation.type === OperationType.UNION) {
			data0.set(data1);
			done = true;
		}
	} else {
		if(!(data0.full && operation.type === OperationType.UNION)) {
			combineMaterialIndices(operation, data0, data1, bounds);
		}
	}
	// Edges.
	if(!done && !data0.empty && !data0.full) {
		result = (operation.type === OperationType.DENSITY_FUNCTION) ?
			generateEdges(operation, data0, bounds) :
			combineEdges(operation, data0, data1);
		edgeData = result.edgeData;
		lengths = result.lengths;
		// Cut off empty data.
		for(d = 0; d < 3; ++d) {
			edgeData.indices[d] = edgeData.indices[d].slice(0, lengths[d]);
			edgeData.zeroCrossings[d] = edgeData.zeroCrossings[d].slice(0, lengths[d]);
			edgeData.normals[d] = edgeData.normals[d].slice(0, lengths[d] * 3);
		}
		data0.edgeData = edgeData;
	}
}
/**
 * Executes the given operation to generate data.
 *
 * @private
 * @param {Operation} operation - An operation.
 * @return {HermiteData} The generated data or null if the data is empty.
 */
function execute(operation) {
	const children = operation.children;
	let result, data;
	let i, l;
	if(operation.type === OperationType.DENSITY_FUNCTION) {
		// Create a data target.
		result = new HermiteData();
		// Use the density function to generate data.
		update(operation, result);
	}
	// Union, Difference or Intersection.
	for(i = 0, l = children.length; i < l; ++i) {
		// Generate the full result of the child operation recursively.
		data = execute(children[i]);
		if(result === undefined) {
			result = data;
		} else if(data !== null) {
			if(result === null) {
				if(operation.type === OperationType.UNION) {
					// Build upon the first non-empty data.
					result = data;
				}
			} else {
				// Combine the two data sets.
				update(operation, result, data);
			}
		} else if(operation.type === OperationType.INTERSECTION) {
			// An intersection with nothing results in nothing.
			result = null;
		}
		if(result === null && operation.type !== OperationType.UNION) {
			// Further subtractions and intersections would have no effect.
			break;
		}
	}
	return (result !== null && result.empty) ? null : result;
}
/**
 * Constructive Solid Geometry combines Signed Distance Functions by using
 * Boolean operators to generate and transform volume data.
 */
export class ConstructiveSolidGeometry {
	/**
	 * Transforms the given Hermite data in two steps:
	 *
	 *  1. Generate data by executing the given SDF
	 *  2. Combine the generated data with the given data
	 *
	 * @param {Number[]} min - The lower bounds of the volume data cell.
	 * @param {Number} size - The size of the volume data cell.
	 * @param {HermiteData} data - The volume data that should be modified.
	 * @param {SignedDistanceFunction} sdf - An SDF.
	 * @return {HermiteData} The modified, uncompressed data or null if the result is empty.
	 */
	static run(min, size, data, sdf) {
		cellPosition.fromArray(min);
		cellSize = size;
		if(data === null) {
			if(sdf.operation === OperationType.UNION) {
				// Prepare an empty target.
				data = new HermiteData(false);
			}
		} else {
			data.decompress();
		}
		// Step 1.
		let operation = sdf.toCSG();
		const generatedData = (data !== null) ? execute(operation) : null;
		if(generatedData !== null) {
			// Wrap the operation in a super operation.
			switch(sdf.operation) {
				case OperationType.UNION:
					operation = new Union(operation);
					break;
				case OperationType.DIFFERENCE:
					operation = new Difference(operation);
					break;
				case OperationType.INTERSECTION:
					operation = new Intersection(operation);
					break;
			}
			// Step 2.
			update(operation, data, generatedData);
			// Provoke an isosurface extraction.
			data.contoured = false;
		}
		return (data !== null && data.empty) ? null : data;
	}
}