@jgphilpott/polytree
Version:
A modern, high-performance spatial querying library designed specifically for three.js and Node.
1,083 lines (950 loc) • 222 kB
JavaScript
// Generated by CoffeeScript 2.7.0
var Box3, BufferAttribute, BufferGeometry, DoubleSide, Line3, Matrix3, Mesh, Ray, Raycaster, Sphere, ThreePlane, Triangle, Vector2, Vector3;
({Vector2, Vector3, Box3, Line3, Sphere, Matrix3, Triangle, Ray, Raycaster, Mesh, DoubleSide, BufferGeometry, BufferAttribute} = require("three"));
({
Plane: ThreePlane
} = require("three"));
// Generated by CoffeeScript 2.7.0
// =============================================================================
// MATHEMATICAL CONSTANTS
// Constants used for floating-point precision, geometry, and mathematical operations.
// =============================================================================
// Floating-point tolerance for general geometric operations.
var ASYNC_OPERATION_TIMEOUT, DEBUG_COLOR_BACK, DEBUG_COLOR_COPLANAR, DEBUG_COLOR_FRONT, DEBUG_COLOR_SPANNING, DEBUG_GEOMETRY_VALIDATION, DEBUG_INTERSECTION_VERIFICATION, DEBUG_PERFORMANCE_TIMING, DEBUG_VERBOSE_LOGGING, DEFAULT_BUFFER_SIZE, DEFAULT_COORDINATE_PRECISION, DEFAULT_MATERIAL_INDEX, GARBAGE_COLLECTION_THRESHOLD, GEOMETRIC_EPSILON, GEOMETRY_CACHE_SIZE, INTERSECTION_CACHE_SIZE, MAX_REFINEMENT_ITERATIONS, MAX_WORKER_THREADS, MEMORY_USAGE_WARNING_LIMIT, POINT_COINCIDENCE_THRESHOLD, POLYGON_BACK, POLYGON_COPLANAR, POLYGON_FRONT, POLYGON_SPANNING, POLYTREE_MAX_DEPTH, POLYTREE_MAX_POLYGONS_PER_NODE, POLYTREE_MIN_NODE_SIZE, RAY_INTERSECTION_EPSILON, TRIANGLE_2D_EPSILON, WINDING_NUMBER_FULL_ROTATION, defaultRayDirection, meshOperationNormalVector, meshOperationVertexVector, polygonID, rayTriangleEdge1, rayTriangleEdge2, rayTriangleHVector, rayTriangleQVector, rayTriangleSVector, temporaryBoundingBox, temporaryMatrix3, temporaryMatrixWithNormalCalc, temporaryRay, temporaryRaycaster, temporaryTriangleVertex, temporaryTriangleVertexSecondary, temporaryVector3Primary, temporaryVector3Quaternary, temporaryVector3Secondary, temporaryVector3Tertiary, windingNumberEpsilonOffsets, windingNumberEpsilonOffsetsCount, windingNumberMatrix3, windingNumberTestPoint, windingNumberVector1, windingNumberVector2, windingNumberVector3;
GEOMETRIC_EPSILON = 1e-8;
// Ultra-high precision tolerance for ray-triangle intersection calculations.
RAY_INTERSECTION_EPSILON = 1e-12;
// 2D geometry tolerance for triangle overlap calculations.
TRIANGLE_2D_EPSILON = 1e-14;
// Full rotation constant (4π) used in winding number calculations.
WINDING_NUMBER_FULL_ROTATION = 4 * Math.PI;
// =============================================================================
// POLYGON CLASSIFICATION CONSTANTS
// Used for determining spatial relationships between polygons and planes.
// =============================================================================
// Polygon lies exactly on the splitting plane.
POLYGON_COPLANAR = 0;
// Polygon lies entirely in front of the splitting plane.
POLYGON_FRONT = 1;
// Polygon lies entirely behind the splitting plane
POLYGON_BACK = 2;
// Polygon crosses the splitting plane (requires splitting).
POLYGON_SPANNING = 3;
// =============================================================================
// GENERAL PURPOSE TEMPORARY OBJECTS
// Reusable objects to avoid memory allocation during calculations.
// =============================================================================
// Primary temporary vector for general 3D calculations.
temporaryVector3Primary = new Vector3();
// Secondary temporary vector for 3D calculations when two vectors needed.
temporaryVector3Secondary = new Vector3();
// Tertiary temporary vector for calculations requiring three vectors.
temporaryVector3Tertiary = new Vector3();
// Quaternary temporary vector for triangle-specific calculations.
temporaryVector3Quaternary = new Vector3();
// Temporary bounding box for spatial calculations.
temporaryBoundingBox = new Box3();
// Temporary raycaster for intersection calculations.
temporaryRaycaster = new Raycaster();
// Temporary ray object for geometric queries.
temporaryRay = new Ray();
// Default ray direction pointing along positive Z-axis.
defaultRayDirection = new Vector3(0, 0, 1);
// =============================================================================
// WINDING NUMBER ALGORITHM VARIABLES
// Used for point-in-polygon testing using the winding number method.
// =============================================================================
// First vertex vector relative to test point.
windingNumberVector1 = new Vector3();
// Second vertex vector relative to test point.
windingNumberVector2 = new Vector3();
// Third vertex vector relative to test point.
windingNumberVector3 = new Vector3();
// Test point position vector.
windingNumberTestPoint = new Vector3();
// Epsilon offset vectors for handling edge cases in winding number calculation.
windingNumberEpsilonOffsets = [new Vector3(GEOMETRIC_EPSILON, 0, 0), new Vector3(0, GEOMETRIC_EPSILON, 0), new Vector3(0, 0, GEOMETRIC_EPSILON), new Vector3(-GEOMETRIC_EPSILON, 0, 0), new Vector3(0, -GEOMETRIC_EPSILON, 0), new Vector3(0, 0, -GEOMETRIC_EPSILON)];
// Count of epsilon offset vectors for iteration.
windingNumberEpsilonOffsetsCount = windingNumberEpsilonOffsets.length;
// 3x3 matrix for winding number determinant calculations.
windingNumberMatrix3 = new Matrix3();
// =============================================================================
// RAY-TRIANGLE INTERSECTION VARIABLES
// Used in Möller–Trumbore ray-triangle intersection algorithm.
// =============================================================================
// Triangle edge vector from vertex A to vertex B.
rayTriangleEdge1 = new Vector3();
// Triangle edge vector from vertex A to vertex C.
rayTriangleEdge2 = new Vector3();
// Cross product of ray direction and second edge.
rayTriangleHVector = new Vector3();
// Vector from ray origin to triangle vertex A.
rayTriangleSVector = new Vector3();
// Cross product for final intersection calculation.
rayTriangleQVector = new Vector3();
// =============================================================================
// MATRIX AND TRANSFORMATION VARIABLES
// Used for coordinate transformations and matrix operations.
// =============================================================================
// Temporary vertex position for geometric calculations.
temporaryTriangleVertex = new Vector3();
// Second temporary vertex for calculations requiring multiple vertices.
temporaryTriangleVertexSecondary = new Vector3();
// Temporary 3x3 matrix for normal transformations and general calculations.
temporaryMatrix3 = new Matrix3();
// Extended temporary matrix with normal matrix calculation method.
temporaryMatrixWithNormalCalc = new Matrix3();
// Method to calculate normal matrix from 4x4 transformation matrix.
temporaryMatrixWithNormalCalc.getNormalMatrix = function(matrix) {
return this.setFromMatrix4(matrix).invert().transpose();
};
// Temporary vector for mesh normal calculations and geometry operations.
meshOperationNormalVector = new Vector3();
// Temporary vertex vector for mesh transformation and vertex operations.
meshOperationVertexVector = new Vector3();
// =============================================================================
// CSG AND POLYTREE OPERATION VARIABLES
// Variables for Constructive Solid Geometry and spatial partitioning.
// =============================================================================
// Maximum subdivision depth for polytree structures.
POLYTREE_MAX_DEPTH = 1000;
// Maximum polygons per polytree node before subdivision.
POLYTREE_MAX_POLYGONS_PER_NODE = 100000;
// Minimum polytree node size to prevent excessive subdivision.
POLYTREE_MIN_NODE_SIZE = 1e-6;
// Default material index for CSG operations.
DEFAULT_MATERIAL_INDEX = 0;
// Maximum iterations for iterative refinement algorithms.
MAX_REFINEMENT_ITERATIONS = 10000;
// =============================================================================
// PERFORMANCE AND OPTIMIZATION VARIABLES
// Settings for balancing accuracy vs performance.
// =============================================================================
// Default precision for point coordinate rounding.
DEFAULT_COORDINATE_PRECISION = 15;
// Threshold for considering two points as identical.
POINT_COINCIDENCE_THRESHOLD = 1e-15;
// Buffer size for batch processing operations.
DEFAULT_BUFFER_SIZE = 16384;
// Maximum number of worker threads for parallel processing.
MAX_WORKER_THREADS = 2e308;
// Timeout for async operations (in milliseconds).
ASYNC_OPERATION_TIMEOUT = 2e308;
// Memory management thresholds.
GARBAGE_COLLECTION_THRESHOLD = 100000;
MEMORY_USAGE_WARNING_LIMIT = 0.95;
// Cache size limits.
GEOMETRY_CACHE_SIZE = 2e308;
INTERSECTION_CACHE_SIZE = 2e308;
// =============================================================================
// POLYGON ID MANAGEMENT
// Global counter for assigning unique IDs to polygon instances.
// =============================================================================
// Global polygon ID counter for unique polygon identification.
polygonID = 0;
// =============================================================================
// DEBUGGING AND DEVELOPMENT VARIABLES
// Variables useful for debugging and development.
// =============================================================================
// Enable detailed logging for debugging.
DEBUG_VERBOSE_LOGGING = false;
// Enable performance timing measurements.
DEBUG_PERFORMANCE_TIMING = false;
// Enable geometry validation checks.
DEBUG_GEOMETRY_VALIDATION = false;
// Enable intersection result verification.
DEBUG_INTERSECTION_VERIFICATION = false;
// Color codes for debug visualization.
DEBUG_COLOR_FRONT = 0x00ff00;
DEBUG_COLOR_BACK = 0xff0000;
DEBUG_COLOR_COPLANAR = 0x0000ff;
DEBUG_COLOR_SPANNING = 0xffff00;
// =============================================================================
// EXPORTS FOR TESTING
// Export all variables so they can be tested.
// =============================================================================
// Only export when in a testing environment (when module.exports exists).
if (typeof module !== 'undefined' && module.exports) {
// Mathematical Constants
module.exports.GEOMETRIC_EPSILON = GEOMETRIC_EPSILON;
module.exports.RAY_INTERSECTION_EPSILON = RAY_INTERSECTION_EPSILON;
module.exports.TRIANGLE_2D_EPSILON = TRIANGLE_2D_EPSILON;
module.exports.WINDING_NUMBER_FULL_ROTATION = WINDING_NUMBER_FULL_ROTATION;
// Polygon Classification Constants
module.exports.POLYGON_COPLANAR = POLYGON_COPLANAR;
module.exports.POLYGON_FRONT = POLYGON_FRONT;
module.exports.POLYGON_BACK = POLYGON_BACK;
module.exports.POLYGON_SPANNING = POLYGON_SPANNING;
// General Purpose Temporary Objects
module.exports.temporaryVector3Primary = temporaryVector3Primary;
module.exports.temporaryVector3Secondary = temporaryVector3Secondary;
module.exports.temporaryVector3Tertiary = temporaryVector3Tertiary;
module.exports.temporaryVector3Quaternary = temporaryVector3Quaternary;
module.exports.temporaryBoundingBox = temporaryBoundingBox;
module.exports.temporaryRaycaster = temporaryRaycaster;
module.exports.temporaryRay = temporaryRay;
module.exports.defaultRayDirection = defaultRayDirection;
// Winding Number Algorithm Variables
module.exports.windingNumberVector1 = windingNumberVector1;
module.exports.windingNumberVector2 = windingNumberVector2;
module.exports.windingNumberVector3 = windingNumberVector3;
module.exports.windingNumberTestPoint = windingNumberTestPoint;
module.exports.windingNumberEpsilonOffsets = windingNumberEpsilonOffsets;
module.exports.windingNumberEpsilonOffsetsCount = windingNumberEpsilonOffsetsCount;
module.exports.windingNumberMatrix3 = windingNumberMatrix3;
// Ray-Triangle Intersection Variables
module.exports.rayTriangleEdge1 = rayTriangleEdge1;
module.exports.rayTriangleEdge2 = rayTriangleEdge2;
module.exports.rayTriangleHVector = rayTriangleHVector;
module.exports.rayTriangleSVector = rayTriangleSVector;
module.exports.rayTriangleQVector = rayTriangleQVector;
// Matrix and Transformation Variables
module.exports.temporaryTriangleVertex = temporaryTriangleVertex;
module.exports.temporaryTriangleVertexSecondary = temporaryTriangleVertexSecondary;
module.exports.temporaryMatrix3 = temporaryMatrix3;
module.exports.temporaryMatrixWithNormalCalc = temporaryMatrixWithNormalCalc;
module.exports.meshOperationNormalVector = meshOperationNormalVector;
module.exports.meshOperationVertexVector = meshOperationVertexVector;
// CSG and Polytree Operation Variables
module.exports.POLYTREE_MAX_DEPTH = POLYTREE_MAX_DEPTH;
module.exports.POLYTREE_MAX_POLYGONS_PER_NODE = POLYTREE_MAX_POLYGONS_PER_NODE;
module.exports.POLYTREE_MIN_NODE_SIZE = POLYTREE_MIN_NODE_SIZE;
module.exports.DEFAULT_MATERIAL_INDEX = DEFAULT_MATERIAL_INDEX;
module.exports.MAX_REFINEMENT_ITERATIONS = MAX_REFINEMENT_ITERATIONS;
// Performance and Optimization Variables
module.exports.DEFAULT_COORDINATE_PRECISION = DEFAULT_COORDINATE_PRECISION;
module.exports.POINT_COINCIDENCE_THRESHOLD = POINT_COINCIDENCE_THRESHOLD;
module.exports.DEFAULT_BUFFER_SIZE = DEFAULT_BUFFER_SIZE;
module.exports.MAX_WORKER_THREADS = MAX_WORKER_THREADS;
module.exports.ASYNC_OPERATION_TIMEOUT = ASYNC_OPERATION_TIMEOUT;
module.exports.GARBAGE_COLLECTION_THRESHOLD = GARBAGE_COLLECTION_THRESHOLD;
module.exports.MEMORY_USAGE_WARNING_LIMIT = MEMORY_USAGE_WARNING_LIMIT;
module.exports.GEOMETRY_CACHE_SIZE = GEOMETRY_CACHE_SIZE;
module.exports.INTERSECTION_CACHE_SIZE = INTERSECTION_CACHE_SIZE;
// Polygon ID Management
module.exports.polygonID = polygonID;
// Debugging and Development Variables
module.exports.DEBUG_VERBOSE_LOGGING = DEBUG_VERBOSE_LOGGING;
module.exports.DEBUG_PERFORMANCE_TIMING = DEBUG_PERFORMANCE_TIMING;
module.exports.DEBUG_GEOMETRY_VALIDATION = DEBUG_GEOMETRY_VALIDATION;
module.exports.DEBUG_INTERSECTION_VERIFICATION = DEBUG_INTERSECTION_VERIFICATION;
module.exports.DEBUG_COLOR_FRONT = DEBUG_COLOR_FRONT;
module.exports.DEBUG_COLOR_BACK = DEBUG_COLOR_BACK;
module.exports.DEBUG_COLOR_COPLANAR = DEBUG_COLOR_COPLANAR;
module.exports.DEBUG_COLOR_SPANNING = DEBUG_COLOR_SPANNING;
}
// Generated by CoffeeScript 2.7.0
// === BUFFER UTILITIES ===
// Simple LRU cache for intersection results to improve performance.
var calculateWindingNumberFromBuffer, checkMemoryUsage, clearGeometryCache, clearIntersectionCache, createIntersectionCacheKey, createVector2Buffer, createVector3Buffer, disposePolytreeResources, extractCoordinatesFromArray, geometryCache, geometryCacheKeys, handleIntersectingPolytrees, intersectionCache, intersectionCacheKeys, operationCounter, prepareTriangleBufferFromPolygons, roundPointCoordinates, signedVolumeOfTriangle, sortRaycastIntersectionsByDistance, splitPolygonByPlane, splitPolygonVertexArray, testPolygonInsideUsingWindingNumber, testRayTriangleIntersection;
intersectionCache = new Map();
intersectionCacheKeys = [];
// Geometry calculation cache for expensive operations.
geometryCache = new Map();
geometryCacheKeys = [];
// Operation counter for garbage collection threshold tracking.
operationCounter = 0;
// Clear intersection cache when it exceeds size limit.
clearIntersectionCache = function() {
var i, key, keysToRemove, len, results;
if (INTERSECTION_CACHE_SIZE !== 2e308 && intersectionCache.size > INTERSECTION_CACHE_SIZE) {
// Remove oldest entries (simple FIFO approach).
keysToRemove = intersectionCacheKeys.splice(0, Math.floor(INTERSECTION_CACHE_SIZE / 2));
results = [];
for (i = 0, len = keysToRemove.length; i < len; i++) {
key = keysToRemove[i];
results.push(intersectionCache.delete(key));
}
return results;
}
};
// Clear geometry cache when it exceeds size limit.
clearGeometryCache = function() {
var i, key, keysToRemove, len, results;
if (GEOMETRY_CACHE_SIZE !== 2e308 && geometryCache.size > GEOMETRY_CACHE_SIZE) {
// Remove oldest entries (simple FIFO approach).
keysToRemove = geometryCacheKeys.splice(0, Math.floor(GEOMETRY_CACHE_SIZE / 2));
results = [];
for (i = 0, len = keysToRemove.length; i < len; i++) {
key = keysToRemove[i];
results.push(geometryCache.delete(key));
}
return results;
}
};
// Create cache key from two polygons.
createIntersectionCacheKey = function(polygonA, polygonB) {
return `${polygonA.id}_${polygonB.id}`;
};
// Memory management utilities.
checkMemoryUsage = function() {
var heapTotalMB, heapUsedMB, memoryUsage, usageRatio;
if (typeof process !== 'undefined' && process.memoryUsage) {
memoryUsage = process.memoryUsage();
heapUsedMB = memoryUsage.heapUsed / 1024 / 1024;
heapTotalMB = memoryUsage.heapTotal / 1024 / 1024;
usageRatio = memoryUsage.heapUsed / memoryUsage.heapTotal;
if (DEBUG_VERBOSE_LOGGING) {
console.log(`Memory usage: ${heapUsedMB.toFixed(2)}MB / ${heapTotalMB.toFixed(2)}MB (${(usageRatio * 100).toFixed(1)}%)`);
}
if (usageRatio > MEMORY_USAGE_WARNING_LIMIT) {
console.warn(`Memory usage is high: ${(usageRatio * 100).toFixed(1)}%`);
if (typeof (typeof global !== "undefined" && global !== null ? global.gc : void 0) === 'function') {
console.log("Triggering garbage collection...");
global.gc();
}
}
return usageRatio;
}
return 0;
};
// Create a 2D vector buffer with write functionality.
// This helper provides efficient storage and writing of 2D vector data.
// @param vectorCount - The number of vectors this buffer can hold (defaults to DEFAULT_BUFFER_SIZE).
// @return Buffer object with write method and Float32Array storage.
createVector2Buffer = function(vectorCount = DEFAULT_BUFFER_SIZE) {
return {
top: 0,
array: new Float32Array(vectorCount * 2),
write: function(vector) {
this.array[this.top++] = vector.x;
return this.array[this.top++] = vector.y;
}
};
};
// Create a 3D vector buffer with write functionality.
// This helper provides efficient storage and writing of 3D vector data.
// @param vectorCount - The number of vectors this buffer can hold (defaults to DEFAULT_BUFFER_SIZE).
// @return Buffer object with write method and Float32Array storage.
createVector3Buffer = function(vectorCount = DEFAULT_BUFFER_SIZE) {
return {
top: 0,
array: new Float32Array(vectorCount * 3),
write: function(vector) {
this.array[this.top++] = vector.x;
this.array[this.top++] = vector.y;
return this.array[this.top++] = vector.z;
}
};
};
// === POINT AND GEOMETRIC UTILITIES ===
// Sort raycast intersections by distance in ascending order.
// Used for ordering ray intersection results from closest to farthest.
// @param intersectionA - First intersection object with distance property.
// @param intersectionB - Second intersection object with distance property.
// @return Comparison result for sorting (-1, 0, or 1).
sortRaycastIntersectionsByDistance = function(intersectionA, intersectionB) {
return intersectionA.distance - intersectionB.distance;
};
// Round point coordinates to specified decimal precision.
// This helps eliminate floating point precision errors in geometric calculations.
// @param point - Vector3 point to round.
// @param decimalPlaces - Number of decimal places to round to (defaults to DEFAULT_COORDINATE_PRECISION).
// @return The same point object with rounded coordinates.
roundPointCoordinates = function(point, decimalPlaces = DEFAULT_COORDINATE_PRECISION) {
point.x = +point.x.toFixed(decimalPlaces);
point.y = +point.y.toFixed(decimalPlaces);
point.z = +point.z.toFixed(decimalPlaces);
return point;
};
// Extract XYZ coordinates from a flat array at the specified index.
// Helper for working with triangle buffer data in winding number calculations.
// @param coordinatesArray - Float32Array containing XYZ coordinates.
// @param startIndex - Starting index in the array.
// @return Object with x, y, z properties.
extractCoordinatesFromArray = function(coordinatesArray, startIndex) {
return {
x: coordinatesArray[startIndex],
y: coordinatesArray[startIndex + 1],
z: coordinatesArray[startIndex + 2]
};
};
// === POLYGON OPERATIONS ===
// Split a polygon by a plane into front and back fragments.
// This is a core CSG operation that classifies polygon parts relative to a plane.
// The algorithm handles coplanar, front, back, and spanning polygon cases.
// @param polygon - The polygon to split.
// @param plane - The cutting plane with normal and distance properties.
// @param result - Array to store results (optional).
// @return Array of polygon fragments with classification types.
splitPolygonByPlane = function(polygon, plane, result = []) {
var backPolygonFragments, backVertices, currentVertex, currentVertexType, distanceToPlane, fragmentIndex, frontPolygonFragments, frontVertices, i, interpolatedVertex, intersectionParameter, j, k, l, nextVertex, nextVertexIndex, nextVertexType, polygonType, ref, ref1, ref2, ref3, returnPolygon, vertexIndex, vertexType, vertexTypes;
returnPolygon = {
polygon: polygon,
type: "undecided"
};
polygonType = 0;
vertexTypes = [];
// Classify each vertex relative to the plane.
for (vertexIndex = i = 0, ref = polygon.vertices.length; (0 <= ref ? i < ref : i > ref); vertexIndex = 0 <= ref ? ++i : --i) {
distanceToPlane = plane.normal.dot(polygon.vertices[vertexIndex].pos) - plane.distanceFromOrigin;
vertexType = distanceToPlane < -GEOMETRIC_EPSILON ? POLYGON_BACK : distanceToPlane > GEOMETRIC_EPSILON ? POLYGON_FRONT : POLYGON_COPLANAR;
polygonType |= vertexType;
vertexTypes.push(vertexType);
}
// Handle polygon classification based on vertex types.
switch (polygonType) {
case POLYGON_COPLANAR:
returnPolygon.type = plane.normal.dot(polygon.plane.normal) > 0 ? "coplanar-front" : "coplanar-back";
result.push(returnPolygon);
break;
case POLYGON_FRONT:
returnPolygon.type = "front";
result.push(returnPolygon);
break;
case POLYGON_BACK:
returnPolygon.type = "back";
result.push(returnPolygon);
break;
case POLYGON_SPANNING:
frontVertices = [];
backVertices = [];
// Process each edge to build front and back vertex lists.
for (vertexIndex = j = 0, ref1 = polygon.vertices.length; (0 <= ref1 ? j < ref1 : j > ref1); vertexIndex = 0 <= ref1 ? ++j : --j) {
nextVertexIndex = (vertexIndex + 1) % polygon.vertices.length;
currentVertexType = vertexTypes[vertexIndex];
nextVertexType = vertexTypes[nextVertexIndex];
currentVertex = polygon.vertices[vertexIndex];
nextVertex = polygon.vertices[nextVertexIndex];
// Add vertex to front list if not behind plane.
if (currentVertexType !== POLYGON_BACK) {
frontVertices.push(currentVertex);
}
// Add vertex to back list if not in front of plane.
if (currentVertexType !== POLYGON_FRONT) {
backVertices.push(currentVertexType !== POLYGON_BACK ? currentVertex.clone() : currentVertex);
}
// Handle edge intersections with the plane.
if ((currentVertexType | nextVertexType) === POLYGON_SPANNING) {
intersectionParameter = (plane.distanceFromOrigin - plane.normal.dot(currentVertex.pos)) / plane.normal.dot(temporaryTriangleVertex.copy(nextVertex.pos).sub(currentVertex.pos));
interpolatedVertex = currentVertex.interpolate(nextVertex, intersectionParameter);
frontVertices.push(interpolatedVertex);
backVertices.push(interpolatedVertex.clone());
}
}
// Create front polygon fragments if we have enough vertices.
if (frontVertices.length >= 3) {
if (frontVertices.length > 3) {
frontPolygonFragments = splitPolygonVertexArray(frontVertices);
for (fragmentIndex = k = 0, ref2 = frontPolygonFragments.length; (0 <= ref2 ? k < ref2 : k > ref2); fragmentIndex = 0 <= ref2 ? ++k : --k) {
result.push({
polygon: new Polygon(frontPolygonFragments[fragmentIndex], polygon.shared),
type: "front"
});
}
} else {
result.push({
polygon: new Polygon(frontVertices, polygon.shared),
type: "front"
});
}
}
// Create back polygon fragments if we have enough vertices.
if (backVertices.length >= 3) {
if (backVertices.length > 3) {
backPolygonFragments = splitPolygonVertexArray(backVertices);
for (fragmentIndex = l = 0, ref3 = backPolygonFragments.length; (0 <= ref3 ? l < ref3 : l > ref3); fragmentIndex = 0 <= ref3 ? ++l : --l) {
result.push({
polygon: new Polygon(backPolygonFragments[fragmentIndex], polygon.shared),
type: "back"
});
}
} else {
result.push({
polygon: new Polygon(backVertices, polygon.shared),
type: "back"
});
}
}
}
// If no fragments were created, return the original polygon.
if (result.length === 0) {
result.push(returnPolygon);
}
return result;
};
// Split a polygon vertex array into triangulated fragments.
// This handles polygons with more than 3 vertices by creating triangle fans.
// @param vertexArray - Array of vertices to triangulate.
// @return Array of vertex arrays, each representing a triangle.
splitPolygonVertexArray = function(vertexArray) {
var i, ref, triangleFragments, triangleIndex, triangleVertices;
triangleFragments = [];
// Handle polygons with more than 4 vertices using fan triangulation.
if (vertexArray.length > 4) {
console.warn("[splitPolygonVertexArray] vertexArray.length > 4", vertexArray.length);
for (triangleIndex = i = 3, ref = vertexArray.length; (3 <= ref ? i <= ref : i >= ref); triangleIndex = 3 <= ref ? ++i : --i) {
triangleVertices = [];
triangleVertices.push(vertexArray[0].clone());
triangleVertices.push(vertexArray[triangleIndex - 2].clone());
triangleVertices.push(vertexArray[triangleIndex - 1].clone());
triangleFragments.push(triangleVertices);
}
} else {
// For quadrilaterals, choose the best diagonal based on distance.
if (vertexArray[0].pos.distanceTo(vertexArray[2].pos) <= vertexArray[1].pos.distanceTo(vertexArray[3].pos)) {
triangleFragments.push([vertexArray[0].clone(), vertexArray[1].clone(), vertexArray[2].clone()], [vertexArray[0].clone(), vertexArray[2].clone(), vertexArray[3].clone()]);
} else {
triangleFragments.push([vertexArray[0].clone(), vertexArray[1].clone(), vertexArray[3].clone()], [vertexArray[1].clone(), vertexArray[2].clone(), vertexArray[3].clone()]);
}
}
return triangleFragments;
};
// === WINDING NUMBER CALCULATIONS ===
// Calculate the winding number for a point relative to triangle mesh data.
// The winding number determines how many times the mesh winds around the test point.
// This is used for robust inside/outside testing of complex 3D geometry.
// @param triangleDataArray - Float32Array containing triangle vertex coordinates.
// @param testPoint - Point to test for winding number.
// @return Integer winding number (0 = outside, non-zero = inside).
calculateWindingNumberFromBuffer = function(triangleDataArray, testPoint) {
var i, ref, solidAngle, triangleStartIndex, vectorLengthA, vectorLengthB, vectorLengthC, windingNumber;
windingNumber = 0;
// Process each triangle in the buffer (9 floats per triangle: 3 vertices × 3 coordinates).
for (triangleStartIndex = i = 0, ref = triangleDataArray.length; i < ref; triangleStartIndex = i += 9) {
windingNumberVector1.subVectors(extractCoordinatesFromArray(triangleDataArray, triangleStartIndex), testPoint);
windingNumberVector2.subVectors(extractCoordinatesFromArray(triangleDataArray, triangleStartIndex + 3), testPoint);
windingNumberVector3.subVectors(extractCoordinatesFromArray(triangleDataArray, triangleStartIndex + 6), testPoint);
vectorLengthA = windingNumberVector1.length();
vectorLengthB = windingNumberVector2.length();
vectorLengthC = windingNumberVector3.length();
// Skip degenerate triangles that are too small to contribute meaningfully.
if (vectorLengthA < POINT_COINCIDENCE_THRESHOLD || vectorLengthB < POINT_COINCIDENCE_THRESHOLD || vectorLengthC < POINT_COINCIDENCE_THRESHOLD) {
continue;
}
// Calculate the solid angle using the determinant formula.
windingNumberMatrix3.set(windingNumberVector1.x, windingNumberVector1.y, windingNumberVector1.z, windingNumberVector2.x, windingNumberVector2.y, windingNumberVector2.z, windingNumberVector3.x, windingNumberVector3.y, windingNumberVector3.z);
solidAngle = 2 * Math.atan2(windingNumberMatrix3.determinant(), vectorLengthA * vectorLengthB * vectorLengthC + windingNumberVector1.dot(windingNumberVector2) * vectorLengthC + windingNumberVector2.dot(windingNumberVector3) * vectorLengthA + windingNumberVector3.dot(windingNumberVector1) * vectorLengthB);
windingNumber += solidAngle;
}
// Round to nearest integer to get the final winding number.
windingNumber = Math.round(windingNumber / WINDING_NUMBER_FULL_ROTATION);
return windingNumber;
};
// Test if a polygon is inside a mesh using winding number algorithm.
// This provides robust inside/outside testing that handles complex cases.
// For coplanar polygons, epsilon offsets are tested to resolve ambiguity.
// @param triangleDataArray - Float32Array containing mesh triangle data.
// @param testPoint - Point to test for inside/outside status.
// @param isCoplanar - Whether the polygon is coplanar with mesh surfaces.
// @return Boolean indicating if the polygon is inside the mesh.
testPolygonInsideUsingWindingNumber = function(triangleDataArray, testPoint, isCoplanar) {
var i, isInside, maxOffsetTests, offsetIndex, ref, windingNumber;
isInside = false;
windingNumberTestPoint.copy(testPoint);
windingNumber = calculateWindingNumberFromBuffer(triangleDataArray, windingNumberTestPoint);
// For non-coplanar cases, winding number directly indicates inside/outside.
if (windingNumber === 0) {
if (isCoplanar) {
// For coplanar cases, test multiple epsilon-offset points.
// Limit iterations to prevent infinite loops in degenerate cases.
maxOffsetTests = Math.min(windingNumberEpsilonOffsetsCount, MAX_REFINEMENT_ITERATIONS);
for (offsetIndex = i = 0, ref = maxOffsetTests; (0 <= ref ? i < ref : i > ref); offsetIndex = 0 <= ref ? ++i : --i) {
windingNumberTestPoint.copy(testPoint).add(windingNumberEpsilonOffsets[offsetIndex]);
windingNumber = calculateWindingNumberFromBuffer(triangleDataArray, windingNumberTestPoint);
if (windingNumber !== 0) {
isInside = true;
break;
}
}
}
} else {
isInside = true;
}
return isInside;
};
// Prepare a triangle buffer from polygon array for winding number calculations.
// This converts polygon data into a flat Float32Array for efficient processing.
// @param polygonArray - Array of polygons to convert.
// @return Float32Array containing triangle vertex coordinates.
prepareTriangleBufferFromPolygons = function(polygonArray) {
var bufferIndex, coordinateArray, i, polygonIndex, ref, triangle, triangleCount;
triangleCount = polygonArray.length;
coordinateArray = new Float32Array(triangleCount * 3 * 3); // 3 vertices × 3 coordinates per triangle
bufferIndex = 0;
for (polygonIndex = i = 0, ref = triangleCount; (0 <= ref ? i < ref : i > ref); polygonIndex = 0 <= ref ? ++i : --i) {
triangle = polygonArray[polygonIndex].triangle;
// Store first vertex coordinates.
coordinateArray[bufferIndex++] = triangle.a.x;
coordinateArray[bufferIndex++] = triangle.a.y;
coordinateArray[bufferIndex++] = triangle.a.z;
// Store second vertex coordinates.
coordinateArray[bufferIndex++] = triangle.b.x;
coordinateArray[bufferIndex++] = triangle.b.y;
coordinateArray[bufferIndex++] = triangle.b.z;
// Store third vertex coordinates.
coordinateArray[bufferIndex++] = triangle.c.x;
coordinateArray[bufferIndex++] = triangle.c.y;
coordinateArray[bufferIndex++] = triangle.c.z;
}
return coordinateArray;
};
// === RAY-TRIANGLE INTERSECTION ===
// Test ray-triangle intersection using the Möller–Trumbore algorithm.
// This is a fast, efficient algorithm for ray-triangle intersection testing.
// Returns the intersection point if found, or null if no intersection exists.
// @param ray - Ray object with origin and direction properties.
// @param triangle - Triangle object with a, b, c vertex properties.
// @param targetVector - Optional Vector3 to store the intersection point.
// @return Vector3 intersection point or null if no intersection.
testRayTriangleIntersection = function(ray, triangle, targetVector = new Vector3()) {
var determinant, firstBarycentricCoordinate, intersectionDistance, inverseDeterminant, secondBarycentricCoordinate;
// Calculate triangle edge vectors.
rayTriangleEdge1.subVectors(triangle.b, triangle.a);
rayTriangleEdge2.subVectors(triangle.c, triangle.a);
// Calculate determinant to check if ray is parallel to triangle.
rayTriangleHVector.crossVectors(ray.direction, rayTriangleEdge2);
determinant = rayTriangleEdge1.dot(rayTriangleHVector);
// If determinant is near zero, ray is parallel to triangle plane.
if (determinant > -RAY_INTERSECTION_EPSILON && determinant < RAY_INTERSECTION_EPSILON) {
return null;
}
inverseDeterminant = 1 / determinant;
rayTriangleSVector.subVectors(ray.origin, triangle.a);
firstBarycentricCoordinate = inverseDeterminant * rayTriangleSVector.dot(rayTriangleHVector);
// Check if intersection point is outside triangle (first barycentric test).
if (firstBarycentricCoordinate < 0 || firstBarycentricCoordinate > 1) {
return null;
}
rayTriangleQVector.crossVectors(rayTriangleSVector, rayTriangleEdge1);
secondBarycentricCoordinate = inverseDeterminant * ray.direction.dot(rayTriangleQVector);
// Check if intersection point is outside triangle (second barycentric test).
if (secondBarycentricCoordinate < 0 || firstBarycentricCoordinate + secondBarycentricCoordinate > 1) {
return null;
}
// Calculate intersection distance along ray.
intersectionDistance = inverseDeterminant * rayTriangleEdge2.dot(rayTriangleQVector);
// Check if intersection is in front of ray origin.
if (intersectionDistance > RAY_INTERSECTION_EPSILON) {
return targetVector.copy(ray.direction).multiplyScalar(intersectionDistance).add(ray.origin);
}
return null;
};
// === POLYTREE MANAGEMENT ===
// Handle intersection processing between two polytrees.
// This coordinates the CSG intersection algorithm by preparing triangle buffers
// and calling intersection handling methods on the polytree instances.
// @param polytreeA - First polytree for intersection processing.
// @param polytreeB - Second polytree for intersection processing.
// @param processBothDirections - Whether to process intersections in both directions.
handleIntersectingPolytrees = function(polytreeA, polytreeB, processBothDirections = true) {
var polytreeABuffer, polytreeBBuffer;
operationCounter++; // Increment operation counter and check for GC threshold.
if (operationCounter >= GARBAGE_COLLECTION_THRESHOLD) {
operationCounter = 0;
clearIntersectionCache();
clearGeometryCache();
checkMemoryUsage();
}
polytreeABuffer = void 0;
polytreeBBuffer = void 0;
// Prepare triangle buffers if winding number algorithm is enabled.
if (Polytree.useWindingNumber === true) {
if (processBothDirections) {
polytreeABuffer = prepareTriangleBufferFromPolygons(polytreeA.getPolygons());
}
polytreeBBuffer = prepareTriangleBufferFromPolygons(polytreeB.getPolygons());
}
// Process intersections from A's perspective.
polytreeA.handleIntersectingPolygons(polytreeB, polytreeBBuffer);
// Process intersections from B's perspective if requested.
if (processBothDirections) {
polytreeB.handleIntersectingPolygons(polytreeA, polytreeABuffer);
}
// Clean up buffers to free memory.
if (polytreeABuffer !== void 0) {
polytreeABuffer = void 0;
}
if (polytreeBBuffer !== void 0) {
return polytreeBBuffer = void 0;
}
};
// Calculate the signed volume of a tetrahedron formed by the origin and three triangle vertices.
// This is used for volume calculations of 3D geometries by summing the signed volumes of all triangles in the mesh.
// The formula is: V = (1/6) * dot(v1, cross(v2, v3))
// where v1, v2, v3 are the three vertices of the triangle.
// @param vertex1 - First vertex of the triangle (Vector3).
// @param vertex2 - Second vertex of the triangle (Vector3).
// @param vertex3 - Third vertex of the triangle (Vector3).
// @return Signed volume of the tetrahedron formed by origin and the three vertices.
signedVolumeOfTriangle = function(vertex1, vertex2, vertex3) {
// Use temporary vectors to avoid creating new objects
temporaryVector3Primary.copy(vertex2).cross(vertex3);
return vertex1.dot(temporaryVector3Primary) / 6.0;
};
// Dispose of polytree resources to prevent memory leaks.
// This utility safely calls the delete method on polytree instances
// if the disposal feature is enabled in the Polytree configuration.
// @param polytreeInstances - Variable number of polytree instances to dispose.
disposePolytreeResources = function(...polytreeInstances) {
if (Polytree.disposePolytree) {
return polytreeInstances.forEach(function(polytreeInstance) {
return polytreeInstance.delete();
});
}
};
// Generated by CoffeeScript 2.7.0
// Main operation handler for processing CSG operations on Polytree objects.
// This function handles both synchronous and asynchronous CSG operations (unite, subtract, intersect)
// and manages the conversion between meshes and polytrees as needed.
// @param operationObject - Object containing the operation definition with properties:
// - op: Operation type ('unite', 'subtract', 'intersect')
// - objA: First operand (mesh, polytree, or nested operation)
// - objB: Second operand (mesh, polytree, or nested operation)
// - material: Optional material for final mesh output
// @param returnPolytrees - Whether to return polytree objects instead of meshes.
// @param buildTargetPolytree - Whether to build the target polytree structure.
// @param options - Configuration options including objCounter for unique IDs.
// @param firstRun - Whether this is the top-level operation call.
// @param async - Whether to execute asynchronously using promises.
// @return Result mesh, polytree, or operation tree depending on parameters.
var handleObjectForOperation, operationHandler;
operationHandler = function(operationObject, returnPolytrees = false, buildTargetPolytree = true, options = {
objCounter: 0
}, firstRun = true, async = true) {
var allPolygons, defaultMaterial, finalMesh, firstOperand, materialForMesh, ref, resultPolytree, secondOperand;
if (async) {
return new Promise(function(resolve, reject) {
var asyncError, firstOperand, materialForMesh, operandPromise, operandPromises, originalMaterialA, originalMaterialB, ref, ref1, resultPolytree, secondOperand;
try {
// Initialize variables for the two operands and result.
firstOperand = void 0;
secondOperand = void 0;
resultPolytree = void 0;
materialForMesh = void 0;
// Capture original material for default use before processing operands.
originalMaterialA = (ref = operationObject.objA) != null ? ref.material : void 0;
originalMaterialB = (ref1 = operationObject.objB) != null ? ref1.material : void 0;
if (operationObject.material) {
materialForMesh = operationObject.material;
}
// Process operands in parallel for async operations.
operandPromises = [];
if (operationObject.objA) {
operandPromise = handleObjectForOperation(operationObject.objA, returnPolytrees, buildTargetPolytree, options, 0, async);
operandPromises.push(operandPromise);
}
if (operationObject.objB) {
operandPromise = handleObjectForOperation(operationObject.objB, returnPolytrees, buildTargetPolytree, options, 1, async);
operandPromises.push(operandPromise);
}
return Promise.allSettled(operandPromises).then(function(promiseResults) {
var operationPromise;
// Extract operands from promise results.
promiseResults.forEach(function(promiseResult) {
if (promiseResult.status === "fulfilled") {
if (promiseResult.value.objIndex === 0) {
return firstOperand = promiseResult.value;
} else if (promiseResult.value.objIndex === 1) {
return secondOperand = promiseResult.value;
}
}
});
// Handle polytree return mode by updating operation object references.
if (returnPolytrees === true) {
// Extract polytrees from wrapper objects when in polytree return mode.
if (firstOperand) {
operationObject.objA = firstOperand.original;
firstOperand = firstOperand.result;
}
if (secondOperand) {
operationObject.objB = secondOperand.original;
secondOperand = secondOperand.result;
}
}
// Execute the appropriate CSG operation based on operation type.
operationPromise = void 0;
switch (operationObject.op) {
case 'unite':
if (firstOperand && secondOperand) {
operationPromise = Polytree.async.unite(firstOperand, secondOperand, buildTargetPolytree);
} else {
// Handle missing operands gracefully.
operationPromise = Promise.resolve(firstOperand || secondOperand || new Polytree());
}
break;
case 'subtract':
if (firstOperand && secondOperand) {
operationPromise = Polytree.async.subtract(firstOperand, secondOperand, buildTargetPolytree);
} else {
// For subtract, return first operand or empty polytree.
operationPromise = Promise.resolve(firstOperand || new Polytree());
}
break;
case 'intersect':
if (firstOperand && secondOperand) {
operationPromise = Polytree.async.intersect(firstOperand, secondOperand, buildTargetPolytree);
} else {
// For intersect, missing operand means no result.
operationPromise = Promise.resolve(new Polytree());
}
break;
default:
// Handle invalid operation types gracefully.
operationPromise = Promise.resolve(new Polytree());
}
// Handle the operation result and determine final output format.
return operationPromise.then(function(resultPolytree) {
var allPolygons, defaultMaterial, finalMesh;
// Ensure result polytree has proper bounding box for subsequent operations.
if (resultPolytree && !resultPolytree.box && resultPolytree.bounds) {
resultPolytree.buildTree();
}
// Convert to mesh for first run operations unless returning polytrees.
if (firstRun && !returnPolytrees) {
// Skip mesh conversion if result polytree has no valid polygons.
allPolygons = resultPolytree.getPolygons();
if (!resultPolytree || allPolygons.length === 0) {
resolve(void 0);
return;
}
if (materialForMesh) {
finalMesh = Polytree.toMesh(resultPolytree, materialForMesh);
} else {
// Use default material from original operands if no material specified.
defaultMaterial = void 0;
if (originalMaterialA) {
defaultMaterial = Array.isArray(originalMaterialA) ? originalMaterialA[0] : originalMaterialA;
defaultMaterial = defaultMaterial.clone();
} else if (originalMaterialB) {
defaultMaterial = Array.isArray(originalMaterialB) ? originalMaterialB[0] : originalMaterialB;
defaultMaterial = defaultMaterial.clone();
} else {
// No material available - resolve with undefined.
resolve(void 0);
return;
}
finalMesh = Polytree.toMesh(resultPolytree, defaultMaterial);
}
disposePolytreeResources(resultPolytree);
resolve(finalMesh);
} else if (firstRun && returnPolytrees) {
if (materialForMesh) {
finalMesh = Polytree.toMesh(resultPolytree, materialForMesh);
disposePolytreeResources(resultPolytree);
resolve({
result: finalMesh,
operationTree: operationObject
});
} else {
resolve({
result: resultPolytree,
operationTree: operationObject
});
}
} else {
resolve(resultPolytree);
}
// Clean up intermediate polytrees unless returning them.
if (!returnPolytrees) {
if (firstOperand || secondOperand) {
return disposePolytreeResources(firstOperand, secondOperand);
}
}
}).catch(function(operationError) {
return reject(operationError);
});
});
} catch (error) {
asyncError = error;
return reject(asyncError);
}
});
} else {
// Synchronous operation handling.
firstOperand = void 0;
secondOperand = void 0;
resultPolytree = void 0;
materialForMesh = void 0;
if (operationObject.material) {
materialForMesh = operationObject.material;
}
// Process first operand.
if (operationObject.objA) {
firstOperand = handleObjectForOperation(operationObject.objA, returnPolytrees, buildTargetPolytree, options, void 0, async);
if (returnPolytrees === true) {
operationObject.objA = firstOperand.original;
firstOperand = firstOperand.result;
}
}
// Process second operand.
if (operationObject.objB) {
secondOperand = handleObjectForOperation(operationObject.objB, returnPolytrees, buildTargetPolytree, options, void 0, async);
if (returnPolytrees === true) {
operationObject.objB = secondOperand.original;
secondOperand = secondOperand.result;
}
}
// Execute the appropriate CSG operation.
switch (operationObject.op) {
case 'unite':
if (firstOperand && secondOperand) {
resultPolytree = Polytree.unite(firstOperand, secondOperand, false);
} else {
// Handle missing operands - return the available operand or empty polytree.
resultPolytree = firstOperand || secondOperand || new Polytree();
}
break;
case 'subtract':
if (firstOperand && secondOperand) {
resultPolytree = Polytree.subtract(firstOperand, secondOperand, false);
} else {
// For subtract, if missing second operand, return first; if missing first, return empty.
resultPolytree = firstOperand || new Polytree();
}
break;
case 'intersect':
if (firstOperand && secondOperand) {
resultPolytree = Polytree.intersect(firstOperand, secondOperand, false);
} else {
// For intersect, missing either operand means no intersection - return empty polytree.
resultPolytree = new Polytree();
}
break;
default:
// Handle invalid operation types gracefully - return empty polytree.
resultPolytree = new Polytree();
}
// Ensure result polytree has proper bounding box for subsequent operations.
if (resultPolytree && !resultPolytree.box && resultPolytree.bounds) {
resultPolytree.buildTree();
}
// Clean up intermediate polytrees unless returning them.
if (!returnPolytrees) {
if (firstOperand || secondOperand) {
disposePolytreeResources(firstOperand, secondOperand);
}
}
// Handle final output format for synchronous operations.
if (firstRun && !returnPolytrees) {
// Skip mesh conversion if result polytree has no valid polygons.
allPolygons = resultPolytree.getPolygons();
if (!resultPolytree || allPolygons.length === 0) {
return void 0;
}
// Convert polytree result to mesh for top-level operations.
if (materialForMesh) {
finalMesh = Polytree.toMesh(resultPolytree, materialForMesh);
} else {
// Use default material from first operand if no material specified.
defaultMaterial = void 0;
if ((ref = operationObject.objA) != null ? ref.material : void 0) {
defaultMaterial = Array.isArray(operationObject.objA.material) ? operationObject.objA.material[0] : operationObject.objA.material;
defaultMaterial = defaultMaterial.clone();
} else {
return void 0; // No material available - return undefined instead of creating empty mesh.
}
finalMesh = Polytree.toMesh(resultPolytree, defaultMaterial);
}
disposePolytreeResources(resultPolytree);
return finalMesh;
}
if (firstRun && returnPolytrees) {
return {
result: resultPolytree,
operationTree: operationObject
};
}
return resultPolytree;
}
};