UNPKG

three-bvh-csg

Version:

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

1 lines 144 kB
{"version":3,"file":"index.umd.cjs","sources":["../src/utils/hashUtils.js","../src/core/HalfEdgeMap.js","../src/core/utils.js","../src/core/Brush.js","../src/core/IntersectionMap.js","../src/core/constants.js","../src/core/operationsUtils.js","../src/core/TriangleSplitter.js","../src/core/TypeBackedArray.js","../src/core/TypedAttributeData.js","../src/core/OperationDebugData.js","../src/core/operations.js","../src/core/Evaluator.js","../src/core/Operation.js","../src/core/OperationGroup.js","../src/materials/shaderUtils.js","../src/materials/GridMaterial.js","../src/core/debugUtils.js","../src/objects/TriangleSetHelper.js","../src/objects/EdgesHelper.js","../src/objects/PointsHelper.js","../src/objects/HalfEdgeHelper.js","../src/utils/computeMeshVolume.js"],"sourcesContent":["const HASH_MULTIPLIER = ( 1 + 1e-7 ) * 1e6;\n\nexport function hashNumber( v ) {\n\n\treturn ~ ~ ( v * HASH_MULTIPLIER );\n\n}\n\nexport function hashVertex( v ) {\n\n\treturn `${ hashNumber( v.x ) },${ hashNumber( v.y ) },${ hashNumber( v.z ) }`;\n\n}\n","import { Vector3 } from 'three';\nimport { hashVertex } from '../utils/hashUtils.js';\n\nconst _vertices = [ new Vector3(), new Vector3(), new Vector3() ];\n\nexport class HalfEdgeMap {\n\n\tconstructor( geometry = null ) {\n\n\t\tthis.data = null;\n\t\tthis.unmatchedEdges = null;\n\t\tthis.matchedEdges = null;\n\t\tthis.useDrawRange = true;\n\n\t\tif ( geometry ) {\n\n\t\t\tthis.updateFrom( geometry );\n\n\t\t}\n\n\t}\n\n\tgetSiblingTriangleIndex( triIndex, edgeIndex ) {\n\n\t\tconst otherIndex = this.data[ triIndex * 3 + edgeIndex ];\n\t\treturn otherIndex === - 1 ? - 1 : ~ ~ ( otherIndex / 3 );\n\n\t}\n\n\tgetSiblingEdgeIndex( triIndex, edgeIndex ) {\n\n\t\tconst otherIndex = this.data[ triIndex * 3 + edgeIndex ];\n\t\treturn otherIndex === - 1 ? - 1 : ( otherIndex % 3 );\n\n\t}\n\n\tupdateFrom( geometry ) {\n\n\t\t// runs on the assumption that there is a 1 : 1 match of edges\n\t\tconst map = new Map();\n\n\t\t// attributes\n\t\tconst { attributes } = geometry;\n\t\tconst indexAttr = geometry.index;\n\t\tconst posAttr = attributes.position;\n\n\t\t// get the potential number of triangles\n\t\tlet triCount = indexAttr ? indexAttr.count / 3 : posAttr.count / 3;\n\t\tconst maxTriCount = triCount;\n\n\t\t// get the real number of triangles from the based on the draw range\n\t\tlet offset = 0;\n\t\tif ( this.useDrawRange ) {\n\n\t\t\toffset = geometry.drawRange.start;\n\t\t\tif ( geometry.drawRange.count !== Infinity ) {\n\n\t\t\t\ttriCount = ~ ~ ( geometry.drawRange.count / 3 );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// initialize the connectivity buffer - 1 means no connectivity\n\t\tlet data = this.data;\n\t\tif ( ! data || data.length < 3 * maxTriCount ) {\n\n\t\t\tdata = new Int32Array( 3 * maxTriCount );\n\n\t\t}\n\n\t\tdata.fill( - 1 );\n\n\t\t// iterate over all triangles\n\t\tlet unmatchedEdges = 0;\n\t\tlet matchedEdges = 0;\n\t\tfor ( let i = 0; i < triCount; i ++ ) {\n\n\t\t\tconst i3 = 3 * i + offset;\n\t\t\tfor ( let e = 0; e < 3; e ++ ) {\n\n\t\t\t\tlet i0 = i3 + e;\n\t\t\t\tif ( indexAttr ) {\n\n\t\t\t\t\ti0 = indexAttr.getX( i0 );\n\n\t\t\t\t}\n\n\t\t\t\t_vertices[ e ].fromBufferAttribute( posAttr, i0 );\n\n\t\t\t}\n\n\t\t\tfor ( let e = 0; e < 3; e ++ ) {\n\n\t\t\t\tconst nextE = ( e + 1 ) % 3;\n\t\t\t\tconst _vec0 = _vertices[ e ];\n\t\t\t\tconst _vec1 = _vertices[ nextE ];\n\n\t\t\t\tconst vh0 = hashVertex( _vec0 );\n\t\t\t\tconst vh1 = hashVertex( _vec1 );\n\n\t\t\t\tconst reverseHash = `${ vh1 }_${ vh0 }`;\n\t\t\t\tif ( map.has( reverseHash ) ) {\n\n\t\t\t\t\t// create a reference between the two triangles and clear the hash\n\t\t\t\t\tconst otherIndex = map.get( reverseHash );\n\t\t\t\t\tdata[ i3 + e ] = otherIndex;\n\t\t\t\t\tdata[ otherIndex ] = i3 + e;\n\t\t\t\t\tmap.delete( reverseHash );\n\t\t\t\t\tunmatchedEdges --;\n\t\t\t\t\tmatchedEdges ++;\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// save the triangle and triangle edge index captured in one value\n\t\t\t\t\t// triIndex = ~ ~ ( i0 / 3 );\n\t\t\t\t\t// edgeIndex = i0 % 3;\n\t\t\t\t\tconst hash = `${ vh0 }_${ vh1 }`;\n\t\t\t\t\tmap.set( hash, i3 + e );\n\t\t\t\t\tunmatchedEdges ++;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tthis.matchedEdges = matchedEdges;\n\t\tthis.unmatchedEdges = unmatchedEdges;\n\t\tthis.data = data;\n\n\t}\n\n}\n","export function areSharedArrayBuffersSupported() {\n\n\treturn typeof SharedArrayBuffer !== 'undefined';\n\n}\n\nexport function convertToSharedArrayBuffer( array ) {\n\n\tif ( array.buffer instanceof SharedArrayBuffer ) {\n\n\t\treturn array;\n\n\t}\n\n\tconst cons = array.constructor;\n\tconst buffer = array.buffer;\n\tconst sharedBuffer = new SharedArrayBuffer( buffer.byteLength );\n\n\tconst uintArray = new Uint8Array( buffer );\n\tconst sharedUintArray = new Uint8Array( sharedBuffer );\n\tsharedUintArray.set( uintArray, 0 );\n\n\treturn new cons( sharedBuffer );\n\n}\n","import { Mesh, Matrix4 } from 'three';\nimport { MeshBVH } from 'three-mesh-bvh';\nimport { HalfEdgeMap } from './HalfEdgeMap.js';\nimport { areSharedArrayBuffersSupported, convertToSharedArrayBuffer } from './utils.js';\n\nexport class Brush extends Mesh {\n\n\tconstructor( ...args ) {\n\n\t\tsuper( ...args );\n\n\t\tthis.isBrush = true;\n\t\tthis._previousMatrix = new Matrix4();\n\t\tthis._previousMatrix.elements.fill( 0 );\n\n\t}\n\n\tmarkUpdated() {\n\n\t\tthis._previousMatrix.copy( this.matrix );\n\n\t}\n\n\tisDirty() {\n\n\t\tconst { matrix, _previousMatrix } = this;\n\t\tconst el1 = matrix.elements;\n\t\tconst el2 = _previousMatrix.elements;\n\t\tfor ( let i = 0; i < 16; i ++ ) {\n\n\t\t\tif ( el1[ i ] !== el2[ i ] ) {\n\n\t\t\t\treturn true;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn false;\n\n\t}\n\n\tprepareGeometry() {\n\n\t\t// generate shared array buffers\n\t\tconst geometry = this.geometry;\n\t\tconst attributes = geometry.attributes;\n\t\tif ( areSharedArrayBuffersSupported() ) {\n\n\t\t\tfor ( const key in attributes ) {\n\n\t\t\t\tconst attribute = attributes[ key ];\n\t\t\t\tif ( attribute.isInterleavedBufferAttribute ) {\n\n\t\t\t\t\tthrow new Error( 'Brush: InterleavedBufferAttributes are not supported.' );\n\n\t\t\t\t}\n\n\t\t\t\tattribute.array = convertToSharedArrayBuffer( attribute.array );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// generate bounds tree\n\t\tif ( ! geometry.boundsTree ) {\n\n\t\t\tgeometry.boundsTree = new MeshBVH( geometry, { maxLeafTris: 3 } );\n\t\t\tif ( geometry.halfEdges ) {\n\n\t\t\t\tgeometry.halfEdges.updateFrom( geometry );\n\n\t\t\t}\n\n\t\t}\n\n\t\t// generate half edges\n\t\tif ( ! geometry.halfEdges ) {\n\n\t\t\tgeometry.halfEdges = new HalfEdgeMap( geometry );\n\n\t\t}\n\n\t\t// save group indices for materials\n\t\tif ( ! geometry.groupIndices ) {\n\n\t\t\tconst triCount = geometry.index.count / 3;\n\t\t\tconst array = new Uint16Array( triCount );\n\t\t\tconst groups = geometry.groups;\n\t\t\tfor ( let i = 0, l = groups.length; i < l; i ++ ) {\n\n\t\t\t\tconst { start, count } = groups[ i ];\n\t\t\t\tfor ( let g = start / 3, lg = ( start + count ) / 3; g < lg; g ++ ) {\n\n\t\t\t\t\tarray[ g ] = i;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tgeometry.groupIndices = array;\n\n\t\t}\n\n\t}\n\n\tdisposeCacheData() {\n\n\t\tconst { geometry } = this;\n\t\tgeometry.halfEdges = null;\n\t\tgeometry.boundsTree = null;\n\t\tgeometry.groupIndices = null;\n\n\t}\n\n}\n","export class IntersectionMap {\n\n\tconstructor() {\n\n\t\tthis.intersectionSet = {};\n\t\tthis.ids = [];\n\n\t}\n\n\tadd( id, intersectionId ) {\n\n\t\tconst { intersectionSet, ids } = this;\n\t\tif ( ! intersectionSet[ id ] ) {\n\n\t\t\tintersectionSet[ id ] = [];\n\t\t\tids.push( id );\n\n\t\t}\n\n\t\tintersectionSet[ id ].push( intersectionId );\n\n\t}\n\n}\n","export const ADDITION = 0;\nexport const SUBTRACTION = 1;\nexport const DIFFERENCE = 3;\nexport const INTERSECTION = 4;\n","import { Ray, Matrix4, DoubleSide, Vector3, Vector4, Triangle, Line3 } from 'three';\nimport { IntersectionMap } from './IntersectionMap.js';\nimport { ADDITION, SUBTRACTION, INTERSECTION, DIFFERENCE } from './constants.js';\n\nconst _ray = new Ray();\nconst _matrix = new Matrix4();\nconst _tri = new Triangle();\nconst _vec3 = new Vector3();\nconst _vec4a = new Vector4();\nconst _vec4b = new Vector4();\nconst _vec4c = new Vector4();\nconst _vec4_0 = new Vector4();\nconst _vec4_1 = new Vector4();\nconst _vec4_2 = new Vector4();\nconst _edge = new Line3();\nconst _normal = new Vector3();\nconst JITTER_EPSILON = 1e-8;\nconst OFFSET_EPSILON = 1e-15;\n\nexport const BACK_SIDE = - 1;\nexport const FRONT_SIDE = 1;\nexport const COPLANAR_OPPOSITE = - 2;\nexport const COPLANAR_ALIGNED = 2;\n\nexport const INVERT_TRI = 0;\nexport const ADD_TRI = 1;\nexport const SKIP_TRI = 2;\n\nlet _debugContext = null;\nexport function setDebugContext( debugData ) {\n\n\t_debugContext = debugData;\n\n}\n\nexport function getHitSide( tri, bvh ) {\n\n\t// random function that returns [ - 0.5, 0.5 ];\n\tfunction rand() {\n\n\t\treturn Math.random() - 0.5;\n\n\t}\n\n\t// get the ray the check the triangle for\n\ttri.getNormal( _normal );\n\t_ray.direction.copy( _normal );\n\ttri.getMidpoint( _ray.origin );\n\n\tconst total = 3;\n\tlet count = 0;\n\tlet minDistance = Infinity;\n\tfor ( let i = 0; i < total; i ++ ) {\n\n\t\t// jitter the ray slightly\n\t\t_ray.direction.x += rand() * JITTER_EPSILON;\n\t\t_ray.direction.y += rand() * JITTER_EPSILON;\n\t\t_ray.direction.z += rand() * JITTER_EPSILON;\n\n\t\t// and invert it so we can account for floating point error by checking both directions\n\t\t// to catch coplanar distances\n\t\t_ray.direction.multiplyScalar( - 1 );\n\n\t\t// check if the ray hit the backside\n\t\tconst hit = bvh.raycastFirst( _ray, DoubleSide );\n\t\tlet hitBackSide = Boolean( hit && _ray.direction.dot( hit.face.normal ) > 0 );\n\t\tif ( hitBackSide ) {\n\n\t\t\tcount ++;\n\n\t\t}\n\n\t\tif ( hit !== null ) {\n\n\t\t\tminDistance = Math.min( minDistance, hit.distance );\n\n\t\t}\n\n\t\t// if we're right up against another face then we're coplanar\n\t\tif ( minDistance <= OFFSET_EPSILON ) {\n\n\t\t\treturn hit.face.normal.dot( _normal ) > 0 ? COPLANAR_ALIGNED : COPLANAR_OPPOSITE;\n\n\t\t}\n\n\t\t// if our current casts meet our requirements then early out\n\t\tif ( count / total > 0.5 || ( i - count + 1 ) / total > 0.5 ) {\n\n\t\t\tbreak;\n\n\t\t}\n\n\t}\n\n\treturn count / total > 0.5 ? BACK_SIDE : FRONT_SIDE;\n\n}\n\n// returns the intersected triangles and returns objects mapping triangle indices to\n// the other triangles intersected\nexport function collectIntersectingTriangles( a, b ) {\n\n\tconst aIntersections = new IntersectionMap();\n\tconst bIntersections = new IntersectionMap();\n\n\t_matrix\n\t\t.copy( a.matrixWorld )\n\t\t.invert()\n\t\t.multiply( b.matrixWorld );\n\n\ta.geometry.boundsTree.bvhcast( b.geometry.boundsTree, _matrix, {\n\n\t\tintersectsTriangles( triangleA, triangleB, ia, ib ) {\n\n\t\t\tif ( triangleA.intersectsTriangle( triangleB, _edge, true ) ) {\n\n\t\t\t\t// if the edge distance is zero (and not from being coplanar) then exit early and don't include the\n\t\t\t\t// triangle in the set of intersecting triangles\n\t\t\t\tif ( _edge.distanceSq() === 0 && triangleA.plane.normal.dot( triangleB.plane.normal ) < 1.0 - 1e-10 ) {\n\n\t\t\t\t\treturn false;\n\n\t\t\t\t}\n\n\t\t\t\taIntersections.add( ia, ib );\n\t\t\t\tbIntersections.add( ib, ia );\n\n\t\t\t\tif ( _debugContext ) {\n\n\t\t\t\t\t_debugContext.addEdge( _edge );\n\t\t\t\t\t_debugContext.addIntersectingTriangles( ia, triangleA, ib, triangleB );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t}\n\n\t} );\n\n\treturn { aIntersections, bIntersections };\n\n}\n\n// Add the barycentric interpolated values fro the triangle into the new attribute data\nexport function appendAttributeFromTriangle(\n\ttriIndex,\n\tbaryCoordTri,\n\tgeometry,\n\tmatrixWorld,\n\tnormalMatrix,\n\tattributeInfo,\n\tinvert = false,\n) {\n\n\tconst attributes = geometry.attributes;\n\tconst indexAttr = geometry.index;\n\tconst i3 = triIndex * 3;\n\tconst i0 = indexAttr.getX( i3 + 0 );\n\tconst i1 = indexAttr.getX( i3 + 1 );\n\tconst i2 = indexAttr.getX( i3 + 2 );\n\n\tfor ( const key in attributeInfo ) {\n\n\t\t// check if the key we're asking for is in the geometry at all\n\t\tconst attr = attributes[ key ];\n\t\tconst arr = attributeInfo[ key ];\n\t\tif ( ! ( key in attributes ) ) {\n\n\t\t\tthrow new Error( `CSG Operations: Attribute ${ key } not available on geometry.` );\n\n\t\t}\n\n\t\t// handle normals and positions specially because they require transforming\n\t\t// TODO: handle tangents\n\t\tconst itemSize = attr.itemSize;\n\t\tif ( key === 'position' ) {\n\n\t\t\t_tri.a.fromBufferAttribute( attr, i0 ).applyMatrix4( matrixWorld );\n\t\t\t_tri.b.fromBufferAttribute( attr, i1 ).applyMatrix4( matrixWorld );\n\t\t\t_tri.c.fromBufferAttribute( attr, i2 ).applyMatrix4( matrixWorld );\n\n\t\t\tpushBarycoordInterpolatedValues( _tri.a, _tri.b, _tri.c, baryCoordTri, 3, arr, invert );\n\n\t\t} else if ( key === 'normal' ) {\n\n\t\t\t_tri.a.fromBufferAttribute( attr, i0 ).applyNormalMatrix( normalMatrix );\n\t\t\t_tri.b.fromBufferAttribute( attr, i1 ).applyNormalMatrix( normalMatrix );\n\t\t\t_tri.c.fromBufferAttribute( attr, i2 ).applyNormalMatrix( normalMatrix );\n\n\t\t\tif ( invert ) {\n\n\t\t\t\t_tri.a.multiplyScalar( - 1 );\n\t\t\t\t_tri.b.multiplyScalar( - 1 );\n\t\t\t\t_tri.c.multiplyScalar( - 1 );\n\n\t\t\t}\n\n\t\t\tpushBarycoordInterpolatedValues( _tri.a, _tri.b, _tri.c, baryCoordTri, 3, arr, invert, true );\n\n\t\t} else {\n\n\t\t\t_vec4a.fromBufferAttribute( attr, i0 );\n\t\t\t_vec4b.fromBufferAttribute( attr, i1 );\n\t\t\t_vec4c.fromBufferAttribute( attr, i2 );\n\n\t\t\tpushBarycoordInterpolatedValues( _vec4a, _vec4b, _vec4c, baryCoordTri, itemSize, arr, invert );\n\n\t\t}\n\n\t}\n\n}\n\n// Append all the values of the attributes for the triangle onto the new attribute arrays\nexport function appendAttributesFromIndices(\n\ti0,\n\ti1,\n\ti2,\n\tattributes,\n\tmatrixWorld,\n\tnormalMatrix,\n\tattributeInfo,\n\tinvert = false,\n) {\n\n\tappendAttributeFromIndex( i0, attributes, matrixWorld, normalMatrix, attributeInfo, invert );\n\tappendAttributeFromIndex( invert ? i2 : i1, attributes, matrixWorld, normalMatrix, attributeInfo, invert );\n\tappendAttributeFromIndex( invert ? i1 : i2, attributes, matrixWorld, normalMatrix, attributeInfo, invert );\n\n}\n\n// Returns the triangle to add when performing an operation\nexport function getOperationAction( operation, hitSide, invert = false ) {\n\n\tswitch ( operation ) {\n\n\t\tcase ADDITION:\n\n\t\t\tif ( hitSide === FRONT_SIDE || ( hitSide === COPLANAR_ALIGNED && ! invert ) ) {\n\n\t\t\t\treturn ADD_TRI;\n\n\t\t\t}\n\n\t\t\tbreak;\n\t\tcase SUBTRACTION:\n\n\t\t\tif ( invert ) {\n\n\t\t\t\tif ( hitSide === BACK_SIDE ) {\n\n\t\t\t\t\treturn INVERT_TRI;\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tif ( hitSide === FRONT_SIDE || hitSide === COPLANAR_OPPOSITE ) {\n\n\t\t\t\t\treturn ADD_TRI;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tbreak;\n\t\tcase DIFFERENCE:\n\n\t\t\tif ( hitSide === BACK_SIDE ) {\n\n\t\t\t\treturn INVERT_TRI;\n\n\t\t\t} else if ( hitSide === FRONT_SIDE ) {\n\n\t\t\t\treturn ADD_TRI;\n\n\t\t\t}\n\n\t\t\tbreak;\n\t\tcase INTERSECTION:\n\t\t\tif ( hitSide === BACK_SIDE || ( hitSide === COPLANAR_ALIGNED && ! invert ) ) {\n\n\t\t\t\treturn ADD_TRI;\n\n\t\t\t}\n\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new Error( `Unrecognized CSG operation enum \"${ operation }\".` );\n\n\t}\n\n\treturn SKIP_TRI;\n\n}\n\n// takes a set of barycentric values in the form of a triangle, a set of vectors, number of components,\n// and whether to invert the result and pushes the new values onto the provided attribute array\nfunction pushBarycoordInterpolatedValues( v0, v1, v2, baryCoordTri, itemSize, attrArr, invert = false, normalize = false ) {\n\n\t// adds the appropriate number of values for the vector onto the array\n\tconst addValues = v => {\n\n\t\tattrArr.push( v.x );\n\t\tif ( itemSize > 1 ) attrArr.push( v.y );\n\t\tif ( itemSize > 2 ) attrArr.push( v.z );\n\t\tif ( itemSize > 3 ) attrArr.push( v.w );\n\n\t};\n\n\t// barycentric interpolate the first component\n\t_vec4_0.set( 0, 0, 0, 0 )\n\t\t.addScaledVector( v0, baryCoordTri.a.x )\n\t\t.addScaledVector( v1, baryCoordTri.a.y )\n\t\t.addScaledVector( v2, baryCoordTri.a.z );\n\n\t_vec4_1.set( 0, 0, 0, 0 )\n\t\t.addScaledVector( v0, baryCoordTri.b.x )\n\t\t.addScaledVector( v1, baryCoordTri.b.y )\n\t\t.addScaledVector( v2, baryCoordTri.b.z );\n\n\t_vec4_2.set( 0, 0, 0, 0 )\n\t\t.addScaledVector( v0, baryCoordTri.c.x )\n\t\t.addScaledVector( v1, baryCoordTri.c.y )\n\t\t.addScaledVector( v2, baryCoordTri.c.z );\n\n\tif ( normalize ) {\n\n\t\t_vec4_0.normalize();\n\t\t_vec4_1.normalize();\n\t\t_vec4_2.normalize();\n\n\t}\n\n\t// if the face is inverted then add the values in an inverted order\n\taddValues( _vec4_0 );\n\n\tif ( invert ) {\n\n\t\taddValues( _vec4_2 );\n\t\taddValues( _vec4_1 );\n\n\t} else {\n\n\t\taddValues( _vec4_1 );\n\t\taddValues( _vec4_2 );\n\n\t}\n\n}\n\n// Adds the values for the given vertex index onto the new attribute arrays\nfunction appendAttributeFromIndex(\n\tindex,\n\tattributes,\n\tmatrixWorld,\n\tnormalMatrix,\n\tattributeInfo,\n\tinvert = false,\n) {\n\n\tfor ( const key in attributeInfo ) {\n\n\t\t// check if the key we're asking for is in the geometry at all\n\t\tconst attr = attributes[ key ];\n\t\tconst arr = attributeInfo[ key ];\n\t\tif ( ! ( key in attributes ) ) {\n\n\t\t\tthrow new Error( `CSG Operations: Attribute ${ key } no available on geometry.` );\n\n\t\t}\n\n\t\t// specially handle the position and normal attributes because they require transforms\n\t\t// TODO: handle tangents\n\t\tconst itemSize = attr.itemSize;\n\t\tif ( key === 'position' ) {\n\n\t\t\t_vec3.fromBufferAttribute( attr, index ).applyMatrix4( matrixWorld );\n\t\t\tarr.push( _vec3.x, _vec3.y, _vec3.z );\n\n\t\t} else if ( key === 'normal' ) {\n\n\t\t\t_vec3.fromBufferAttribute( attr, index ).applyNormalMatrix( normalMatrix );\n\t\t\tif ( invert ) {\n\n\t\t\t\t_vec3.multiplyScalar( - 1 );\n\n\t\t\t}\n\n\t\t\tarr.push( _vec3.x, _vec3.y, _vec3.z );\n\n\t\t} else {\n\n\t\t\tarr.push( attr.getX( index ) );\n\t\t\tif ( itemSize > 1 ) arr.push( attr.getY( index ) );\n\t\t\tif ( itemSize > 2 ) arr.push( attr.getZ( index ) );\n\t\t\tif ( itemSize > 3 ) arr.push( attr.getW( index ) );\n\n\t\t}\n\n\t}\n\n}\n","import { Triangle, Line3, Vector3, Plane } from 'three';\nimport { ExtendedTriangle } from 'three-mesh-bvh';\nimport { BACK_SIDE, FRONT_SIDE } from './operationsUtils.js';\n\nconst EPSILON = 1e-14;\nconst COPLANAR_EPSILON = 1e-10;\nconst _edge = new Line3();\nconst _foundEdge = new Line3();\nconst _vec = new Vector3();\nconst _planeNormal = new Vector3();\nconst _plane = new Plane();\nconst _exTriangle = new ExtendedTriangle();\n\nexport function isTriDegenerate( tri ) {\n\n\treturn tri.a.distanceToSquared( tri.b ) < EPSILON ||\n\t\ttri.a.distanceToSquared( tri.c ) < EPSILON ||\n\t\ttri.b.distanceToSquared( tri.c ) < EPSILON;\n\n}\n\n// A pool of triangles to avoid unnecessary triangle creation\nclass TrianglePool {\n\n\tconstructor() {\n\n\t\tthis._pool = [];\n\t\tthis._index = 0;\n\n\t}\n\n\tgetTriangle() {\n\n\t\tif ( this._index >= this._pool.length ) {\n\n\t\t\tthis._pool.push( new Triangle() );\n\n\t\t}\n\n\t\treturn this._pool[ this._index ++ ];\n\n\t}\n\n\tclear() {\n\n\t\tthis._index = 0;\n\n\t}\n\n\treset() {\n\n\t\tthis._pool.length = 0;\n\t\tthis._index = 0;\n\n\t}\n\n}\n\n// Utility class for splitting triangles\nexport class TriangleSplitter {\n\n\tconstructor() {\n\n\t\tthis.trianglePool = new TrianglePool();\n\t\tthis.triangles = [];\n\t\tthis.normal = new Vector3();\n\n\t}\n\n\t// initialize the class with a triangle\n\tinitialize( tri ) {\n\n\t\tconst { triangles, trianglePool, normal } = this;\n\t\ttriangles.length = 0;\n\t\ttrianglePool.clear();\n\n\t\tif ( Array.isArray( tri ) ) {\n\n\t\t\tfor ( let i = 0, l = tri.length; i < l; i ++ ) {\n\n\t\t\t\tconst t = tri[ i ];\n\t\t\t\tif ( i === 0 ) {\n\n\t\t\t\t\tt.getNormal( normal );\n\n\t\t\t\t} else if ( Math.abs( 1.0 - t.getNormal( _vec ).dot( normal ) ) > EPSILON ) {\n\n\t\t\t\t\tthrow new Error( 'Triangle Splitter: Cannot initialize with triangles that have different normals.' );\n\n\t\t\t\t}\n\n\t\t\t\tconst poolTri = trianglePool.getTriangle();\n\t\t\t\tpoolTri.copy( t );\n\t\t\t\ttriangles.push( poolTri );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\ttri.getNormal( normal );\n\n\t\t\tconst poolTri = trianglePool.getTriangle();\n\t\t\tpoolTri.copy( tri );\n\t\t\ttriangles.push( poolTri );\n\n\t\t}\n\n\t}\n\n\t// Split the current set of triangles by passing a single triangle in. If the triangle is\n\t// coplanar it will attempt to split by the triangle edge planes\n\tsplitByTriangle( triangle ) {\n\n\t\tconst { normal, triangles } = this;\n\t\ttriangle.getPlane( _plane );\n\n\t\tif ( Math.abs( 1.0 - Math.abs( _plane.normal.dot( normal ) ) ) < COPLANAR_EPSILON ) {\n\n\t\t\t// if the triangle is coplanar then split by the edge planes\n\t\t\tconst arr = [ triangle.a, triangle.b, triangle.c ];\n\t\t\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\t\t\tconst nexti = ( i + 1 ) % 3;\n\n\t\t\t\tconst v0 = arr[ i ];\n\t\t\t\tconst v1 = arr[ nexti ];\n\n\t\t\t\t_vec.subVectors( v1, v0 ).normalize();\n\t\t\t\t_planeNormal.crossVectors( normal, _vec );\n\t\t\t\t_plane.setFromNormalAndCoplanarPoint( _planeNormal, v0 );\n\n\t\t\t\tthis.splitByPlane( _plane, triangle, i );\n\n\t\t\t}\n\n\t\t\tfor ( let i = 0, l = triangles.length; i < l; i ++ ) {\n\n\t\t\t\tconst t = triangles[ i ];\n\t\t\t\tt.coplanarCount = 0;\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// otherwise split by the triangle plane\n\t\t\tthis.splitByPlane( _plane, triangle );\n\n\t\t}\n\n\t}\n\n\t// Split the triangles by the given plan. If a triangle is provided then we ensure we\n\t// intersect the triangle before splitting the plane\n\tsplitByPlane( plane, triangle = null, coplanarIndex = - 1 ) {\n\n\t\tconst { triangles, trianglePool } = this;\n\n\t\t// init our triangle to check for intersection\n\t\tlet splittingTriangle = null;\n\t\tif ( triangle !== null ) {\n\n\t\t\tsplittingTriangle = _exTriangle;\n\t\t\tsplittingTriangle.copy( triangle );\n\t\t\tsplittingTriangle.needsUpdate = true;\n\n\t\t}\n\n\t\t// try to split every triangle in the class\n\t\tfor ( let i = 0, l = triangles.length; i < l; i ++ ) {\n\n\t\t\tconst tri = triangles[ i ];\n\t\t\tconst { a, b, c } = tri;\n\n\t\t\t// skip the triangle if we don't intersect with it\n\t\t\tif ( splittingTriangle ) {\n\n\t\t\t\tif ( ! splittingTriangle.intersectsTriangle( tri, _edge, true ) ) {\n\n\t\t\t\t\tcontinue;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tlet intersects = 0;\n\t\t\tlet vertexSplitEnd = - 1;\n\t\t\tlet positiveSide = 0;\n\t\t\tlet coplanarEdge = false;\n\t\t\tconst arr = [ a, b, c ];\n\t\t\tfor ( let t = 0; t < 3; t ++ ) {\n\n\t\t\t\t// get the triangle edge\n\t\t\t\tconst tNext = ( t + 1 ) % 3;\n\t\t\t\t_edge.start.copy( arr[ t ] );\n\t\t\t\t_edge.end.copy( arr[ tNext ] );\n\n\t\t\t\t// track if the start point sits on the plane or if it's on the positive side of it\n\t\t\t\t// so we can use that information to determine whether to split later.\n\t\t\t\tconst startDist = plane.distanceToPoint( _edge.start );\n\t\t\t\tconst endDist = plane.distanceToPoint( _edge.end );\n\t\t\t\tif ( Math.abs( startDist ) < COPLANAR_EPSILON && Math.abs( endDist ) < COPLANAR_EPSILON ) {\n\n\t\t\t\t\tcoplanarEdge = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t\t// we only don't consider this an intersection if the start points hits the plane\n\t\t\t\tif ( Math.abs( startDist ) < COPLANAR_EPSILON ) {\n\n\t\t\t\t\tcontinue;\n\n\t\t\t\t}\n\n\t\t\t\tif ( startDist > 0 ) {\n\n\t\t\t\t\tpositiveSide ++;\n\n\t\t\t\t}\n\n\t\t\t\t// double check the end point since the \"intersectLine\" function sometimes does not\n\t\t\t\t// return it as an intersection (see issue #28)\n\t\t\t\t// Because we ignore the start point intersection above we have to make sure we check the end\n\t\t\t\t// point intersection here.\n\t\t\t\tlet didIntersect = ! ! plane.intersectLine( _edge, _vec );\n\t\t\t\tif ( ! didIntersect && Math.abs( endDist ) < COPLANAR_EPSILON ) {\n\n\t\t\t\t\t_vec.copy( _edge.end );\n\t\t\t\t\tdidIntersect = true;\n\n\t\t\t\t}\n\n\t\t\t\t// check if we intersect the plane (ignoring the start point so we don't double count)\n\t\t\t\tif ( didIntersect && ! ( _vec.distanceTo( _edge.start ) < EPSILON ) ) {\n\n\t\t\t\t\t// if we intersect at the end point then we track that point as one that we\n\t\t\t\t\t// have to split down the middle\n\t\t\t\t\tif ( _vec.distanceTo( _edge.end ) < EPSILON ) {\n\n\t\t\t\t\t\tvertexSplitEnd = t;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t// track the split edge\n\t\t\t\t\tif ( intersects === 0 ) {\n\n\t\t\t\t\t\t_foundEdge.start.copy( _vec );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t_foundEdge.end.copy( _vec );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tintersects ++;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( coplanarEdge ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\t// skip splitting if:\n\t\t\t// - we have two points on the plane then the plane intersects the triangle exactly on an edge\n\t\t\t// - the plane does not intersect on 2 points\n\t\t\t// - the intersection edge is too small\n\t\t\tif ( intersects === 2 && _foundEdge.distance() > COPLANAR_EPSILON ) {\n\n\t\t\t\tif ( vertexSplitEnd !== - 1 ) {\n\n\t\t\t\t\tvertexSplitEnd = ( vertexSplitEnd + 1 ) % 3;\n\n\t\t\t\t\t// we're splitting along a vertex\n\t\t\t\t\tlet otherVert1 = 0;\n\t\t\t\t\tif ( otherVert1 === vertexSplitEnd ) otherVert1 = ( otherVert1 + 1 ) % 3;\n\n\t\t\t\t\tlet otherVert2 = otherVert1 + 1;\n\t\t\t\t\tif ( otherVert2 === vertexSplitEnd ) otherVert2 = ( otherVert2 + 1 ) % 3;\n\n\t\t\t\t\tconst nextTri = trianglePool.getTriangle();\n\t\t\t\t\tnextTri.a.copy( arr[ otherVert2 ] );\n\t\t\t\t\tnextTri.b.copy( _foundEdge.end );\n\t\t\t\t\tnextTri.c.copy( _foundEdge.start );\n\n\t\t\t\t\tif ( ! isTriDegenerate( nextTri ) ) {\n\n\t\t\t\t\t\ttriangles.push( nextTri );\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttri.a.copy( arr[ otherVert1 ] );\n\t\t\t\t\ttri.b.copy( _foundEdge.start );\n\t\t\t\t\ttri.c.copy( _foundEdge.end );\n\n\t\t\t\t\tif ( isTriDegenerate( tri ) ) {\n\n\t\t\t\t\t\ttriangles.splice( i, 1 );\n\t\t\t\t\t\ti --;\n\t\t\t\t\t\tl --;\n\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\n\t\t\t\t\t// we're splitting with a quad and a triangle\n\t\t\t\t\tconst singleVert = arr.findIndex( v => {\n\n\t\t\t\t\t\tif ( positiveSide >= 2 ) {\n\n\t\t\t\t\t\t\treturn plane.distanceToPoint( v ) < 0;\n\n\t\t\t\t\t\t} else {\n\n\t\t\t\t\t\t\treturn plane.distanceToPoint( v ) > 0;\n\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} );\n\n\t\t\t\t\tif ( singleVert === 0 ) {\n\n\t\t\t\t\t\tlet tmp = _foundEdge.start;\n\t\t\t\t\t\t_foundEdge.start = _foundEdge.end;\n\t\t\t\t\t\t_foundEdge.end = tmp;\n\n\t\t\t\t\t} else if ( singleVert === - 1 ) {\n\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tconst nextVert1 = ( singleVert + 1 ) % 3;\n\t\t\t\t\tconst nextVert2 = ( singleVert + 2 ) % 3;\n\n\t\t\t\t\tconst nextTri1 = trianglePool.getTriangle();\n\t\t\t\t\tconst nextTri2 = trianglePool.getTriangle();\n\n\t\t\t\t\t// choose the triangle that has the larger areas (shortest split distance)\n\t\t\t\t\tif ( arr[ nextVert1 ].distanceToSquared( _foundEdge.start ) < arr[ nextVert2 ].distanceToSquared( _foundEdge.end ) ) {\n\n\t\t\t\t\t\tnextTri1.a.copy( arr[ nextVert1 ] );\n\t\t\t\t\t\tnextTri1.b.copy( _foundEdge.start );\n\t\t\t\t\t\tnextTri1.c.copy( _foundEdge.end );\n\n\t\t\t\t\t\tnextTri2.a.copy( arr[ nextVert1 ] );\n\t\t\t\t\t\tnextTri2.b.copy( arr[ nextVert2 ] );\n\t\t\t\t\t\tnextTri2.c.copy( _foundEdge.start );\n\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\tnextTri1.a.copy( arr[ nextVert2 ] );\n\t\t\t\t\t\tnextTri1.b.copy( _foundEdge.start );\n\t\t\t\t\t\tnextTri1.c.copy( _foundEdge.end );\n\n\t\t\t\t\t\tnextTri2.a.copy( arr[ nextVert1 ] );\n\t\t\t\t\t\tnextTri2.b.copy( arr[ nextVert2 ] );\n\t\t\t\t\t\tnextTri2.c.copy( _foundEdge.end );\n\n\t\t\t\t\t}\n\n\t\t\t\t\ttri.a.copy( arr[ singleVert ] );\n\t\t\t\t\ttri.b.copy( _foundEdge.end );\n\t\t\t\t\ttri.c.copy( _foundEdge.start );\n\n\t\t\t\t\t// don't add degenerate triangles to the list\n\t\t\t\t\tif ( ! isTriDegenerate( nextTri1 ) ) {\n\n\t\t\t\t\t\ttriangles.push( nextTri1 );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( ! isTriDegenerate( nextTri2 ) ) {\n\n\t\t\t\t\t\ttriangles.push( nextTri2 );\n\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( isTriDegenerate( tri ) ) {\n\n\t\t\t\t\t\ttriangles.splice( i, 1 );\n\t\t\t\t\t\ti --;\n\t\t\t\t\t\tl --;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t} else if ( intersects === 3 ) {\n\n\t\t\t\tconsole.warn( 'TriangleClipper: Coplanar clip not handled' );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treset() {\n\n\t\tthis.triangles.length = 0;\n\n\t}\n\n}\n","import { areSharedArrayBuffersSupported } from './utils.js';\n\n// Make a new array wrapper class that more easily affords expansion when reaching it's max capacity\nexport class TypeBackedArray {\n\n\tconstructor( type, initialSize = 500 ) {\n\n\t\tconst bufferType = areSharedArrayBuffersSupported() ? SharedArrayBuffer : ArrayBuffer;\n\n\t\tthis.expansionFactor = 1.5;\n\t\tthis.type = type;\n\t\tthis.array = new type( new bufferType( initialSize * type.BYTES_PER_ELEMENT ) );\n\t\tthis.length = 0;\n\n\t}\n\n\texpand( size = null ) {\n\n\t\tconst { type, array, expansionFactor } = this;\n\n\t\tif ( size === null ) {\n\n\t\t\tsize = ~ ~ ( array.length * expansionFactor );\n\n\t\t}\n\n\t\tconst newArray = new type( size );\n\t\tnewArray.set( array, 0 );\n\t\tthis.array = newArray;\n\n\t}\n\n\tpush( ...args ) {\n\n\t\tlet { array, length } = this;\n\t\tif ( length + args.length > array.length ) {\n\n\t\t\tthis.expand();\n\t\t\tarray = this.array;\n\n\t\t}\n\n\t\tfor ( let i = 0, l = args.length; i < l; i ++ ) {\n\n\t\t\tarray[ length + i ] = args[ i ];\n\n\t\t}\n\n\t\tthis.length += args.length;\n\n\t}\n\n\tclear() {\n\n\t\tthis.length = 0;\n\n\t}\n\n}\n","import { TypeBackedArray } from './TypeBackedArray.js';\n\n// utility class for for tracking attribute data in type-backed arrays\nexport class TypedAttributeData {\n\n\tconstructor() {\n\n\t\tthis.groupAttributes = [ {} ];\n\t\tthis.groupCount = 0;\n\n\t}\n\n\tgetType( name ) {\n\n\t\treturn this.groupAttributes[ 0 ][ name ].type;\n\n\t}\n\n\tgetTotalLength( name ) {\n\n\t\tconst { groupCount, groupAttributes } = this;\n\n\t\tlet length = 0;\n\t\tfor ( let i = 0; i < groupCount; i ++ ) {\n\n\t\t\tconst attrSet = groupAttributes[ i ];\n\t\t\tlength += attrSet[ name ].length;\n\n\t\t}\n\n\t\treturn length;\n\n\t}\n\n\tgetGroupSet( index = 0 ) {\n\n\t\t// throw an error if we've never\n\t\tconst { groupAttributes } = this;\n\t\tif ( groupAttributes[ index ] ) {\n\n\t\t\tthis.groupCount = Math.max( this.groupCount, index + 1 );\n\t\t\treturn groupAttributes[ index ];\n\n\t\t}\n\n\t\t// add any new group sets required\n\t\tconst rootAttrSet = groupAttributes[ 0 ];\n\t\tthis.groupCount = Math.max( this.groupCount, index + 1 );\n\t\twhile ( index >= groupAttributes.length ) {\n\n\t\t\tconst newAttrSet = {};\n\t\t\tgroupAttributes.push( newAttrSet );\n\t\t\tfor ( const key in rootAttrSet ) {\n\n\t\t\t\tnewAttrSet[ key ] = new TypeBackedArray( rootAttrSet[ key ].type );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn groupAttributes[ index ];\n\n\t}\n\n\tgetGroupArray( name, index = 0 ) {\n\n\t\t// throw an error if we've never\n\t\tconst { groupAttributes } = this;\n\t\tconst rootAttrSet = groupAttributes[ 0 ];\n\t\tconst referenceAttr = rootAttrSet[ name ];\n\t\tif ( ! referenceAttr ) {\n\n\t\t\tthrow new Error( `TypedAttributeData: Attribute with \"${ name }\" has not been initialized` );\n\n\t\t}\n\n\t\treturn this.getGroupSet( index )[ name ];\n\n\t}\n\n\t// initializes an attribute array with the given name, type, and size\n\tinitializeArray( name, type ) {\n\n\t\tconst { groupAttributes } = this;\n\t\tconst rootSet = groupAttributes[ 0 ];\n\t\tconst referenceAttr = rootSet[ name ];\n\t\tif ( referenceAttr ) {\n\n\t\t\tif ( referenceAttr.type !== type ) {\n\n\t\t\t\tthrow new Error( `TypedAttributeData: Array ${ name } already initialized with a different type.` );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tfor ( let i = 0, l = groupAttributes.length; i < l; i ++ ) {\n\n\t\t\t\tgroupAttributes[ i ][ name ] = new TypeBackedArray( type );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tclear() {\n\n\t\tthis.groupCount = 0;\n\n\t\tconst { groupAttributes } = this;\n\t\tgroupAttributes.forEach( attrSet => {\n\n\t\t\tfor ( const key in attrSet ) {\n\n\t\t\t\tattrSet[ key ].clear();\n\n\t\t\t}\n\n\n\t\t} );\n\n\t}\n\n\tdelete( key ) {\n\n\t\tthis.groupAttributes.forEach( attrSet => {\n\n\t\t\tdelete attrSet[ key ];\n\n\t\t} );\n\n\t}\n\n\treset() {\n\n\t\tthis.groupAttributes = [];\n\n\t}\n\n}\n","import { Triangle } from 'three';\n\nclass TriangleIntersectData {\n\n\tconstructor( tri ) {\n\n\t\tthis.triangle = new Triangle().copy( tri );\n\t\tthis.intersects = {};\n\n\t}\n\n\taddTriangle( index, tri ) {\n\n\t\tthis.intersects[ index ] = new Triangle().copy( tri );\n\n\t}\n\n\tgetIntersectArray() {\n\n\t\tconst array = [];\n\t\tconst { intersects } = this;\n\t\tfor ( const key in intersects ) {\n\n\t\t\tarray.push( intersects[ key ] );\n\n\t\t}\n\n\t\treturn array;\n\n\t}\n\n}\n\nclass TriangleIntersectionSets {\n\n\tconstructor() {\n\n\t\tthis.data = {};\n\n\t}\n\n\taddTriangleIntersection( ia, triA, ib, triB ) {\n\n\t\tconst { data } = this;\n\t\tif ( ! data[ ia ] ) {\n\n\t\t\tdata[ ia ] = new TriangleIntersectData( triA );\n\n\t\t}\n\n\t\tdata[ ia ].addTriangle( ib, triB );\n\n\t}\n\n\tgetTrianglesAsArray( id = null ) {\n\n\t\tconst { data } = this;\n\t\tconst arr = [];\n\n\t\tif ( id !== null ) {\n\n\t\t\tif ( id in data ) {\n\n\t\t\t\tarr.push( data[ id ].triangle );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tfor ( const key in data ) {\n\n\t\t\t\tarr.push( data[ key ].triangle );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn arr;\n\n\t}\n\n\tgetTriangleIndices() {\n\n\t\treturn Object.keys( this.data ).map( i => parseInt( i ) );\n\n\t}\n\n\tgetIntersectionIndices( id ) {\n\n\t\tconst { data } = this;\n\t\tif ( ! data[ id ] ) {\n\n\t\t\treturn [];\n\n\t\t} else {\n\n\t\t\treturn Object.keys( data[ id ].intersects ).map( i => parseInt( i ) );\n\n\n\t\t}\n\n\t}\n\n\tgetIntersectionsAsArray( id = null, id2 = null ) {\n\n\t\tconst { data } = this;\n\t\tconst triSet = new Set();\n\t\tconst arr = [];\n\n\t\tconst addTriangles = key => {\n\n\t\t\tif ( ! data[ key ] ) return;\n\n\t\t\tif ( id2 !== null ) {\n\n\t\t\t\tif ( data[ key ].intersects[ id2 ] ) {\n\n\t\t\t\t\tarr.push( data[ key ].intersects[ id2 ] );\n\n\t\t\t\t}\n\n\t\t\t} else {\n\n\t\t\t\tconst intersects = data[ key ].intersects;\n\t\t\t\tfor ( const key2 in intersects ) {\n\n\t\t\t\t\tif ( ! triSet.has( key2 ) ) {\n\n\t\t\t\t\t\ttriSet.add( key2 );\n\t\t\t\t\t\tarr.push( intersects[ key2 ] );\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t};\n\n\t\tif ( id !== null ) {\n\n\t\t\taddTriangles( id );\n\n\t\t} else {\n\n\t\t\tfor ( const key in data ) {\n\n\t\t\t\taddTriangles( key );\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn arr;\n\n\t}\n\n\treset() {\n\n\t\tthis.data = {};\n\n\t}\n\n}\n\nexport class OperationDebugData {\n\n\tconstructor() {\n\n\t\tthis.enabled = false;\n\t\tthis.triangleIntersectsA = new TriangleIntersectionSets();\n\t\tthis.triangleIntersectsB = new TriangleIntersectionSets();\n\t\tthis.intersectionEdges = [];\n\n\t}\n\n\taddIntersectingTriangles( ia, triA, ib, triB ) {\n\n\t\tconst { triangleIntersectsA, triangleIntersectsB } = this;\n\t\ttriangleIntersectsA.addTriangleIntersection( ia, triA, ib, triB );\n\t\ttriangleIntersectsB.addTriangleIntersection( ib, triB, ia, triA );\n\n\t}\n\n\taddEdge( edge ) {\n\n\t\tthis.intersectionEdges.push( edge.clone() );\n\n\t}\n\n\treset() {\n\n\t\tthis.triangleIntersectsA.reset();\n\t\tthis.triangleIntersectsB.reset();\n\t\tthis.intersectionEdges = [];\n\n\t}\n\n}\n","import { Matrix4, Matrix3, Triangle } from 'three';\nimport {\n\tgetHitSide,\n\tcollectIntersectingTriangles,\n\tappendAttributeFromTriangle,\n\tappendAttributesFromIndices,\n\tgetOperationAction,\n\tSKIP_TRI, INVERT_TRI,\n} from './operationsUtils.js';\n\nconst _matrix = new Matrix4();\nconst _normalMatrix = new Matrix3();\nconst _triA = new Triangle();\nconst _triB = new Triangle();\nconst _tri = new Triangle();\nconst _barycoordTri = new Triangle();\n\nfunction getFirstIdFromSet( set ) {\n\n\tfor ( const id of set ) return id;\n\n}\n\n// runs the given operation against a and b using the splitter and appending data to the\n// typedAttributeData object.\nexport function performOperation( a, b, operation, splitter, typedAttributeData, options ) {\n\n\tconst { useGroups = true } = options;\n\tconst { aIntersections, bIntersections } = collectIntersectingTriangles( a, b );\n\n\tconst resultGroups = [];\n\tlet resultMaterials = null;\n\n\tlet groupOffset;\n\tgroupOffset = useGroups ? 0 : - 1;\n\tperformWholeTriangleOperations( a, b, aIntersections, operation, false, typedAttributeData, groupOffset );\n\tperformSplitTriangleOperations( a, b, aIntersections, operation, false, splitter, typedAttributeData, groupOffset );\n\n\tgroupOffset = useGroups ? a.geometry.groups.length || 1 : - 1;\n\tperformWholeTriangleOperations( b, a, bIntersections, operation, true, typedAttributeData, groupOffset );\n\tperformSplitTriangleOperations( b, a, bIntersections, operation, true, splitter, typedAttributeData, groupOffset );\n\n\treturn {\n\t\tgroups: resultGroups,\n\t\tmaterials: resultMaterials\n\t};\n\n}\n\n// perform triangle splitting and CSG operations on the set of split triangles\nfunction performSplitTriangleOperations( a, b, intersectionMap, operation, invert, splitter, attributeInfo, groupOffset = 0 ) {\n\n\tconst invertedGeometry = a.matrixWorld.determinant() < 0;\n\n\t// transforms into the local frame of matrix b\n\t_matrix\n\t\t.copy( b.matrixWorld )\n\t\t.invert()\n\t\t.multiply( a.matrixWorld );\n\n\t_normalMatrix\n\t\t.getNormalMatrix( a.matrixWorld )\n\t\t.multiplyScalar( invertedGeometry ? - 1 : 1 );\n\n\tconst groupIndices = a.geometry.groupIndices;\n\tconst aIndex = a.geometry.index;\n\tconst aPosition = a.geometry.attributes.position;\n\n\tconst bBVH = b.geometry.boundsTree;\n\tconst bIndex = b.geometry.index;\n\tconst bPosition = b.geometry.attributes.position;\n\tconst splitIds = intersectionMap.ids;\n\tconst intersectionSet = intersectionMap.intersectionSet;\n\n\t// iterate over all split triangle indices\n\tfor ( let i = 0, l = splitIds.length; i < l; i ++ ) {\n\n\t\tconst ia = splitIds[ i ];\n\t\tconst groupIndex = groupOffset === - 1 ? 0 : groupIndices[ ia ] + groupOffset;\n\t\tconst attrSet = attributeInfo.getGroupSet( groupIndex );\n\n\t\t// get the triangle in the geometry B local frame\n\t\tconst ia3 = 3 * ia;\n\t\tconst ia0 = aIndex.getX( ia3 + 0 );\n\t\tconst ia1 = aIndex.getX( ia3 + 1 );\n\t\tconst ia2 = aIndex.getX( ia3 + 2 );\n\t\t_triA.a.fromBufferAttribute( aPosition, ia0 ).applyMatrix4( _matrix );\n\t\t_triA.b.fromBufferAttribute( aPosition, ia1 ).applyMatrix4( _matrix );\n\t\t_triA.c.fromBufferAttribute( aPosition, ia2 ).applyMatrix4( _matrix );\n\n\t\t// initialize the splitter with the triangle from geometry A\n\t\tsplitter.initialize( _triA );\n\n\t\t// split the triangle with the intersecting triangles from B\n\t\tconst intersectingIndices = intersectionSet[ ia ];\n\t\tfor ( let ib = 0, l = intersectingIndices.length; ib < l; ib ++ ) {\n\n\t\t\tconst ib3 = 3 * intersectingIndices[ ib ];\n\t\t\tconst ib0 = bIndex.getX( ib3 + 0 );\n\t\t\tconst ib1 = bIndex.getX( ib3 + 1 );\n\t\t\tconst ib2 = bIndex.getX( ib3 + 2 );\n\t\t\t_triB.a.fromBufferAttribute( bPosition, ib0 );\n\t\t\t_triB.b.fromBufferAttribute( bPosition, ib1 );\n\t\t\t_triB.c.fromBufferAttribute( bPosition, ib2 );\n\t\t\tsplitter.splitByTriangle( _triB );\n\n\t\t}\n\n\t\t// for all triangles in the split result\n\t\tconst triangles = splitter.triangles;\n\t\tfor ( let ib = 0, l = triangles.length; ib < l; ib ++ ) {\n\n\t\t\t// get the barycentric coordinates of the clipped triangle to add\n\t\t\tconst clippedTri = triangles[ ib ];\n\n\t\t\t// try to use the side derived from the clipping but if it turns out to be\n\t\t\t// uncertain then fall back to the raycasting approach\n\t\t\tconst hitSide = getHitSide( clippedTri, bBVH );\n\t\t\tconst action = getOperationAction( operation, hitSide, invert );\n\t\t\tif ( action !== SKIP_TRI ) {\n\n\t\t\t\t_triA.getBarycoord( clippedTri.a, _barycoordTri.a );\n\t\t\t\t_triA.getBarycoord( clippedTri.b, _barycoordTri.b );\n\t\t\t\t_triA.getBarycoord( clippedTri.c, _barycoordTri.c );\n\n\t\t\t\tconst invertTri = action === INVERT_TRI;\n\t\t\t\tappendAttributeFromTriangle( ia, _barycoordTri, a.geometry, a.matrixWorld, _normalMatrix, attrSet, invertedGeometry !== invertTri );\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn splitIds.length;\n\n}\n\n// perform CSG operations on the set of whole triangles using a half edge structure\n// at the moment this isn't always faster due to overhead of building the half edge structure\n// and degraded connectivity due to split triangles.\n\nfunction performWholeTriangleOperations( a, b, splitTriSet, operation, invert, attributeInfo, groupOffset = 0 ) {\n\n\tconst invertedGeometry = a.matrixWorld.determinant() < 0;\n\n\t// matrix for transforming into the local frame of geometry b\n\t_matrix\n\t\t.copy( b.matrixWorld )\n\t\t.invert()\n\t\t.multiply( a.matrixWorld );\n\n\t_normalMatrix\n\t\t.getNormalMatrix( a.matrixWorld )\n\t\t.multiplyScalar( invertedGeometry ? - 1 : 1 );\n\n\tconst bBVH = b.geometry.boundsTree;\n\tconst groupIndices = a.geometry.groupIndices;\n\tconst aIndex = a.geometry.index;\n\tconst aAttributes = a.geometry.attributes;\n\tconst aPosition = aAttributes.position;\n\n\tconst stack = [];\n\tconst halfEdges = a.geometry.halfEdges;\n\tconst traverseSet = new Set();\n\tfor ( let i = 0, l = aIndex.count / 3; i < l; i ++ ) {\n\n\t\tif ( ! ( i in splitTriSet.intersectionSet ) ) {\n\n\t\t\ttraverseSet.add( i );\n\n\t\t}\n\n\t}\n\n\twhile ( traverseSet.size > 0 ) {\n\n\t\tconst id = getFirstIdFromSet( traverseSet );\n\t\ttraverseSet.delete( id );\n\n\t\tstack.push( id );\n\n\t\t// get the vertex indices\n\t\tconst i3 = 3 * id;\n\t\tconst i0 = aIndex.getX( i3 + 0 );\n\t\tconst i1 = aIndex.getX( i3 + 1 );\n\t\tconst i2 = aIndex.getX( i3 + 2 );\n\n\t\t// get the vertex position in the frame of geometry b so we can\n\t\t// perform hit testing\n\t\t_tri.a.fromBufferAttribute( aPosition, i0 ).applyMatrix4( _matrix );\n\t\t_tri.b.fromBufferAttribute( aPosition, i1 ).applyMatrix4( _matrix );\n\t\t_tri.c.fromBufferAttribute( aPosition, i2 ).applyMatrix4( _matrix );\n\n\t\t// get the side and decide if we need to cull the triangle based on the operation\n\t\tconst hitSide = getHitSide( _tri, bBVH );\n\t\tconst action = getOperationAction( operation, hitSide, invert );\n\n\t\twhile ( stack.length > 0 ) {\n\n\t\t\tconst currId = stack.pop();\n\t\t\tconst groupIndex = groupOffset === - 1 ? 0 : groupIndices[ currId ] + groupOffset;\n\t\t\tconst attrSet = attributeInfo.getGroupSet( groupIndex );\n\n\t\t\tfor ( let i = 0; i < 3; i ++ ) {\n\n\t\t\t\tconst sid = halfEdges.getSiblingTriangleIndex( currId, i );\n\t\t\t\tif ( sid !== - 1 && traverseSet.has( sid ) ) {\n\n\t\t\t\t\tstack.push( sid );\n\t\t\t\t\ttraverseSet.delete( sid );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tif ( action === SKIP_TRI ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tconst i3 = 3 * currId;\n\t\t\tconst i0 = aIndex.getX( i3 + 0 );\n\t\t\tconst i1 = aIndex.getX( i3 + 1 );\n\t\t\tconst i2 = aIndex.getX( i3 + 2 );\n\t\t\tconst invertTri = action === INVERT_TRI;\n\t\t\tappendAttributesFromIndices( i0, i1, i2, aAttributes, a.matrixWorld, _normalMatrix, attrSet, invertTri !== invertedGeometry );\n\n\t\t}\n\n\t}\n\n}\n\n","import { BufferAttribute } from 'three';\nimport { TriangleSplitter } from './TriangleSplitter.js';\nimport { TypedAttributeData } from './TypedAttributeData.js';\nimport { OperationDebugData } from './OperationDebugData.js';\nimport { performOperation } from './operations.js';\nimport { setDebugContext } from './operationsUtils.js';\nimport { Brush } from './Brush.js';\n\n// applies the given set of attribute data to the provided geometry. If the attributes are\n// not large enough to hold the new set of data then new attributes will be created. Otherwise\n// the existing attributes will be used and draw range updated to accommodate the new size.\nfunction applyToGeometry( geometry, referenceGeometry, groups, attributeInfo ) {\n\n\tlet needsDisposal = false;\n\tlet drawRange = - 1;\n\tconst groupCount = attributeInfo.groupCount;\n\n\t// set the data\n\tconst attributes = geometry.attributes;\n\tconst rootAttrSet = attributeInfo.groupAttributes[ 0 ];\n\tfor ( const key in rootAttrSet ) {\n\n\t\tconst requiredLength = attributeInfo.getTotalLength( key, groupCount );\n\t\tconst type = rootAttrSet[ key ].type;\n\t\tlet attr = attributes[ key ];\n\t\tif ( ! attr || attr.array.length < requiredLength ) {\n\n\t\t\t// create the attribute if it doesn't exist yet\n\t\t\tconst refAttr = referenceGeometry.attributes[ key ];\n\t\t\tattr = new BufferAttribute( new type( requiredLength ), refAttr.itemSize, refAttr.normalized );\n\t\t\tgeometry.setAttribute( key, attr );\n\t\t\tneedsDisposal = true;\n\n\t\t}\n\n\t\tlet offset = 0;\n\t\tfor ( let i = 0; i < groupCount; i ++ ) {\n\n\t\t\tconst { array, type, length } = attributeInfo.groupAttributes[ i ][ key ];\n\t\t\tconst trimmedArray = new type( array.buffer, 0, length );\n\t\t\tattr.array.set( trimmedArray, offset );\n\t\t\toffset += trimmedArray.length;\n\n\t\t}\n\n\t\tattr.needsUpdate = true;\n\t\tdrawRange = requiredLength / attr.itemSize;\n\n\t}\n\n\t// update the draw range\n\tgeometry.setDrawRange( 0, drawRange );\n\tgeometry.clearGroups();\n\n\tlet groupOffset = 0;\n\tfor ( let i = 0; i < groupCount; i ++ ) {\n\n\t\tconst posCount = attributeInfo.getGroupArray( 'position', i ).length / 3;\n\t\tif ( posCount !== 0 ) {\n\n\t\t\tconst group = groups[ i ];\n\t\t\tgeometry.addGroup( groupOffset, posCount, group.materialIndex );\n\t\t\tgroupOffset += posCount;\n\n\t\t}\n\n\t}\n\n\t// remove or update the index appropriately\n\tif ( geometry.index ) {\n\n\t\tconst indexArray = geometry.index.array;\n\t\tif ( indexArray.length < drawRange ) {\n\n\t\t\tgeometry.index = null;\n\t\t\tneedsDisposal = true;\n\n\t\t} else {\n\n\t\t\tfor ( let i = 0, l = indexArray.length; i < l; i ++ ) {\n\n\t\t\t\tindexArray[ i ] = i;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t// remove the bounds tree if it exists because its now out of date\n\t// TODO: can we have this dispose in the same way that a brush does?\n\tgeometry.boundsTree = null;\n\n\tif ( needsDisposal ) {\n\n\t\tgeometry.dispose();\n\n\t}\n\n\treturn geometry;\n\n}\n\nfunction getMaterialList( groups, materials ) {\n\n\tlet result = materials;\n\tif ( ! Array.isArray( materials ) ) {\n\n\t\tresult = [];\n\t\tgroups.forEach( g => {\n\n\t\t\tresult[ g.materialIndex ] = materials;\n\n\t\t} );\n\n\t}\n\n\treturn result;\n\n}\n\n// Utility class for performing CSG operations\nexport class Evaluator {\n\n\tconstructor() {\n\n\t\tthis.triangleSplitter = new TriangleSplitter();\n\t\tthis.attributeData = new TypedAttributeData();\n\t\tthis.attributes = [ 'position', 'uv', 'normal' ];\n\t\tthis.useGroups = true;\n\t\tthis.debug = new OperationDebugData();\n\n\t}\n\n\tevaluate( a, b, operation, targetBrush = new Brush() ) {\n\n\t\ta.prepareGeometry();\n\t\tb.prepareGeometry();\n\n\t\tconst { triangleSplitter, attributeData, attributes, useGroups, debug } = this;\n\t\tconst targetGeometry = targetBrush.geometry;\n\t\tconst aAttributes = a.geometry.attributes;\n\t\tfor ( let i = 0, l = attributes.length; i < l; i ++ ) {\n\n\t\t\tconst key = attributes[ i ];\n\t\t\tconst attr = aAttributes[ key ];\n\t\t\tattributeData.initializeArray( key, attr.array.constructor );\n\n\t\t}\n\n\t\tfor ( const key in attributeData.attributes ) {\n\n\t\t\tif ( ! attributes.includes( key ) ) {\n\n\t\t\t\tattributeData.delete( key );\n\n\t\t\t}\n\n\t\t}\n\n\t\tfor ( const key in targetGeometry.attributes ) {\n\n\t\t\tif ( ! attributes.includes( key ) ) {\n\n\t\t\t\ttargetGeometry.deleteAttribute( key );\n\t\t\t\ttargetGeometry.dispose();\n\n\t\t\t}\n\n\t\t}\n\n\t\tattributeData.clear();\n\n\t\tif ( debug.enabled ) {\n\n\t\t\tdebug.reset();\n\t\t\tsetDebugContext( debug );\n\n\t\t}\n\n\t\tperformOperation( a, b, operation, triangleSplitter, attributeData, { useGroups } );\n\n\t\tif ( debug.enabled ) {\n\n\t\t\tsetDebugContext( null );\n\n\t\t}\n\n\t\t// structure the groups appropriately\n\t\tconst aGroups = ! useGroups || a.geometry.groups.length === 0 ?\n\t\t\t[ { start: 0, count: Infinity, materialIndex: 0 } ] :\n\t\t\ta.geometry.groups.map( group => ( { ...group } ) );\n\n\t\tconst bGroups = ! useGroups || b.geometry.groups.length === 0 ?\n\t\t\t[ { start: 0, count: Infinity, materialIndex: 0 } ] :\n\t\t\tb.geometry.groups.map( group => ( { ...group } ) );\n\n\t\t// get the materials\n\t\tconst aMaterials = getMaterialList( aGroups, a.material );\n\t\tconst bMaterials = getMaterialList( bGroups, b.material );\n\n\t\t// adjust the material index\n\t\tbGroups.forEach( g => {\n\n\t\t\tg.materialIndex += aMaterials.length;\n\n\t\t} );\n\n\t\t// apply groups and attribute data to the geometry\n\t\tapplyToGeometry( targetGeometry, a.geometry, [ ...aGroups, ...bGroups ], attributeData );\n\n\t\t// generate the minimum set of materials needed for the list of groups and adjust the groups\n\t\t// if they're needed\n\t\tconst groups = targetGeometry.groups;\n\t\tif ( useGroups ) {\n\n\t\t\tconst materialMap = new Map();\n\t\t\tconst allMaterials = [ ...aMaterials, ...bMaterials ];\n\n\t\t\t// create a map from old to new index and remove materials that aren't used\n\t\t\tlet newIndex = 0;\n\t\t\tfor ( let i = 0, l = allMaterials.length; i < l; i ++ ) {\n\n\t\t\t\tconst foundGroup = Boolean( groups.find( group => group.materialIndex === i ) );\n\t\t\t\tif ( ! foundGroup ) {\n\n\t\t\t\t\tallMaterials[ i ] = null;\n\n\t\t\t\t} else {\n\n\t\t\t\t\tmaterialMap.set( i, newIndex );\n\t\t\t\t\tnewIndex ++;\n\n\t\