UNPKG

three-bvh-csg

Version:

A fast, flexible, dynamic CSG implementation on top of three-mesh-bvh

342 lines (233 loc) 8.62 kB
import { Vector3, Line3 } from 'three'; import { ExtendedTriangle } from 'three-mesh-bvh'; import cdt2d from '../libs/cdt2d.js'; import { Pool } from './utils/Pool.js'; // relative tolerance factor — multiplied by the max absolute coordinate // of the base triangle to get scale-appropriate thresholds const RELATIVE_EPSILON = 1e-16; // tolerance for merging nearby vertices (squared distance) const VERTEX_MERGE_EPSILON = 1e-16; const _vec = new Vector3(); const _vec2 = new Vector3(); const _paramPool = new Pool( () => ( { param: 0, index: 0 } ) ); const _vectorPool = new Pool( () => new Vector3() ); function edgesToIndices( edges, outputVertices, outputIndices, epsilonScale ) { _paramPool.clear(); outputVertices.length = 0; outputIndices.length = 0; // Add edge endpoints and find edge-edge intersection points for ( let i = 0, l = edges.length; i < l; i ++ ) { const edge0 = edges[ i ]; getIndex( edge0.start ); getIndex( edge0.end ); } for ( let i = 0, l = edges.length; i < l; i ++ ) { const edge0 = edges[ i ]; for ( let i1 = i + 1; i1 < l; i1 ++ ) { const edge1 = edges[ i1 ]; const dist = edge0.distanceSqToLine3( edge1, _vec, _vec2 ); if ( dist < RELATIVE_EPSILON * epsilonScale ) { getIndex( _vec2 ); } } } // Build sub-segments by finding all vertices on each edge const arr = []; for ( let i = 0, l = edges.length; i < l; i ++ ) { arr.length = 0; const edge = edges[ i ]; for ( let v = 0, lv = outputVertices.length; v < lv; v ++ ) { const vec = outputVertices[ v ]; const param = edge.closestPointToPointParameter( vec, true ); edge.at( param, _vec ); if ( vec.distanceToSquared( _vec ) < RELATIVE_EPSILON * epsilonScale ) { const entry = _paramPool.getInstance(); entry.param = param; entry.index = v; arr.push( entry ); } } arr.sort( paramSort ); for ( let a = 0, la = arr.length - 1; a < la; a ++ ) { const i0 = arr[ a ].index; const i1 = arr[ a + 1 ].index; // Skip self-loops (can arise when two endpoints merge) if ( i0 === i1 ) continue; outputIndices.push( [ i0, i1 ] ); } } // Remove duplicate edges const edgeSet = new Set(); let ptr = 0; for ( let i = 0, l = outputIndices.length; i < l; i ++ ) { const e = outputIndices[ i ]; const lo = Math.min( e[ 0 ], e[ 1 ] ); const hi = Math.max( e[ 0 ], e[ 1 ] ); const key = lo + ',' + hi; if ( ! edgeSet.has( key ) ) { edgeSet.add( key ); outputIndices[ ptr ++ ] = e; } } outputIndices.length = ptr; function paramSort( a, b ) { return a.param - b.param; } function getIndex( v ) { for ( let i = 0; i < outputVertices.length; i ++ ) { const v2 = outputVertices[ i ]; if ( v === v2 || v.distanceToSquared( v2 ) < VERTEX_MERGE_EPSILON * epsilonScale ) { return i; } } outputVertices.push( _vectorPool.getInstance().copy( v ) ); return outputVertices.length - 1; } } export class CDTTriangleSplitter { constructor() { this.trianglePool = new Pool( () => new ExtendedTriangle() ); this.linePool = new Pool( () => new Line3() ); // TODO: use array pool this.triangles = []; this.triangleIndices = []; this.constrainedEdges = []; this.triangleConnectivity = []; this.normal = new Vector3(); this.projOrigin = new Vector3(); this.projU = new Vector3(); this.projV = new Vector3(); this.baseTri = new ExtendedTriangle(); this.baseIndices = new Array( 3 ); } // initialize the class with a triangle to be split initialize( tri, i0 = null, i1 = null, i2 = null ) { this.reset(); const { normal, baseTri, projU, projV, projOrigin, constrainedEdges, linePool, baseIndices } = this; tri.getNormal( normal ); baseTri.copy( tri ); baseTri.update(); baseIndices[ 0 ] = i0; baseIndices[ 1 ] = i1; baseIndices[ 2 ] = i2; // initialize constrained edges to the triangle boundary constrainedEdges.length = 0; // inserting these edges in this order guarantee that indices a, b, c will be given the // indices 0, 1, 2 so we can infer base indices from them later. const e0 = linePool.getInstance(); e0.start.copy( baseTri.a ); e0.end.copy( baseTri.b ); const e1 = linePool.getInstance(); e1.start.copy( baseTri.b ); e1.end.copy( baseTri.c ); const e2 = linePool.getInstance(); e2.start.copy( baseTri.c ); e2.end.copy( baseTri.a ); constrainedEdges.push( e0, e1, e2 ); // Build 2D projection frame from base triangle projOrigin.copy( baseTri.a ); projU.subVectors( baseTri.b, baseTri.a ).normalize(); projV.crossVectors( normal, projU ).normalize(); } // Add a pre-computed constraint edge to the splitter addConstraintEdge( edge ) { const { constrainedEdges, linePool } = this; const e = linePool.getInstance().copy( edge ); constrainedEdges.push( e ); } // Project a 3D point onto the 2D frame defined by _projOrigin / _projU / _projV _to2D( point, target ) { const { projOrigin, projU, projV } = this; _vec.subVectors( point, projOrigin ); return target.set( _vec.dot( projU ), _vec.dot( projV ), 0 ); } _from2D( u, v, target ) { const { projOrigin, projU, projV } = this; target.copy( projOrigin ).addScaledVector( projU, u ).addScaledVector( projV, v ); return target; } // Run the CDT and populate this.triangles with the result triangulate() { const { triangles, trianglePool, triangleConnectivity, triangleIndices, linePool, baseTri, constrainedEdges, baseIndices } = this; triangles.length = 0; trianglePool.clear(); // Get the edges into a 2d frame const edges2d = []; for ( let i = 0, l = constrainedEdges.length; i < l; i ++ ) { const edge = constrainedEdges[ i ]; const e2d = linePool.getInstance(); this._to2D( edge.start, e2d.start ); this._to2D( edge.end, e2d.end ); edges2d.push( e2d ); } // Precompute scale factor from base triangle for epsilon scaling let epsilonScale = 0; for ( let i = 0; i < 3; i ++ ) { const v = this._to2D( baseTri.points[ i ], _vec ); epsilonScale = Math.max( epsilonScale, Math.abs( v.x ), Math.abs( v.y ) ); } // Use custom deduplication and edge splitting const vertices = []; const indices = []; edgesToIndices( edges2d, vertices, indices, epsilonScale ); const cdt2dPoints = []; for ( let i = 0, l = vertices.length; i < l; i ++ ) { const vert = vertices[ i ]; cdt2dPoints.push( [ vert.x, vert.y ] ); } // Run the CDT triangulation const triangulation = cdt2d( cdt2dPoints, indices, { exterior: false } ); // construct the half edge structure, marking the constrained edges as disconnected to // mark the polygon edges const halfEdgeMap = new Map(); for ( let i = 0, l = indices.length; i < l; i ++ ) { const pair = indices[ i ]; halfEdgeMap.set( `${ pair[ 0 ] }_${ pair[ 1 ] }`, - 1 ); halfEdgeMap.set( `${ pair[ 1 ] }_${ pair[ 0 ] }`, - 1 ); } // create an index key to construct unique indices across the geometry const indexKeyPrefix = `${ baseIndices[ 0 ] }_${ baseIndices[ 1 ] }_${ baseIndices[ 2 ] }_`; for ( let ti = 0, l = triangulation.length; ti < l; ti ++ ) { // covert back to 2d const indexList = triangulation[ ti ]; const [ i0, i1, i2 ] = indexList; const tri = trianglePool.getInstance(); this._from2D( cdt2dPoints[ i0 ][ 0 ], cdt2dPoints[ i0 ][ 1 ], tri.a ); this._from2D( cdt2dPoints[ i1 ][ 0 ], cdt2dPoints[ i1 ][ 1 ], tri.b ); this._from2D( cdt2dPoints[ i2 ][ 0 ], cdt2dPoints[ i2 ][ 1 ], tri.c ); triangles.push( tri ); // construct the connectivity and custom index list const connected = []; triangleConnectivity.push( connected ); const indexKeys = []; triangleIndices.push( indexKeys ); for ( let i = 0; i < 3; i ++ ) { // use the original geometry index for base triangle corners, // otherwise construct a unique index key for constraint edge vertices const p0 = indexList[ i ]; indexKeys.push( p0 < 3 ? baseIndices[ p0 ] : indexKeyPrefix + p0 ); // find the connected triangles const p1 = indexList[ ( i + 1 ) % 3 ]; const hash0 = `${ p0 }_${ p1 }`; if ( halfEdgeMap.has( hash0 ) ) { const index = halfEdgeMap.get( hash0 ); if ( index !== - 1 ) { connected.push( index ); triangleConnectivity[ index ].push( ti ); } } else { const hash1 = `${ p1 }_${ p0 }`; halfEdgeMap.set( hash1, ti ); } } } } reset() { this.trianglePool.clear(); this.linePool.clear(); this.triangles.length = 0; this.triangleIndices.length = 0; this.triangleConnectivity.length = 0; this.constrainedEdges.length = 0; } }