@wendraw/three-bvh-csg
Version:
A fast, flexible, dynamic CSG implementation on top of three-mesh-bvh
2,409 lines (1,521 loc) • 82.6 kB
JavaScript
import { BufferAttribute, Vector3, Ray, Vector2, Vector4, Mesh, Matrix4, Line3, Plane, Triangle, DoubleSide, Matrix3, BufferGeometry, Group, Color, MeshPhongMaterial, MathUtils, LineSegments, LineBasicMaterial, InstancedMesh, SphereGeometry, MeshBasicMaterial } from 'three';
import { MeshBVH, ExtendedTriangle } from 'three-mesh-bvh';
const HASH_WIDTH = 1e-6;
const HASH_HALF_WIDTH = HASH_WIDTH * 0.5;
const HASH_MULTIPLIER = Math.pow( 10, - Math.log10( HASH_WIDTH ) );
const HASH_ADDITION = HASH_HALF_WIDTH * HASH_MULTIPLIER;
function hashNumber( v ) {
return ~ ~ ( v * HASH_MULTIPLIER + HASH_ADDITION );
}
function hashVertex2( v ) {
return `${ hashNumber( v.x ) },${ hashNumber( v.y ) }`;
}
function hashVertex3( v ) {
return `${ hashNumber( v.x ) },${ hashNumber( v.y ) },${ hashNumber( v.z ) }`;
}
function hashVertex4( v ) {
return `${ hashNumber( v.x ) },${ hashNumber( v.y ) },${ hashNumber( v.z ) },${ hashNumber( v.w ) }`;
}
function hashRay( r ) {
return `${ hashVertex3( r.origin ) }-${ hashVertex3( r.direction ) }`;
}
function toNormalizedRay( v0, v1, target ) {
// get a normalized direction
target
.direction
.subVectors( v1, v0 )
.normalize();
// project the origin onto the perpendicular plane that
// passes through 0, 0, 0
const scalar = v0.dot( target.direction );
target.
origin
.copy( v0 )
.addScaledVector( target.direction, - scalar );
return target;
}
function areSharedArrayBuffersSupported() {
return typeof SharedArrayBuffer !== 'undefined';
}
function convertToSharedArrayBuffer( array ) {
if ( array.buffer instanceof SharedArrayBuffer ) {
return array;
}
const cons = array.constructor;
const buffer = array.buffer;
const sharedBuffer = new SharedArrayBuffer( buffer.byteLength );
const uintArray = new Uint8Array( buffer );
const sharedUintArray = new Uint8Array( sharedBuffer );
sharedUintArray.set( uintArray, 0 );
return new cons( sharedBuffer );
}
function getIndexArray( vertexCount, BufferConstructor = ArrayBuffer ) {
if ( vertexCount > 65535 ) {
return new Uint32Array( new BufferConstructor( 4 * vertexCount ) );
} else {
return new Uint16Array( new BufferConstructor( 2 * vertexCount ) );
}
}
function ensureIndex( geo, options ) {
if ( ! geo.index ) {
const vertexCount = geo.attributes.position.count;
const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
const index = getIndexArray( vertexCount, BufferConstructor );
geo.setIndex( new BufferAttribute( index, 1 ) );
for ( let i = 0; i < vertexCount; i ++ ) {
index[ i ] = i;
}
}
}
function getVertexCount( geo ) {
return geo.index ? geo.index.count : geo.attributes.position.count;
}
function getTriCount( geo ) {
return getVertexCount( geo ) / 3;
}
const DEGENERATE_EPSILON = 1e-8;
const _tempVec = new Vector3();
function toTriIndex( v ) {
return ~ ~ ( v / 3 );
}
function toEdgeIndex( v ) {
return v % 3;
}
function sortEdgeFunc( a, b ) {
return a.start - b.start;
}
function getProjectedDistance( ray, vec ) {
return _tempVec.subVectors( vec, ray.origin ).dot( ray.direction );
}
function hasOverlaps( arr ) {
arr = [ ...arr ].sort( sortEdgeFunc );
for ( let i = 0, l = arr.length; i < l - 1; i ++ ) {
const info0 = arr[ i ];
const info1 = arr[ i + 1 ];
if ( info1.start < info0.end && Math.abs( info1.start - info0.end ) > 1e-5 ) {
return true;
}
}
return false;
}
function getEdgeSetLength( arr ) {
let tot = 0;
arr.forEach( ( { start, end } ) => tot += end - start );
return tot;
}
function matchEdges( forward, reverse, disjointConnectivityMap, eps = DEGENERATE_EPSILON ) {
forward.sort( sortEdgeFunc );
reverse.sort( sortEdgeFunc );
for ( let i = 0; i < forward.length; i ++ ) {
const e0 = forward[ i ];
for ( let o = 0; o < reverse.length; o ++ ) {
const e1 = reverse[ o ];
if ( e1.start > e0.end ) {
// e2 is completely after e1
// break;
// NOTE: there are cases where there are overlaps due to precision issues or
// thin / degenerate triangles. Assuming the sibling side has the same issues
// we let the matching work here. Long term we should remove the degenerate
// triangles before this.
} else if ( e0.end < e1.start || e1.end < e0.start ) {
// e1 is completely before e2
continue;
} else if ( e0.start <= e1.start && e0.end >= e1.end ) {
// e1 is larger than and e2 is completely within e1
if ( ! areDistancesDegenerate( e1.end, e0.end ) ) {
forward.splice( i + 1, 0, {
start: e1.end,
end: e0.end,
index: e0.index,
} );
}
e0.end = e1.start;
e1.start = 0;
e1.end = 0;
} else if ( e0.start >= e1.start && e0.end <= e1.end ) {
// e2 is larger than and e1 is completely within e2
if ( ! areDistancesDegenerate( e0.end, e1.end ) ) {
reverse.splice( o + 1, 0, {
start: e0.end,
end: e1.end,
index: e1.index,
} );
}
e1.end = e0.start;
e0.start = 0;
e0.end = 0;
} else if ( e0.start <= e1.start && e0.end <= e1.end ) {
// e1 overlaps e2 at the beginning
const tmp = e0.end;
e0.end = e1.start;
e1.start = tmp;
} else if ( e0.start >= e1.start && e0.end >= e1.end ) {
// e1 overlaps e2 at the end
const tmp = e1.end;
e1.end = e0.start;
e0.start = tmp;
} else {
throw new Error();
}
// Add the connectivity information
if ( ! disjointConnectivityMap.has( e0.index ) ) {
disjointConnectivityMap.set( e0.index, [] );
}
if ( ! disjointConnectivityMap.has( e1.index ) ) {
disjointConnectivityMap.set( e1.index, [] );
}
disjointConnectivityMap
.get( e0.index )
.push( e1.index );
disjointConnectivityMap
.get( e1.index )
.push( e0.index );
if ( isEdgeDegenerate( e1 ) ) {
reverse.splice( o, 1 );
o --;
}
if ( isEdgeDegenerate( e0 ) ) {
// and if we have to remove the current original edge then exit this loop
// so we can work on the next one
forward.splice( i, 1 );
i --;
break;
}
}
}
cleanUpEdgeSet( forward );
cleanUpEdgeSet( reverse );
function cleanUpEdgeSet( arr ) {
for ( let i = 0; i < arr.length; i ++ ) {
if ( isEdgeDegenerate( arr[ i ] ) ) {
arr.splice( i, 1 );
i --;
}
}
}
function areDistancesDegenerate( start, end ) {
return Math.abs( end - start ) < eps;
}
function isEdgeDegenerate( e ) {
return Math.abs( e.end - e.start ) < eps;
}
}
const DIST_EPSILON = 1e-5;
const ANGLE_EPSILON = 1e-4;
class RaySet {
constructor() {
this._rays = [];
}
addRay( ray ) {
this._rays.push( ray );
}
findClosestRay( ray ) {
const rays = this._rays;
const inv = ray.clone();
inv.direction.multiplyScalar( - 1 );
let bestScore = Infinity;
let bestRay = null;
for ( let i = 0, l = rays.length; i < l; i ++ ) {
const r = rays[ i ];
if ( skipRay( r, ray ) && skipRay( r, inv ) ) {
continue;
}
const rayScore = scoreRays( r, ray );
const invScore = scoreRays( r, inv );
const score = Math.min( rayScore, invScore );
if ( score < bestScore ) {
bestScore = score;
bestRay = r;
}
}
return bestRay;
function skipRay( r0, r1 ) {
const distOutOfThreshold = r0.origin.distanceTo( r1.origin ) > DIST_EPSILON;
const angleOutOfThreshold = r0.direction.angleTo( r1.direction ) > ANGLE_EPSILON;
return angleOutOfThreshold || distOutOfThreshold;
}
function scoreRays( r0, r1 ) {
const originDistance = r0.origin.distanceTo( r1.origin );
const angleDistance = r0.direction.angleTo( r1.direction );
return originDistance / DIST_EPSILON + angleDistance / ANGLE_EPSILON;
}
}
}
const _v0 = new Vector3();
const _v1 = new Vector3();
const _ray$2 = new Ray();
function computeDisjointEdges(
geometry,
unmatchedSet,
eps,
) {
const attributes = geometry.attributes;
const indexAttr = geometry.index;
const posAttr = attributes.position;
const disjointConnectivityMap = new Map();
const fragmentMap = new Map();
const edges = Array.from( unmatchedSet );
const rays = new RaySet();
for ( let i = 0, l = edges.length; i < l; i ++ ) {
// get the triangle edge
const index = edges[ i ];
const triIndex = toTriIndex( index );
const edgeIndex = toEdgeIndex( index );
let i0 = 3 * triIndex + edgeIndex;
let i1 = 3 * triIndex + ( edgeIndex + 1 ) % 3;
if ( indexAttr ) {
i0 = indexAttr.getX( i0 );
i1 = indexAttr.getX( i1 );
}
_v0.fromBufferAttribute( posAttr, i0 );
_v1.fromBufferAttribute( posAttr, i1 );
// get the ray corresponding to the edge
toNormalizedRay( _v0, _v1, _ray$2 );
// find the shared ray with other edges
let info;
let commonRay = rays.findClosestRay( _ray$2 );
if ( commonRay === null ) {
commonRay = _ray$2.clone();
rays.addRay( commonRay );
}
if ( ! fragmentMap.has( commonRay ) ) {
fragmentMap.set( commonRay, {
forward: [],
reverse: [],
ray: commonRay,
} );
}
info = fragmentMap.get( commonRay );
// store the stride of edge endpoints along the ray
let start = getProjectedDistance( commonRay, _v0 );
let end = getProjectedDistance( commonRay, _v1 );
if ( start > end ) {
[ start, end ] = [ end, start ];
}
if ( _ray$2.direction.dot( commonRay.direction ) < 0 ) {
info.reverse.push( { start, end, index } );
} else {
info.forward.push( { start, end, index } );
}
}
// match the found sibling edges
fragmentMap.forEach( ( { forward, reverse }, ray ) => {
matchEdges( forward, reverse, disjointConnectivityMap, eps );
if ( forward.length === 0 && reverse.length === 0 ) {
fragmentMap.delete( ray );
}
} );
return {
disjointConnectivityMap,
fragmentMap,
};
}
const _vec2$1 = new Vector2();
const _vec3$1 = new Vector3();
const _vec4 = new Vector4();
const _hashes = [ '', '', '' ];
class HalfEdgeMap {
constructor( geometry = null ) {
// result data
this.data = null;
this.disjointConnections = null;
this.unmatchedDisjointEdges = null;
this.unmatchedEdges = - 1;
this.matchedEdges = - 1;
// options
this.useDrawRange = true;
this.useAllAttributes = false;
this.matchDisjointEdges = false;
this.degenerateEpsilon = 1e-8;
if ( geometry ) {
this.updateFrom( geometry );
}
}
getSiblingTriangleIndex( triIndex, edgeIndex ) {
const otherIndex = this.data[ triIndex * 3 + edgeIndex ];
return otherIndex === - 1 ? - 1 : ~ ~ ( otherIndex / 3 );
}
getSiblingEdgeIndex( triIndex, edgeIndex ) {
const otherIndex = this.data[ triIndex * 3 + edgeIndex ];
return otherIndex === - 1 ? - 1 : ( otherIndex % 3 );
}
getDisjointSiblingTriangleIndices( triIndex, edgeIndex ) {
const index = triIndex * 3 + edgeIndex;
const arr = this.disjointConnections.get( index );
return arr ? arr.map( i => ~ ~ ( i / 3 ) ) : [];
}
getDisjointSiblingEdgeIndices( triIndex, edgeIndex ) {
const index = triIndex * 3 + edgeIndex;
const arr = this.disjointConnections.get( index );
return arr ? arr.map( i => i % 3 ) : [];
}
isFullyConnected() {
return this.unmatchedEdges === 0;
}
updateFrom( geometry ) {
const { useAllAttributes, useDrawRange, matchDisjointEdges, degenerateEpsilon } = this;
const hashFunction = useAllAttributes ? hashAllAttributes : hashPositionAttribute;
// runs on the assumption that there is a 1 : 1 match of edges
const map = new Map();
// attributes
const { attributes } = geometry;
const attrKeys = useAllAttributes ? Object.keys( attributes ) : null;
const indexAttr = geometry.index;
const posAttr = attributes.position;
// get the potential number of triangles
let triCount = getTriCount( geometry );
const maxTriCount = triCount;
// get the real number of triangles from the based on the draw range
let offset = 0;
if ( useDrawRange ) {
offset = geometry.drawRange.start;
if ( geometry.drawRange.count !== Infinity ) {
triCount = ~ ~ ( geometry.drawRange.count / 3 );
}
}
// initialize the connectivity buffer - 1 means no connectivity
let data = this.data;
if ( ! data || data.length < 3 * maxTriCount ) {
data = new Int32Array( 3 * maxTriCount );
}
data.fill( - 1 );
// iterate over all triangles
let matchedEdges = 0;
let unmatchedSet = new Set();
for ( let i = offset, l = triCount * 3 + offset; i < l; i += 3 ) {
const i3 = i;
for ( let e = 0; e < 3; e ++ ) {
let i0 = i3 + e;
if ( indexAttr ) {
i0 = indexAttr.getX( i0 );
}
_hashes[ e ] = hashFunction( i0 );
}
for ( let e = 0; e < 3; e ++ ) {
const nextE = ( e + 1 ) % 3;
const vh0 = _hashes[ e ];
const vh1 = _hashes[ nextE ];
const reverseHash = `${ vh1 }_${ vh0 }`;
if ( map.has( reverseHash ) ) {
// create a reference between the two triangles and clear the hash
const index = i3 + e;
const otherIndex = map.get( reverseHash );
data[ index ] = otherIndex;
data[ otherIndex ] = index;
map.delete( reverseHash );
matchedEdges += 2;
unmatchedSet.delete( otherIndex );
} else {
// save the triangle and triangle edge index captured in one value
// triIndex = ~ ~ ( i0 / 3 );
// edgeIndex = i0 % 3;
const hash = `${ vh0 }_${ vh1 }`;
const index = i3 + e;
map.set( hash, index );
unmatchedSet.add( index );
}
}
}
if ( matchDisjointEdges ) {
const {
fragmentMap,
disjointConnectivityMap,
} = computeDisjointEdges( geometry, unmatchedSet, degenerateEpsilon );
unmatchedSet.clear();
fragmentMap.forEach( ( { forward, reverse } ) => {
forward.forEach( ( { index } ) => unmatchedSet.add( index ) );
reverse.forEach( ( { index } ) => unmatchedSet.add( index ) );
} );
this.unmatchedDisjointEdges = fragmentMap;
this.disjointConnections = disjointConnectivityMap;
matchedEdges = triCount * 3 - unmatchedSet.size;
}
this.matchedEdges = matchedEdges;
this.unmatchedEdges = unmatchedSet.size;
this.data = data;
function hashPositionAttribute( i ) {
_vec3$1.fromBufferAttribute( posAttr, i );
return hashVertex3( _vec3$1 );
}
function hashAllAttributes( i ) {
let result = '';
for ( let k = 0, l = attrKeys.length; k < l; k ++ ) {
const attr = attributes[ attrKeys[ k ] ];
let str;
switch ( attr.itemSize ) {
case 1:
str = hashNumber( attr.getX( i ) );
break;
case 2:
str = hashVertex2( _vec2$1.fromBufferAttribute( attr, i ) );
break;
case 3:
str = hashVertex3( _vec3$1.fromBufferAttribute( attr, i ) );
break;
case 4:
str = hashVertex4( _vec4.fromBufferAttribute( attr, i ) );
break;
}
if ( result !== '' ) {
result += '|';
}
result += str;
}
return result;
}
}
}
class Brush extends Mesh {
constructor( ...args ) {
super( ...args );
this.isBrush = true;
this._previousMatrix = new Matrix4();
this._previousMatrix.elements.fill( 0 );
}
markUpdated() {
this._previousMatrix.copy( this.matrix );
}
isDirty() {
const { matrix, _previousMatrix } = this;
const el1 = matrix.elements;
const el2 = _previousMatrix.elements;
for ( let i = 0; i < 16; i ++ ) {
if ( el1[ i ] !== el2[ i ] ) {
return true;
}
}
return false;
}
prepareGeometry() {
// generate shared array buffers
const geometry = this.geometry;
const attributes = geometry.attributes;
const useSharedArrayBuffer = areSharedArrayBuffersSupported();
if ( useSharedArrayBuffer ) {
for ( const key in attributes ) {
const attribute = attributes[ key ];
if ( attribute.isInterleavedBufferAttribute ) {
throw new Error( 'Brush: InterleavedBufferAttributes are not supported.' );
}
attribute.array = convertToSharedArrayBuffer( attribute.array );
}
}
// generate bounds tree
if ( ! geometry.boundsTree ) {
ensureIndex( geometry, { useSharedArrayBuffer } );
geometry.boundsTree = new MeshBVH( geometry, { maxLeafTris: 3, indirect: true, useSharedArrayBuffer } );
}
// generate half edges
if ( ! geometry.halfEdges ) {
geometry.halfEdges = new HalfEdgeMap( geometry );
}
// save group indices for materials
if ( ! geometry.groupIndices ) {
const triCount = getTriCount( geometry );
const array = new Uint16Array( triCount );
const groups = geometry.groups;
for ( let i = 0, l = groups.length; i < l; i ++ ) {
const { start, count } = groups[ i ];
for ( let g = start / 3, lg = ( start + count ) / 3; g < lg; g ++ ) {
array[ g ] = i;
}
}
geometry.groupIndices = array;
}
}
disposeCacheData() {
const { geometry } = this;
geometry.halfEdges = null;
geometry.boundsTree = null;
geometry.groupIndices = null;
}
}
const EPSILON$1 = 1e-14;
const _AB = new Vector3();
const _AC = new Vector3();
const _CB = new Vector3();
function isTriDegenerate( tri, eps = EPSILON$1 ) {
// compute angles to determine whether they're degenerate
_AB.subVectors( tri.b, tri.a );
_AC.subVectors( tri.c, tri.a );
_CB.subVectors( tri.b, tri.c );
const angle1 = _AB.angleTo( _AC ); // AB v AC
const angle2 = _AB.angleTo( _CB ); // AB v BC
const angle3 = Math.PI - angle1 - angle2; // 180deg - angle1 - angle2
return Math.abs( angle1 ) < eps ||
Math.abs( angle2 ) < eps ||
Math.abs( angle3 ) < eps ||
tri.a.distanceToSquared( tri.b ) < eps ||
tri.a.distanceToSquared( tri.c ) < eps ||
tri.b.distanceToSquared( tri.c ) < eps;
}
// NOTE: these epsilons likely should all be the same since they're used to measure the
// distance from a point to a plane which needs to be done consistently
const EPSILON = 1e-10;
const COPLANAR_EPSILON = 1e-10;
const PARALLEL_EPSILON = 1e-10;
const _edge$2 = new Line3();
const _foundEdge = new Line3();
const _vec$1 = new Vector3();
const _triangleNormal = new Vector3();
const _planeNormal = new Vector3();
const _plane$1 = new Plane();
const _splittingTriangle = new ExtendedTriangle();
// A pool of triangles to avoid unnecessary triangle creation
class TrianglePool {
constructor() {
this._pool = [];
this._index = 0;
}
getTriangle() {
if ( this._index >= this._pool.length ) {
this._pool.push( new Triangle() );
}
return this._pool[ this._index ++ ];
}
clear() {
this._index = 0;
}
reset() {
this._pool.length = 0;
this._index = 0;
}
}
// Utility class for splitting triangles
class TriangleSplitter {
constructor() {
this.trianglePool = new TrianglePool();
this.triangles = [];
this.normal = new Vector3();
this.coplanarTriangleUsed = false;
}
// initialize the class with a triangle
initialize( tri ) {
this.reset();
const { triangles, trianglePool, normal } = this;
if ( Array.isArray( tri ) ) {
for ( let i = 0, l = tri.length; i < l; i ++ ) {
const t = tri[ i ];
if ( i === 0 ) {
t.getNormal( normal );
} else if ( Math.abs( 1.0 - t.getNormal( _vec$1 ).dot( normal ) ) > EPSILON ) {
throw new Error( 'Triangle Splitter: Cannot initialize with triangles that have different normals.' );
}
const poolTri = trianglePool.getTriangle();
poolTri.copy( t );
triangles.push( poolTri );
}
} else {
tri.getNormal( normal );
const poolTri = trianglePool.getTriangle();
poolTri.copy( tri );
triangles.push( poolTri );
}
}
// Split the current set of triangles by passing a single triangle in. If the triangle is
// coplanar it will attempt to split by the triangle edge planes
splitByTriangle( triangle ) {
const { normal, triangles } = this;
triangle.getNormal( _triangleNormal ).normalize();
if ( Math.abs( 1.0 - Math.abs( _triangleNormal.dot( normal ) ) ) < PARALLEL_EPSILON ) {
this.coplanarTriangleUsed = true;
for ( let i = 0, l = triangles.length; i < l; i ++ ) {
const t = triangles[ i ];
t.coplanarCount = 0;
}
// if the triangle is coplanar then split by the edge planes
const arr = [ triangle.a, triangle.b, triangle.c ];
for ( let i = 0; i < 3; i ++ ) {
const nexti = ( i + 1 ) % 3;
const v0 = arr[ i ];
const v1 = arr[ nexti ];
// plane positive direction is toward triangle center
_vec$1.subVectors( v1, v0 ).normalize();
_planeNormal.crossVectors( _triangleNormal, _vec$1 );
_plane$1.setFromNormalAndCoplanarPoint( _planeNormal, v0 );
this.splitByPlane( _plane$1, triangle );
}
} else {
// otherwise split by the triangle plane
triangle.getPlane( _plane$1 );
this.splitByPlane( _plane$1, triangle );
}
}
// Split the triangles by the given plan. If a triangle is provided then we ensure we
// intersect the triangle before splitting the plane
splitByPlane( plane, clippingTriangle ) {
const { triangles, trianglePool } = this;
// init our triangle to check for intersection
_splittingTriangle.copy( clippingTriangle );
_splittingTriangle.needsUpdate = true;
// try to split every triangle in the class
for ( let i = 0, l = triangles.length; i < l; i ++ ) {
const tri = triangles[ i ];
// skip the triangle if we don't intersect with it
if ( ! _splittingTriangle.intersectsTriangle( tri, _edge$2, true ) ) {
continue;
}
const { a, b, c } = tri;
let intersects = 0;
let vertexSplitEnd = - 1;
let coplanarEdge = false;
let posSideVerts = [];
let negSideVerts = [];
const arr = [ a, b, c ];
for ( let t = 0; t < 3; t ++ ) {
// get the triangle edge
const tNext = ( t + 1 ) % 3;
_edge$2.start.copy( arr[ t ] );
_edge$2.end.copy( arr[ tNext ] );
// track if the start point sits on the plane or if it's on the positive side of it
// so we can use that information to determine whether to split later.
const startDist = plane.distanceToPoint( _edge$2.start );
const endDist = plane.distanceToPoint( _edge$2.end );
if ( Math.abs( startDist ) < COPLANAR_EPSILON && Math.abs( endDist ) < COPLANAR_EPSILON ) {
coplanarEdge = true;
break;
}
if ( startDist > 0 ) {
posSideVerts.push( t );
} else {
negSideVerts.push( t );
}
// we only don't consider this an intersection if the start points hits the plane
if ( Math.abs( startDist ) < COPLANAR_EPSILON ) {
continue;
}
// double check the end point since the "intersectLine" function sometimes does not
// return it as an intersection (see issue #28)
// Because we ignore the start point intersection above we have to make sure we check the end
// point intersection here.
let didIntersect = ! ! plane.intersectLine( _edge$2, _vec$1 );
if ( ! didIntersect && Math.abs( endDist ) < COPLANAR_EPSILON ) {
_vec$1.copy( _edge$2.end );
didIntersect = true;
}
// check if we intersect the plane (ignoring the start point so we don't double count)
if ( didIntersect && ! ( _vec$1.distanceTo( _edge$2.start ) < EPSILON ) ) {
// if we intersect at the end point then we track that point as one that we
// have to split down the middle
if ( _vec$1.distanceTo( _edge$2.end ) < EPSILON ) {
vertexSplitEnd = t;
}
// track the split edge
if ( intersects === 0 ) {
_foundEdge.start.copy( _vec$1 );
} else {
_foundEdge.end.copy( _vec$1 );
}
intersects ++;
}
}
// skip splitting if:
// - we have two points on the plane then the plane intersects the triangle exactly on an edge
// - the plane does not intersect on 2 points
// - the intersection edge is too small
// - we're not along a coplanar edge
if ( ! coplanarEdge && intersects === 2 && _foundEdge.distance() > COPLANAR_EPSILON ) {
if ( vertexSplitEnd !== - 1 ) {
vertexSplitEnd = ( vertexSplitEnd + 1 ) % 3;
// we're splitting along a vertex
let otherVert1 = 0;
if ( otherVert1 === vertexSplitEnd ) {
otherVert1 = ( otherVert1 + 1 ) % 3;
}
let otherVert2 = otherVert1 + 1;
if ( otherVert2 === vertexSplitEnd ) {
otherVert2 = ( otherVert2 + 1 ) % 3;
}
const nextTri = trianglePool.getTriangle();
nextTri.a.copy( arr[ otherVert2 ] );
nextTri.b.copy( _foundEdge.end );
nextTri.c.copy( _foundEdge.start );
if ( ! isTriDegenerate( nextTri ) ) {
triangles.push( nextTri );
}
tri.a.copy( arr[ otherVert1 ] );
tri.b.copy( _foundEdge.start );
tri.c.copy( _foundEdge.end );
// finish off the adjusted triangle
if ( isTriDegenerate( tri ) ) {
triangles.splice( i, 1 );
i --;
l --;
}
} else {
// we're splitting with a quad and a triangle
// TODO: what happens when we find that about the pos and negative
// sides have only a single vertex?
const singleVert =
posSideVerts.length >= 2 ?
negSideVerts[ 0 ] :
posSideVerts[ 0 ];
// swap the direction of the intersection edge depending on which
// side of the plane the single vertex is on to align with the
// correct winding order.
if ( singleVert === 0 ) {
let tmp = _foundEdge.start;
_foundEdge.start = _foundEdge.end;
_foundEdge.end = tmp;
}
const nextVert1 = ( singleVert + 1 ) % 3;
const nextVert2 = ( singleVert + 2 ) % 3;
const nextTri1 = trianglePool.getTriangle();
const nextTri2 = trianglePool.getTriangle();
// choose the triangle that has the larger areas (shortest split distance)
if ( arr[ nextVert1 ].distanceToSquared( _foundEdge.start ) < arr[ nextVert2 ].distanceToSquared( _foundEdge.end ) ) {
nextTri1.a.copy( arr[ nextVert1 ] );
nextTri1.b.copy( _foundEdge.start );
nextTri1.c.copy( _foundEdge.end );
nextTri2.a.copy( arr[ nextVert1 ] );
nextTri2.b.copy( arr[ nextVert2 ] );
nextTri2.c.copy( _foundEdge.start );
} else {
nextTri1.a.copy( arr[ nextVert2 ] );
nextTri1.b.copy( _foundEdge.start );
nextTri1.c.copy( _foundEdge.end );
nextTri2.a.copy( arr[ nextVert1 ] );
nextTri2.b.copy( arr[ nextVert2 ] );
nextTri2.c.copy( _foundEdge.end );
}
tri.a.copy( arr[ singleVert ] );
tri.b.copy( _foundEdge.end );
tri.c.copy( _foundEdge.start );
// don't add degenerate triangles to the list
if ( ! isTriDegenerate( nextTri1 ) ) {
triangles.push( nextTri1 );
}
if ( ! isTriDegenerate( nextTri2 ) ) {
triangles.push( nextTri2 );
}
// finish off the adjusted triangle
if ( isTriDegenerate( tri ) ) {
triangles.splice( i, 1 );
i --;
l --;
}
}
} else if ( intersects === 3 ) {
console.warn( 'TriangleClipper: Coplanar clip not handled' );
}
}
}
reset() {
this.triangles.length = 0;
this.trianglePool.clear();
this.coplanarTriangleUsed = false;
}
}
function ceilToFourByteStride( byteLength ) {
byteLength = ~ ~ byteLength;
return byteLength + 4 - byteLength % 4;
}
// Make a new array wrapper class that more easily affords expansion when reaching it's max capacity
class TypeBackedArray {
constructor( type, initialSize = 500 ) {
this.expansionFactor = 1.5;
this.type = type;
this.length = 0;
this.array = null;
this.setSize( initialSize );
}
setType( type ) {
if ( this.length !== 0 ) {
throw new Error( 'TypeBackedArray: Cannot change the type while there is used data in the buffer.' );
}
const buffer = this.array.buffer;
this.array = new type( buffer );
this.type = type;
}
setSize( size ) {
if ( this.array && size === this.array.length ) {
return;
}
// ceil to the nearest 4 bytes so we can replace the array with any type using the same buffer
const type = this.type;
const bufferType = areSharedArrayBuffersSupported() ? SharedArrayBuffer : ArrayBuffer;
const newArray = new type( new bufferType( ceilToFourByteStride( size * type.BYTES_PER_ELEMENT ) ) );
if ( this.array ) {
newArray.set( this.array, 0 );
}
this.array = newArray;
}
expand() {
const { array, expansionFactor } = this;
this.setSize( array.length * expansionFactor );
}
push( ...args ) {
let { array, length } = this;
if ( length + args.length > array.length ) {
this.expand();
array = this.array;
}
for ( let i = 0, l = args.length; i < l; i ++ ) {
array[ length + i ] = args[ i ];
}
this.length += args.length;
}
clear() {
this.length = 0;
}
}
// Utility class for for tracking attribute data in type-backed arrays for a set
// of groups. The set of attributes is kept for each group and are expected to be the
// same buffer type.
class TypedAttributeData {
constructor() {
this.groupAttributes = [ {} ];
this.groupCount = 0;
}
// returns the buffer type for the given attribute
getType( name ) {
return this.groupAttributes[ 0 ][ name ].type;
}
getItemSize( name ) {
return this.groupAttributes[ 0 ][ name ].itemSize;
}
getNormalized( name ) {
return this.groupAttributes[ 0 ][ name ].normalized;
}
getCount( index ) {
if ( this.groupCount <= index ) {
return 0;
}
const pos = this.getGroupAttrArray( 'position', index );
return pos.length / pos.itemSize;
}
// returns the total length required for all groups for the given attribute
getTotalLength( name ) {
const { groupCount, groupAttributes } = this;
let length = 0;
for ( let i = 0; i < groupCount; i ++ ) {
const attrSet = groupAttributes[ i ];
length += attrSet[ name ].length;
}
return length;
}
getGroupAttrSet( index = 0 ) {
// TODO: can this be abstracted?
// Return the exiting group set if necessary
const { groupAttributes } = this;
if ( groupAttributes[ index ] ) {
this.groupCount = Math.max( this.groupCount, index + 1 );
return groupAttributes[ index ];
}
// add any new group sets required
const refAttrSet = groupAttributes[ 0 ];
this.groupCount = Math.max( this.groupCount, index + 1 );
while ( index >= groupAttributes.length ) {
const newAttrSet = {};
groupAttributes.push( newAttrSet );
for ( const key in refAttrSet ) {
const refAttr = refAttrSet[ key ];
const newAttr = new TypeBackedArray( refAttr.type );
newAttr.itemSize = refAttr.itemSize;
newAttr.normalized = refAttr.normalized;
newAttrSet[ key ] = newAttr;
}
}
return groupAttributes[ index ];
}
// Get the raw array for the group set of data
getGroupAttrArray( name, index = 0 ) {
// throw an error if we've never
const { groupAttributes } = this;
const referenceAttrSet = groupAttributes[ 0 ];
const referenceAttr = referenceAttrSet[ name ];
if ( ! referenceAttr ) {
throw new Error( `TypedAttributeData: Attribute with "${ name }" has not been initialized` );
}
return this.getGroupAttrSet( index )[ name ];
}
// initializes an attribute array with the given name, type, and size
initializeArray( name, type, itemSize, normalized ) {
const { groupAttributes } = this;
const referenceAttrSet = groupAttributes[ 0 ];
const referenceAttr = referenceAttrSet[ name ];
if ( referenceAttr ) {
if ( referenceAttr.type !== type ) {
for ( let i = 0, l = groupAttributes.length; i < l; i ++ ) {
const arr = groupAttributes[ i ][ name ];
arr.setType( type );
arr.itemSize = itemSize;
arr.normalized = normalized;
}
}
} else {
for ( let i = 0, l = groupAttributes.length; i < l; i ++ ) {
const arr = new TypeBackedArray( type );
arr.itemSize = itemSize;
arr.normalized = normalized;
groupAttributes[ i ][ name ] = arr;
}
}
}
// Clear all the data
clear() {
this.groupCount = 0;
const { groupAttributes } = this;
groupAttributes.forEach( attrSet => {
for ( const key in attrSet ) {
attrSet[ key ].clear();
}
} );
}
// Remove the given key
delete( key ) {
this.groupAttributes.forEach( attrSet => {
delete attrSet[ key ];
} );
}
// Reset the datasets completely
reset() {
this.groupAttributes = [];
this.groupCount = 0;
}
}
class IntersectionMap {
constructor() {
this.intersectionSet = {};
this.ids = [];
}
add( id, intersectionId ) {
const { intersectionSet, ids } = this;
if ( ! intersectionSet[ id ] ) {
intersectionSet[ id ] = [];
ids.push( id );
}
intersectionSet[ id ].push( intersectionId );
}
}
const ADDITION = 0;
const SUBTRACTION = 1;
const REVERSE_SUBTRACTION = 2;
const INTERSECTION = 3;
const DIFFERENCE = 4;
// guaranteed non manifold results
const HOLLOW_SUBTRACTION = 5;
const HOLLOW_INTERSECTION = 6;
const _ray$1 = new Ray();
const _matrix$2 = new Matrix4();
const _tri$2 = new Triangle();
const _vec3 = new Vector3();
const _vec4a = new Vector4();
const _vec4b = new Vector4();
const _vec4c = new Vector4();
const _vec4_0 = new Vector4();
const _vec4_1 = new Vector4();
const _vec4_2 = new Vector4();
const _edge$1 = new Line3();
const _normal$1 = new Vector3();
const JITTER_EPSILON = 1e-8;
const OFFSET_EPSILON = 1e-15;
const BACK_SIDE = - 1;
const FRONT_SIDE = 1;
const COPLANAR_OPPOSITE = - 2;
const COPLANAR_ALIGNED = 2;
const INVERT_TRI = 0;
const ADD_TRI = 1;
const SKIP_TRI = 2;
const FLOATING_COPLANAR_EPSILON = 1e-14;
let _debugContext = null;
function setDebugContext( debugData ) {
_debugContext = debugData;
}
function getHitSide( tri, bvh ) {
tri.getMidpoint( _ray$1.origin );
tri.getNormal( _ray$1.direction );
const hit = bvh.raycastFirst( _ray$1, DoubleSide );
const hitBackSide = Boolean( hit && _ray$1.direction.dot( hit.face.normal ) > 0 );
return hitBackSide ? BACK_SIDE : FRONT_SIDE;
}
function getHitSideWithCoplanarCheck( tri, bvh ) {
// random function that returns [ - 0.5, 0.5 ];
function rand() {
return Math.random() - 0.5;
}
// get the ray the check the triangle for
tri.getNormal( _normal$1 );
_ray$1.direction.copy( _normal$1 );
tri.getMidpoint( _ray$1.origin );
const total = 3;
let count = 0;
let minDistance = Infinity;
for ( let i = 0; i < total; i ++ ) {
// jitter the ray slightly
_ray$1.direction.x += rand() * JITTER_EPSILON;
_ray$1.direction.y += rand() * JITTER_EPSILON;
_ray$1.direction.z += rand() * JITTER_EPSILON;
// and invert it so we can account for floating point error by checking both directions
// to catch coplanar distances
_ray$1.direction.multiplyScalar( - 1 );
// check if the ray hit the backside
const hit = bvh.raycastFirst( _ray$1, DoubleSide );
let hitBackSide = Boolean( hit && _ray$1.direction.dot( hit.face.normal ) > 0 );
if ( hitBackSide ) {
count ++;
}
if ( hit !== null ) {
minDistance = Math.min( minDistance, hit.distance );
}
// if we're right up against another face then we're coplanar
if ( minDistance <= OFFSET_EPSILON ) {
return hit.face.normal.dot( _normal$1 ) > 0 ? COPLANAR_ALIGNED : COPLANAR_OPPOSITE;
}
// if our current casts meet our requirements then early out
if ( count / total > 0.5 || ( i - count + 1 ) / total > 0.5 ) {
break;
}
}
return count / total > 0.5 ? BACK_SIDE : FRONT_SIDE;
}
// returns the intersected triangles and returns objects mapping triangle indices to
// the other triangles intersected
function collectIntersectingTriangles( a, b ) {
const aIntersections = new IntersectionMap();
const bIntersections = new IntersectionMap();
_matrix$2
.copy( a.matrixWorld )
.invert()
.multiply( b.matrixWorld );
a.geometry.boundsTree.bvhcast( b.geometry.boundsTree, _matrix$2, {
intersectsTriangles( triangleA, triangleB, ia, ib ) {
if ( ! isTriDegenerate( triangleA ) && ! isTriDegenerate( triangleB ) ) {
// due to floating point error it's possible that we can have two overlapping, coplanar triangles
// that are a _tiny_ fraction of a value away from each other. If we find that case then check the
// distance between triangles and if it's small enough consider them intersecting.
let intersected = triangleA.intersectsTriangle( triangleB, _edge$1, true );
if ( ! intersected ) {
const pa = triangleA.plane;
const pb = triangleB.plane;
const na = pa.normal;
const nb = pb.normal;
if ( na.dot( nb ) === 1 && Math.abs( pa.constant - pb.constant ) < FLOATING_COPLANAR_EPSILON ) {
intersected = true;
}
}
if ( intersected ) {
let va = a.geometry.boundsTree.resolveTriangleIndex( ia );
let vb = b.geometry.boundsTree.resolveTriangleIndex( ib );
aIntersections.add( va, vb );
bIntersections.add( vb, va );
if ( _debugContext ) {
_debugContext.addEdge( _edge$1 );
_debugContext.addIntersectingTriangles( ia, triangleA, ib, triangleB );
}
}
}
return false;
}
} );
return { aIntersections, bIntersections };
}
// Add the barycentric interpolated values fro the triangle into the new attribute data
function appendAttributeFromTriangle(
triIndex,
baryCoordTri,
geometry,
matrixWorld,
normalMatrix,
attributeData,
invert = false,
) {
const attributes = geometry.attributes;
const indexAttr = geometry.index;
const i3 = triIndex * 3;
const i0 = indexAttr.getX( i3 + 0 );
const i1 = indexAttr.getX( i3 + 1 );
const i2 = indexAttr.getX( i3 + 2 );
for ( const key in attributeData ) {
// check if the key we're asking for is in the geometry at all
const attr = attributes[ key ];
const arr = attributeData[ key ];
if ( ! ( key in attributes ) ) {
throw new Error( `CSG Operations: Attribute ${ key } not available on geometry.` );
}
// handle normals and positions specially because they require transforming
// TODO: handle tangents
const itemSize = attr.itemSize;
if ( key === 'position' ) {
_tri$2.a.fromBufferAttribute( attr, i0 ).applyMatrix4( matrixWorld );
_tri$2.b.fromBufferAttribute( attr, i1 ).applyMatrix4( matrixWorld );
_tri$2.c.fromBufferAttribute( attr, i2 ).applyMatrix4( matrixWorld );
pushBarycoordInterpolatedValues( _tri$2.a, _tri$2.b, _tri$2.c, baryCoordTri, 3, arr, invert );
} else if ( key === 'normal' ) {
_tri$2.a.fromBufferAttribute( attr, i0 ).applyNormalMatrix( normalMatrix );
_tri$2.b.fromBufferAttribute( attr, i1 ).applyNormalMatrix( normalMatrix );
_tri$2.c.fromBufferAttribute( attr, i2 ).applyNormalMatrix( normalMatrix );
if ( invert ) {
_tri$2.a.multiplyScalar( - 1 );
_tri$2.b.multiplyScalar( - 1 );
_tri$2.c.multiplyScalar( - 1 );
}
pushBarycoordInterpolatedValues( _tri$2.a, _tri$2.b, _tri$2.c, baryCoordTri, 3, arr, invert, true );
} else {
_vec4a.fromBufferAttribute( attr, i0 );
_vec4b.fromBufferAttribute( attr, i1 );
_vec4c.fromBufferAttribute( attr, i2 );
pushBarycoordInterpolatedValues( _vec4a, _vec4b, _vec4c, baryCoordTri, itemSize, arr, invert );
}
}
}
// Append all the values of the attributes for the triangle onto the new attribute arrays
function appendAttributesFromIndices(
i0,
i1,
i2,
attributes,
matrixWorld,
normalMatrix,
attributeData,
invert = false,
) {
appendAttributeFromIndex( i0, attributes, matrixWorld, normalMatrix, attributeData, invert );
appendAttributeFromIndex( invert ? i2 : i1, attributes, matrixWorld, normalMatrix, attributeData, invert );
appendAttributeFromIndex( invert ? i1 : i2, attributes, matrixWorld, normalMatrix, attributeData, invert );
}
// Returns the triangle to add when performing an operation
function getOperationAction( operation, hitSide, invert = false ) {
switch ( operation ) {
case ADDITION:
if ( hitSide === FRONT_SIDE || ( hitSide === COPLANAR_ALIGNED && ! invert ) ) {
return ADD_TRI;
}
break;
case SUBTRACTION:
if ( invert ) {
if ( hitSide === BACK_SIDE ) {
return INVERT_TRI;
}
} else {
if ( hitSide === FRONT_SIDE || hitSide === COPLANAR_OPPOSITE ) {
return ADD_TRI;
}
}
break;
case REVERSE_SUBTRACTION:
if ( invert ) {
if ( hitSide === FRONT_SIDE || hitSide === COPLANAR_OPPOSITE ) {
return ADD_TRI;
}
} else {
if ( hitSide === BACK_SIDE ) {
return INVERT_TRI;
}
}
break;
case DIFFERENCE:
if ( hitSide === BACK_SIDE ) {
return INVERT_TRI;
} else if ( hitSide === FRONT_SIDE ) {
return ADD_TRI;
}
break;
case INTERSECTION:
if ( hitSide === BACK_SIDE || ( hitSide === COPLANAR_ALIGNED && ! invert ) ) {
return ADD_TRI;
}
break;
case HOLLOW_SUBTRACTION:
if ( ! invert && ( hitSide === FRONT_SIDE || hitSide === COPLANAR_OPPOSITE ) ) {
return ADD_TRI;
}
break;
case HOLLOW_INTERSECTION:
if ( ! invert && ( hitSide === BACK_SIDE || hitSide === COPLANAR_ALIGNED ) ) {
return ADD_TRI;
}
break;
default:
throw new Error( `Unrecognized CSG operation enum "${ operation }".` );
}
return SKIP_TRI;
}
// takes a set of barycentric values in the form of a triangle, a set of vectors, number of components,
// and whether to invert the result and pushes the new values onto the provided attribute array
function pushBarycoordInterpolatedValues( v0, v1, v2, baryCoordTri, itemSize, attrArr, invert = false, normalize = false ) {
// adds the appropriate number of values for the vector onto the array
const addValues = v => {
attrArr.push( v.x );
if ( itemSize > 1 ) attrArr.push( v.y );
if ( itemSize > 2 ) attrArr.push( v.z );
if ( itemSize > 3 ) attrArr.push( v.w );
};
// barycentric interpolate the first component
_vec4_0.set( 0, 0, 0, 0 )
.addScaledVector( v0, baryCoordTri.a.x )
.addScaledVector( v1, baryCoordTri.a.y )
.addScaledVector( v2, baryCoordTri.a.z );
_vec4_1.set( 0, 0, 0, 0 )
.addScaledVector( v0, baryCoordTri.b.x )
.addScaledVector( v1, baryCoordTri.b.y )
.addScaledVector( v2, baryCoordTri.b.z );
_vec4_2.set( 0, 0, 0, 0 )
.addScaledVector( v0, baryCoordTri.c.x )
.addScaledVector( v1, baryCoordTri.c.y )
.addScaledVector( v2, baryCoordTri.c.z );
if ( normalize ) {
_vec4_0.normalize();
_vec4_1.normalize();
_vec4_2.normalize();
}
// if the face is inverted then add the values in an inverted order
addValues( _vec4_0 );
if ( invert ) {
addValues( _vec4_2 );
addValues( _vec4_1 );
} else {
addValues( _vec4_1 );
addValues( _vec4_2 );
}
}
// Adds the values for the given vertex index onto the new attribute arrays
function appendAttributeFromIndex(
index,
attributes,
matrixWorld,
normalMatrix,
attributeData,
invert = false,
) {
for ( const key in attributeData ) {
// check if the key we're asking for is in the geometry at all
const attr = attributes[ key ];
const arr = attributeData[ key ];
if ( ! ( key in attributes ) ) {
throw new Error( `CSG Operations: Attribute ${ key } no available on geometry.` );
}
// specially handle the position and normal attributes because they require transforms
// TODO: handle tangents
const itemSize = attr.itemSize;
if ( key === 'position' ) {
_vec3.fromBufferAttribute( attr, index ).applyMatrix4( matrixWorld );
arr.push( _vec3.x, _vec3.y, _vec3.z );
} else if ( key === 'normal' ) {
_vec3.fromBufferAttribute( attr, index ).applyNormalMatrix( normalMatrix );
if ( invert ) {
_vec3.multiplyScalar( - 1 );
}
arr.push( _vec3.x, _vec3.y, _vec3.z );
} else {
arr.push( attr.getX( index ) );
if ( itemSize > 1 ) arr.push( attr.getY( index ) );
if ( itemSize > 2 ) arr.push( attr.getZ( index ) );
if ( itemSize > 3 ) arr.push( attr.getW( index ) );
}
}
}
class TriangleIntersectData {
constructor( tri ) {
this.triangle = new Triangle().copy( tri );
this.intersects = {};
}
addTriangle( index, tri ) {
this.intersects[ index ] = new Triangle().copy( tri );
}
getIntersectArray() {
const array = [];
const { intersects } = this;
for ( const key in intersects ) {
array.push( intersects[ key ] );
}
return array;
}
}
class TriangleIntersectionSets {
constructor() {
this.data = {};
}
addTriangleIntersection( ia, triA, ib, triB ) {
const { data } = this;
if ( ! data[ ia ] ) {
data[ ia ] = new TriangleIntersectData( triA );
}
data[ ia ].addTriangle( ib, triB );
}
getTrianglesAsArray( id = null ) {
const { data } = this;
const arr = [];
if ( id !== null ) {
if ( id in data ) {
arr.push( data[ id ].triangle );
}
} else {
for ( const key in data ) {
arr.push( data[ key ].triangle );
}
}
return arr;
}
getTriangleIndices() {
return Object.keys( this.data ).map( i => parseInt( i ) );
}
getIntersectionIndices( id ) {
const { data } = this;
if ( ! data[ id ] ) {
return [];
} else {
return Object.keys( data[ id ].intersects ).map( i => parseInt( i ) );
}
}
getIntersectionsAsArray( id = null, id2 = null ) {
const { data } = this;
const triSet = new Set();
const arr = [];
const addTriangles = key => {
if ( ! data[ key ] ) return;
if ( id2 !== null ) {
if ( data[ key ].intersects[ id2 ] ) {
arr.push( data[ key ].intersects[ id2 ] );
}
} else {
const intersects = data[ key ].intersects;
for ( const key2 in intersects ) {
if ( ! triSet.has( key2 ) ) {
triSet.add( key2 );
arr.push( intersects[ key2 ] );
}
}
}
};
if ( id !== null ) {
addTriangles( id );
} else {
for ( const key in data ) {
addTriangles( key );
}
}
return arr;
}
reset() {
this.data = {};
}
}
class OperationDebugData {
constructor() {
this.enabled = false;
this.triangleIntersectsA = new TriangleIntersectionSets();
this.triangleIntersectsB = new TriangleIntersectionSets();
this.intersectionEdges = [];
}
addIntersectingTriangles( ia, triA, ib, triB ) {
const { triangleIntersectsA, triangleIntersectsB } = this;
triangleIntersectsA.addTriangleIntersection( ia, triA, ib, triB );
triangleIntersectsB.addTriangleIntersection( ib, triB, ia, triA );
}
addEdge( edge ) {
this.intersectionEdges.push( edge.clone() );
}
reset() {
this.triangleIntersectsA.reset();
this.triangleIntersectsB.reset();
this.intersectionEdges = [];
}
init() {
if ( this.enabled ) {
this.reset();
setDebugContext( this );
}
}
complete() {
if ( this.enabled ) {
setDebugContext( null );
}
}
}
const _matrix$1 = new Matrix4();
const _normalMatrix = new Matrix3();
const _triA = new Triangle();
const _triB = new Triangle();
const _tri$1 = new Triangle();
const _barycoordTri = new Triangle();
const _attr = [];
const _actions = [];
function getFirstIdFromSet( set ) {
for ( const id of set ) return id;
}
// runs the given operation against a and b using the splitter and appending data to the
// attributeData object.
function performOperation(
a,
b,
operations,
splitter,
attributeData,
options = {},
) {
const { useGroups = true } = options;
const { aIntersections, bIntersections } = collectIntersectingTriangles( a, b );
const resultGroups = [];
let resultMaterials = null;
let groupOffset;
groupOffset = useGroups ? 0 : - 1;
performSplitTriangleOperations( a, b, aIntersections, operations, false, splitter, attributeData, groupOffset );
performWholeTriangleOperations( a, b, aIntersections, operations, false, attributeData, groupOffset );
// find whether the set of operations contains a non-hollow operations. If it does then we need
// to perform the second set of triangle additions
const nonHollow = operations
.findIndex( op => op !== HOLLOW_INTERSECTION && op !== HOLLOW_SUBTRACTION ) !== - 1;
if ( nonHollow ) {
groupOffset = useGroups ? a.geometry.groups.length || 1 : - 1;
performSplitTriangleOperations( b, a, bIntersections, operations, true, splitter, attributeData, groupOffset );
performWholeTriangleOperations( b, a, bIntersections, operations, true, attributeData, groupOffset );
}
_attr.length = 0;
_actions.length = 0;
return {
groups: resultGroups,
materials: resultMaterials
};
}
// perform triangle splitting and CSG operations on the set of split triangles
function performSplitTriangleOperations(
a,
b,
intersectionMap,
operations,
invert,
splitter,
attributeData,
groupOffset = 0,
) {
const invertedGeometry = a.matrixWorld.determinant() < 0;
// transforms into the local frame of matrix b
_matrix$1
.copy( b.matrixWorld )
.invert()
.multiply( a.matrixWorld );
_normalMatrix
.getNormalMatrix( a.matrixWorld )
.multiplyScalar( invertedGeometry ? - 1 : 1 );
const groupIndices = a.geometry.groupIndices;
const aIndex = a.geometry.index;
const aPosition = a.geometry.attributes.position;
const bBVH = b.geometry.boundsTree;
const bIndex = b.geometry.index;
const bPosition = b.geometry.attributes.position;
const splitIds = intersectionMap.ids;
const intersectionSet = intersectionMap.intersectionSet;
// iterate over all split triangle indices
for ( let i = 0, l = splitIds.length; i < l; i ++ ) {
const ia = splitIds[ i ];
const groupIndex = groupOffset === - 1 ? 0 : groupIndices[ ia ] + groupOffset;
// get the triangle in the geometry B local frame
const ia3 = 3 * ia;
const ia0 = aIndex.getX( ia3 + 0 );
const ia1 = aIndex.getX( ia3 + 1 );
const ia2 = aIndex.getX( ia3 + 2 );
_triA.a.fromBufferAttribute( aPosition, ia0 ).applyMatrix4( _matrix$1 );
_triA.b.fromBufferAttribute( aPosition, ia1 ).applyMatrix4( _matrix$1 );
_triA.c.fromBufferAttribute( aPosition, ia2 ).applyMatrix4( _matrix$1 );
// initialize the splitter with the triangle from geometry A
splitter.reset();
splitter.initialize( _triA );
// split the triangle with the intersecting triangles from B
const intersectingIndices = intersectionSet[ ia ];
for ( let ib = 0, l = intersectingIndices.length; ib < l; ib ++ ) {
const ib3 = 3 * intersectingIndices[ ib ];
const ib0 = bIndex.getX( ib3 + 0 );
const ib1 = bIndex.getX( ib3 + 1 );
const ib2 = bIndex.getX( ib3 + 2 );
_tri