UNPKG

@wendraw/three-bvh-csg

Version:

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

1 lines 199 kB
{"version":3,"file":"index.umd.cjs","sources":["../src/core/utils/hashUtils.js","../src/core/utils/geometryUtils.js","../src/core/utils/halfEdgeUtils.js","../src/core/utils/RaySet.js","../src/core/utils/computeDisjointEdges.js","../src/core/HalfEdgeMap.js","../src/core/Brush.js","../src/core/utils/triangleUtils.js","../src/core/TriangleSplitter.js","../src/core/TypeBackedArray.js","../src/core/TypedAttributeData.js","../src/core/IntersectionMap.js","../src/core/constants.js","../src/core/operations/operationsUtils.js","../src/core/debug/OperationDebugData.js","../src/core/operations/operations.js","../src/core/Evaluator.js","../src/core/operations/Operation.js","../src/core/operations/OperationGroup.js","../src/materials/shaderUtils.js","../src/materials/GridMaterial.js","../src/core/debug/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_WIDTH = 1e-6;\nconst HASH_HALF_WIDTH = HASH_WIDTH * 0.5;\nconst HASH_MULTIPLIER = Math.pow( 10, - Math.log10( HASH_WIDTH ) );\nconst HASH_ADDITION = HASH_HALF_WIDTH * HASH_MULTIPLIER;\nexport function hashNumber( v ) {\n\n\treturn ~ ~ ( v * HASH_MULTIPLIER + HASH_ADDITION );\n\n}\n\nexport function hashVertex2( v ) {\n\n\treturn `${ hashNumber( v.x ) },${ hashNumber( v.y ) }`;\n\n}\n\nexport function hashVertex3( v ) {\n\n\treturn `${ hashNumber( v.x ) },${ hashNumber( v.y ) },${ hashNumber( v.z ) }`;\n\n}\n\nexport function hashVertex4( v ) {\n\n\treturn `${ hashNumber( v.x ) },${ hashNumber( v.y ) },${ hashNumber( v.z ) },${ hashNumber( v.w ) }`;\n\n}\n\nexport function hashRay( r ) {\n\n\treturn `${ hashVertex3( r.origin ) }-${ hashVertex3( r.direction ) }`;\n\n}\n\nexport function toNormalizedRay( v0, v1, target ) {\n\n\t// get a normalized direction\n\ttarget\n\t\t.direction\n\t\t.subVectors( v1, v0 )\n\t\t.normalize();\n\n\t// project the origin onto the perpendicular plane that\n\t// passes through 0, 0, 0\n\tconst scalar = v0.dot( target.direction );\n\ttarget.\n\t\torigin\n\t\t.copy( v0 )\n\t\t.addScaledVector( target.direction, - scalar );\n\n\treturn target;\n\n}\n","import { BufferAttribute } from 'three';\n\nexport 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\nexport function getIndexArray( vertexCount, BufferConstructor = ArrayBuffer ) {\n\n\tif ( vertexCount > 65535 ) {\n\n\t\treturn new Uint32Array( new BufferConstructor( 4 * vertexCount ) );\n\n\t} else {\n\n\t\treturn new Uint16Array( new BufferConstructor( 2 * vertexCount ) );\n\n\t}\n\n}\n\nexport function ensureIndex( geo, options ) {\n\n\tif ( ! geo.index ) {\n\n\t\tconst vertexCount = geo.attributes.position.count;\n\t\tconst BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;\n\t\tconst index = getIndexArray( vertexCount, BufferConstructor );\n\t\tgeo.setIndex( new BufferAttribute( index, 1 ) );\n\n\t\tfor ( let i = 0; i < vertexCount; i ++ ) {\n\n\t\t\tindex[ i ] = i;\n\n\t\t}\n\n\t}\n\n}\n\nexport function getVertexCount( geo ) {\n\n\treturn geo.index ? geo.index.count : geo.attributes.position.count;\n\n}\n\nexport function getTriCount( geo ) {\n\n\treturn getVertexCount( geo ) / 3;\n\n}\n","import { Vector3 } from 'three';\n\nconst DEGENERATE_EPSILON = 1e-8;\nconst _tempVec = new Vector3();\n\nexport function toTriIndex( v ) {\n\n\treturn ~ ~ ( v / 3 );\n\n}\n\nexport function toEdgeIndex( v ) {\n\n\treturn v % 3;\n\n}\n\nexport function sortEdgeFunc( a, b ) {\n\n\treturn a.start - b.start;\n\n}\n\nexport function getProjectedDistance( ray, vec ) {\n\n\treturn _tempVec.subVectors( vec, ray.origin ).dot( ray.direction );\n\n}\n\nexport function hasOverlaps( arr ) {\n\n\tarr = [ ...arr ].sort( sortEdgeFunc );\n\tfor ( let i = 0, l = arr.length; i < l - 1; i ++ ) {\n\n\t\tconst info0 = arr[ i ];\n\t\tconst info1 = arr[ i + 1 ];\n\n\t\tif ( info1.start < info0.end && Math.abs( info1.start - info0.end ) > 1e-5 ) {\n\n\t\t\treturn true;\n\n\t\t}\n\n\t}\n\n\treturn false;\n\n}\n\nexport function getEdgeSetLength( arr ) {\n\n\tlet tot = 0;\n\tarr.forEach( ( { start, end } ) => tot += end - start );\n\treturn tot;\n\n}\n\nexport function matchEdges( forward, reverse, disjointConnectivityMap, eps = DEGENERATE_EPSILON ) {\n\n\tforward.sort( sortEdgeFunc );\n\treverse.sort( sortEdgeFunc );\n\n\tfor ( let i = 0; i < forward.length; i ++ ) {\n\n\t\tconst e0 = forward[ i ];\n\t\tfor ( let o = 0; o < reverse.length; o ++ ) {\n\n\t\t\tconst e1 = reverse[ o ];\n\t\t\tif ( e1.start > e0.end ) {\n\n\t\t\t\t// e2 is completely after e1\n\t\t\t\t// break;\n\n\t\t\t\t// NOTE: there are cases where there are overlaps due to precision issues or\n\t\t\t\t// thin / degenerate triangles. Assuming the sibling side has the same issues\n\t\t\t\t// we let the matching work here. Long term we should remove the degenerate\n\t\t\t\t// triangles before this.\n\n\t\t\t} else if ( e0.end < e1.start || e1.end < e0.start ) {\n\n\t\t\t\t// e1 is completely before e2\n\t\t\t\tcontinue;\n\n\t\t\t} else if ( e0.start <= e1.start && e0.end >= e1.end ) {\n\n\t\t\t\t// e1 is larger than and e2 is completely within e1\n\t\t\t\tif ( ! areDistancesDegenerate( e1.end, e0.end ) ) {\n\n\t\t\t\t\tforward.splice( i + 1, 0, {\n\t\t\t\t\t\tstart: e1.end,\n\t\t\t\t\t\tend: e0.end,\n\t\t\t\t\t\tindex: e0.index,\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t\te0.end = e1.start;\n\n\t\t\t\te1.start = 0;\n\t\t\t\te1.end = 0;\n\n\t\t\t} else if ( e0.start >= e1.start && e0.end <= e1.end ) {\n\n\t\t\t\t// e2 is larger than and e1 is completely within e2\n\t\t\t\tif ( ! areDistancesDegenerate( e0.end, e1.end ) ) {\n\n\t\t\t\t\treverse.splice( o + 1, 0, {\n\t\t\t\t\t\tstart: e0.end,\n\t\t\t\t\t\tend: e1.end,\n\t\t\t\t\t\tindex: e1.index,\n\t\t\t\t\t} );\n\n\t\t\t\t}\n\n\t\t\t\te1.end = e0.start;\n\n\t\t\t\te0.start = 0;\n\t\t\t\te0.end = 0;\n\n\t\t\t} else if ( e0.start <= e1.start && e0.end <= e1.end ) {\n\n\t\t\t\t// e1 overlaps e2 at the beginning\n\t\t\t\tconst tmp = e0.end;\n\t\t\t\te0.end = e1.start;\n\t\t\t\te1.start = tmp;\n\n\t\t\t} else if ( e0.start >= e1.start && e0.end >= e1.end ) {\n\n\t\t\t\t// e1 overlaps e2 at the end\n\t\t\t\tconst tmp = e1.end;\n\t\t\t\te1.end = e0.start;\n\t\t\t\te0.start = tmp;\n\n\t\t\t} else {\n\n\t\t\t\tthrow new Error();\n\n\t\t\t}\n\n\t\t\t// Add the connectivity information\n\t\t\tif ( ! disjointConnectivityMap.has( e0.index ) ) {\n\n\t\t\t\tdisjointConnectivityMap.set( e0.index, [] );\n\n\t\t\t}\n\n\t\t\tif ( ! disjointConnectivityMap.has( e1.index ) ) {\n\n\t\t\t\tdisjointConnectivityMap.set( e1.index, [] );\n\n\t\t\t}\n\n\t\t\tdisjointConnectivityMap\n\t\t\t\t.get( e0.index )\n\t\t\t\t.push( e1.index );\n\n\t\t\tdisjointConnectivityMap\n\t\t\t\t.get( e1.index )\n\t\t\t\t.push( e0.index );\n\n\t\t\tif ( isEdgeDegenerate( e1 ) ) {\n\n\t\t\t\treverse.splice( o, 1 );\n\t\t\t\to --;\n\n\t\t\t}\n\n\t\t\tif ( isEdgeDegenerate( e0 ) ) {\n\n\t\t\t\t// and if we have to remove the current original edge then exit this loop\n\t\t\t\t// so we can work on the next one\n\t\t\t\tforward.splice( i, 1 );\n\t\t\t\ti --;\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tcleanUpEdgeSet( forward );\n\tcleanUpEdgeSet( reverse );\n\n\tfunction cleanUpEdgeSet( arr ) {\n\n\t\tfor ( let i = 0; i < arr.length; i ++ ) {\n\n\t\t\tif ( isEdgeDegenerate( arr[ i ] ) ) {\n\n\t\t\t\tarr.splice( i, 1 );\n\t\t\t\ti --;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tfunction areDistancesDegenerate( start, end ) {\n\n\t\treturn Math.abs( end - start ) < eps;\n\n\t}\n\n\tfunction isEdgeDegenerate( e ) {\n\n\t\treturn Math.abs( e.end - e.start ) < eps;\n\n\t}\n\n}\n","const DIST_EPSILON = 1e-5;\nconst ANGLE_EPSILON = 1e-4;\n\nexport class RaySet {\n\n\tconstructor() {\n\n\t\tthis._rays = [];\n\n\t}\n\n\taddRay( ray ) {\n\n\t\tthis._rays.push( ray );\n\n\t}\n\n\tfindClosestRay( ray ) {\n\n\t\tconst rays = this._rays;\n\t\tconst inv = ray.clone();\n\t\tinv.direction.multiplyScalar( - 1 );\n\n\t\tlet bestScore = Infinity;\n\t\tlet bestRay = null;\n\t\tfor ( let i = 0, l = rays.length; i < l; i ++ ) {\n\n\t\t\tconst r = rays[ i ];\n\t\t\tif ( skipRay( r, ray ) && skipRay( r, inv ) ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tconst rayScore = scoreRays( r, ray );\n\t\t\tconst invScore = scoreRays( r, inv );\n\t\t\tconst score = Math.min( rayScore, invScore );\n\t\t\tif ( score < bestScore ) {\n\n\t\t\t\tbestScore = score;\n\t\t\t\tbestRay = r;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn bestRay;\n\n\t\tfunction skipRay( r0, r1 ) {\n\n\t\t\tconst distOutOfThreshold = r0.origin.distanceTo( r1.origin ) > DIST_EPSILON;\n\t\t\tconst angleOutOfThreshold = r0.direction.angleTo( r1.direction ) > ANGLE_EPSILON;\n\t\t\treturn angleOutOfThreshold || distOutOfThreshold;\n\n\t\t}\n\n\t\tfunction scoreRays( r0, r1 ) {\n\n\t\t\tconst originDistance = r0.origin.distanceTo( r1.origin );\n\t\t\tconst angleDistance = r0.direction.angleTo( r1.direction );\n\t\t\treturn originDistance / DIST_EPSILON + angleDistance / ANGLE_EPSILON;\n\n\t\t}\n\n\t}\n\n}\n","import { Vector3, Ray } from 'three';\nimport { toEdgeIndex, toTriIndex, matchEdges, getProjectedDistance } from './halfEdgeUtils.js';\nimport { toNormalizedRay } from './hashUtils.js';\nimport { RaySet } from './RaySet.js';\n\nconst _v0 = new Vector3();\nconst _v1 = new Vector3();\nconst _ray = new Ray();\n\nexport function computeDisjointEdges(\n\tgeometry,\n\tunmatchedSet,\n\teps,\n) {\n\n\tconst attributes = geometry.attributes;\n\tconst indexAttr = geometry.index;\n\tconst posAttr = attributes.position;\n\n\tconst disjointConnectivityMap = new Map();\n\tconst fragmentMap = new Map();\n\tconst edges = Array.from( unmatchedSet );\n\tconst rays = new RaySet();\n\n\tfor ( let i = 0, l = edges.length; i < l; i ++ ) {\n\n\t\t// get the triangle edge\n\t\tconst index = edges[ i ];\n\t\tconst triIndex = toTriIndex( index );\n\t\tconst edgeIndex = toEdgeIndex( index );\n\n\t\tlet i0 = 3 * triIndex + edgeIndex;\n\t\tlet i1 = 3 * triIndex + ( edgeIndex + 1 ) % 3;\n\t\tif ( indexAttr ) {\n\n\t\t\ti0 = indexAttr.getX( i0 );\n\t\t\ti1 = indexAttr.getX( i1 );\n\n\t\t}\n\n\t\t_v0.fromBufferAttribute( posAttr, i0 );\n\t\t_v1.fromBufferAttribute( posAttr, i1 );\n\n\t\t// get the ray corresponding to the edge\n\t\ttoNormalizedRay( _v0, _v1, _ray );\n\n\t\t// find the shared ray with other edges\n\t\tlet info;\n\t\tlet commonRay = rays.findClosestRay( _ray );\n\t\tif ( commonRay === null ) {\n\n\t\t\tcommonRay = _ray.clone();\n\t\t\trays.addRay( commonRay );\n\n\t\t}\n\n\t\tif ( ! fragmentMap.has( commonRay ) ) {\n\n\t\t\tfragmentMap.set( commonRay, {\n\n\t\t\t\tforward: [],\n\t\t\t\treverse: [],\n\t\t\t\tray: commonRay,\n\n\t\t\t} );\n\n\t\t}\n\n\t\tinfo = fragmentMap.get( commonRay );\n\n\t\t// store the stride of edge endpoints along the ray\n\t\tlet start = getProjectedDistance( commonRay, _v0 );\n\t\tlet end = getProjectedDistance( commonRay, _v1 );\n\t\tif ( start > end ) {\n\n\t\t\t[ start, end ] = [ end, start ];\n\n\t\t}\n\n\t\tif ( _ray.direction.dot( commonRay.direction ) < 0 ) {\n\n\t\t\tinfo.reverse.push( { start, end, index } );\n\n\t\t} else {\n\n\t\t\tinfo.forward.push( { start, end, index } );\n\n\t\t}\n\n\t}\n\n\t// match the found sibling edges\n\tfragmentMap.forEach( ( { forward, reverse }, ray ) => {\n\n\t\tmatchEdges( forward, reverse, disjointConnectivityMap, eps );\n\n\t\tif ( forward.length === 0 && reverse.length === 0 ) {\n\n\t\t\tfragmentMap.delete( ray );\n\n\t\t}\n\n\t} );\n\n\treturn {\n\t\tdisjointConnectivityMap,\n\t\tfragmentMap,\n\t};\n\n}\n\n","import { Vector2, Vector3, Vector4 } from 'three';\nimport { hashNumber, hashVertex2, hashVertex3, hashVertex4 } from './utils/hashUtils.js';\nimport { getTriCount } from './utils/geometryUtils.js';\nimport { computeDisjointEdges } from './utils/computeDisjointEdges.js';\n\nconst _vec2 = new Vector2();\nconst _vec3 = new Vector3();\nconst _vec4 = new Vector4();\nconst _hashes = [ '', '', '' ];\n\nexport class HalfEdgeMap {\n\n\tconstructor( geometry = null ) {\n\n\t\t// result data\n\t\tthis.data = null;\n\t\tthis.disjointConnections = null;\n\t\tthis.unmatchedDisjointEdges = null;\n\t\tthis.unmatchedEdges = - 1;\n\t\tthis.matchedEdges = - 1;\n\n\t\t// options\n\t\tthis.useDrawRange = true;\n\t\tthis.useAllAttributes = false;\n\t\tthis.matchDisjointEdges = false;\n\t\tthis.degenerateEpsilon = 1e-8;\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\tgetDisjointSiblingTriangleIndices( triIndex, edgeIndex ) {\n\n\t\tconst index = triIndex * 3 + edgeIndex;\n\t\tconst arr = this.disjointConnections.get( index );\n\t\treturn arr ? arr.map( i => ~ ~ ( i / 3 ) ) : [];\n\n\t}\n\n\tgetDisjointSiblingEdgeIndices( triIndex, edgeIndex ) {\n\n\t\tconst index = triIndex * 3 + edgeIndex;\n\t\tconst arr = this.disjointConnections.get( index );\n\t\treturn arr ? arr.map( i => i % 3 ) : [];\n\n\t}\n\n\tisFullyConnected() {\n\n\t\treturn this.unmatchedEdges === 0;\n\n\t}\n\n\tupdateFrom( geometry ) {\n\n\t\tconst { useAllAttributes, useDrawRange, matchDisjointEdges, degenerateEpsilon } = this;\n\t\tconst hashFunction = useAllAttributes ? hashAllAttributes : hashPositionAttribute;\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 attrKeys = useAllAttributes ? Object.keys( attributes ) : null;\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 = getTriCount( geometry );\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 ( 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 matchedEdges = 0;\n\t\tlet unmatchedSet = new Set();\n\t\tfor ( let i = offset, l = triCount * 3 + offset; i < l; i += 3 ) {\n\n\t\t\tconst i3 = i;\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_hashes[ e ] = hashFunction( 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 vh0 = _hashes[ e ];\n\t\t\t\tconst vh1 = _hashes[ nextE ];\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 index = i3 + e;\n\t\t\t\t\tconst otherIndex = map.get( reverseHash );\n\t\t\t\t\tdata[ index ] = otherIndex;\n\t\t\t\t\tdata[ otherIndex ] = index;\n\t\t\t\t\tmap.delete( reverseHash );\n\t\t\t\t\tmatchedEdges += 2;\n\t\t\t\t\tunmatchedSet.delete( otherIndex );\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\tconst index = i3 + e;\n\t\t\t\t\tmap.set( hash, index );\n\t\t\t\t\tunmatchedSet.add( index );\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t\tif ( matchDisjointEdges ) {\n\n\t\t\tconst {\n\t\t\t\tfragmentMap,\n\t\t\t\tdisjointConnectivityMap,\n\t\t\t} = computeDisjointEdges( geometry, unmatchedSet, degenerateEpsilon );\n\n\t\t\tunmatchedSet.clear();\n\t\t\tfragmentMap.forEach( ( { forward, reverse } ) => {\n\n\t\t\t\tforward.forEach( ( { index } ) => unmatchedSet.add( index ) );\n\t\t\t\treverse.forEach( ( { index } ) => unmatchedSet.add( index ) );\n\n\t\t\t} );\n\n\t\t\tthis.unmatchedDisjointEdges = fragmentMap;\n\t\t\tthis.disjointConnections = disjointConnectivityMap;\n\t\t\tmatchedEdges = triCount * 3 - unmatchedSet.size;\n\n\t\t}\n\n\t\tthis.matchedEdges = matchedEdges;\n\t\tthis.unmatchedEdges = unmatchedSet.size;\n\t\tthis.data = data;\n\n\t\tfunction hashPositionAttribute( i ) {\n\n\t\t\t_vec3.fromBufferAttribute( posAttr, i );\n\t\t\treturn hashVertex3( _vec3 );\n\n\t\t}\n\n\t\tfunction hashAllAttributes( i ) {\n\n\t\t\tlet result = '';\n\t\t\tfor ( let k = 0, l = attrKeys.length; k < l; k ++ ) {\n\n\t\t\t\tconst attr = attributes[ attrKeys[ k ] ];\n\t\t\t\tlet str;\n\t\t\t\tswitch ( attr.itemSize ) {\n\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\tstr = hashNumber( attr.getX( i ) );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 2:\n\t\t\t\t\t\tstr = hashVertex2( _vec2.fromBufferAttribute( attr, i ) );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 3:\n\t\t\t\t\t\tstr = hashVertex3( _vec3.fromBufferAttribute( attr, i ) );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 4:\n\t\t\t\t\t\tstr = hashVertex4( _vec4.fromBufferAttribute( attr, i ) );\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t}\n\n\t\t\t\tif ( result !== '' ) {\n\n\t\t\t\t\tresult += '|';\n\n\t\t\t\t}\n\n\t\t\t\tresult += str;\n\n\t\t\t}\n\n\t\t\treturn result;\n\n\t\t}\n\n\t}\n\n}\n","import { Mesh, Matrix4 } from 'three';\nimport { MeshBVH } from 'three-mesh-bvh';\nimport { HalfEdgeMap } from './HalfEdgeMap.js';\nimport { areSharedArrayBuffersSupported, convertToSharedArrayBuffer, ensureIndex, getTriCount } from './utils/geometryUtils.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\tconst useSharedArrayBuffer = areSharedArrayBuffersSupported();\n\t\tif ( useSharedArrayBuffer ) {\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\tensureIndex( geometry, { useSharedArrayBuffer } );\n\t\t\tgeometry.boundsTree = new MeshBVH( geometry, { maxLeafTris: 3, indirect: true, useSharedArrayBuffer } );\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 = getTriCount( geometry );\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","import { Vector3 } from 'three';\n\nconst EPSILON = 1e-14;\nconst _AB = new Vector3();\nconst _AC = new Vector3();\nconst _CB = new Vector3();\n\nexport function isTriDegenerate( tri, eps = EPSILON ) {\n\n\t// compute angles to determine whether they're degenerate\n\t_AB.subVectors( tri.b, tri.a );\n\t_AC.subVectors( tri.c, tri.a );\n\t_CB.subVectors( tri.b, tri.c );\n\n\tconst angle1 = _AB.angleTo( _AC );\t\t\t\t// AB v AC\n\tconst angle2 = _AB.angleTo( _CB );\t\t\t\t// AB v BC\n\tconst angle3 = Math.PI - angle1 - angle2;\t\t// 180deg - angle1 - angle2\n\n\treturn Math.abs( angle1 ) < eps ||\n\t\tMath.abs( angle2 ) < eps ||\n\t\tMath.abs( angle3 ) < eps ||\n\t\ttri.a.distanceToSquared( tri.b ) < eps ||\n\t\ttri.a.distanceToSquared( tri.c ) < eps ||\n\t\ttri.b.distanceToSquared( tri.c ) < eps;\n\n}\n","import { Triangle, Line3, Vector3, Plane } from 'three';\nimport { ExtendedTriangle } from 'three-mesh-bvh';\nimport { isTriDegenerate } from './utils/triangleUtils.js';\n\n// NOTE: these epsilons likely should all be the same since they're used to measure the\n// distance from a point to a plane which needs to be done consistently\nconst EPSILON = 1e-10;\nconst COPLANAR_EPSILON = 1e-10;\nconst PARALLEL_EPSILON = 1e-10;\nconst _edge = new Line3();\nconst _foundEdge = new Line3();\nconst _vec = new Vector3();\nconst _triangleNormal = new Vector3();\nconst _planeNormal = new Vector3();\nconst _plane = new Plane();\nconst _splittingTriangle = new ExtendedTriangle();\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\t\tthis.coplanarTriangleUsed = false;\n\n\t}\n\n\t// initialize the class with a triangle\n\tinitialize( tri ) {\n\n\t\tthis.reset();\n\n\t\tconst { triangles, trianglePool, normal } = this;\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.getNormal( _triangleNormal ).normalize();\n\n\t\tif ( Math.abs( 1.0 - Math.abs( _triangleNormal.dot( normal ) ) ) < PARALLEL_EPSILON ) {\n\n\t\t\tthis.coplanarTriangleUsed = true;\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\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// plane positive direction is toward triangle center\n\t\t\t\t_vec.subVectors( v1, v0 ).normalize();\n\t\t\t\t_planeNormal.crossVectors( _triangleNormal, _vec );\n\t\t\t\t_plane.setFromNormalAndCoplanarPoint( _planeNormal, v0 );\n\n\t\t\t\tthis.splitByPlane( _plane, triangle );\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\t// otherwise split by the triangle plane\n\t\t\ttriangle.getPlane( _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, clippingTriangle ) {\n\n\t\tconst { triangles, trianglePool } = this;\n\n\t\t// init our triangle to check for intersection\n\t\t_splittingTriangle.copy( clippingTriangle );\n\t\t_splittingTriangle.needsUpdate = true;\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\n\t\t\t// skip the triangle if we don't intersect with it\n\t\t\tif ( ! _splittingTriangle.intersectsTriangle( tri, _edge, true ) ) {\n\n\t\t\t\tcontinue;\n\n\t\t\t}\n\n\t\t\tconst { a, b, c } = tri;\n\t\t\tlet intersects = 0;\n\t\t\tlet vertexSplitEnd = - 1;\n\t\t\tlet coplanarEdge = false;\n\t\t\tlet posSideVerts = [];\n\t\t\tlet negSideVerts = [];\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\tif ( startDist > 0 ) {\n\n\t\t\t\t\tposSideVerts.push( t );\n\n\t\t\t\t} else {\n\n\t\t\t\t\tnegSideVerts.push( t );\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\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\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\t// - we're not along a coplanar edge\n\t\t\tif ( ! coplanarEdge && 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 ) {\n\n\t\t\t\t\t\totherVert1 = ( otherVert1 + 1 ) % 3;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tlet otherVert2 = otherVert1 + 1;\n\t\t\t\t\tif ( otherVert2 === vertexSplitEnd ) {\n\n\t\t\t\t\t\totherVert2 = ( otherVert2 + 1 ) % 3;\n\n\t\t\t\t\t}\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\t// finish off the adjusted triangle\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\t// TODO: what happens when we find that about the pos and negative\n\t\t\t\t\t// sides have only a single vertex?\n\t\t\t\t\tconst singleVert =\n\t\t\t\t\t\tposSideVerts.length >= 2 ?\n\t\t\t\t\t\t\tnegSideVerts[ 0 ] :\n\t\t\t\t\t\t\tposSideVerts[ 0 ];\n\n\t\t\t\t\t// swap the direction of the intersection edge depending on which\n\t\t\t\t\t// side of the plane the single vertex is on to align with the\n\t\t\t\t\t// correct winding order.\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}\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\t// finish off the adjusted triangle\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\t\tthis.trianglePool.clear();\n\t\tthis.coplanarTriangleUsed = false;\n\n\t}\n\n}\n","import { areSharedArrayBuffersSupported } from './utils/geometryUtils.js';\n\nfunction ceilToFourByteStride( byteLength ) {\n\n\tbyteLength = ~ ~ byteLength;\n\treturn byteLength + 4 - byteLength % 4;\n\n}\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\n\t\tthis.expansionFactor = 1.5;\n\t\tthis.type = type;\n\t\tthis.length = 0;\n\t\tthis.array = null;\n\n\t\tthis.setSize( initialSize );\n\n\t}\n\n\tsetType( type ) {\n\n\t\tif ( this.length !== 0 ) {\n\n\t\t\tthrow new Error( 'TypeBackedArray: Cannot change the type while there is used data in the buffer.' );\n\n\t\t}\n\n\t\tconst buffer = this.array.buffer;\n\t\tthis.array = new type( buffer );\n\t\tthis.type = type;\n\n\t}\n\n\tsetSize( size ) {\n\n\t\tif ( this.array && size === this.array.length ) {\n\n\t\t\treturn;\n\n\t\t}\n\n\t\t// ceil to the nearest 4 bytes so we can replace the array with any type using the same buffer\n\t\tconst type = this.type;\n\t\tconst bufferType = areSharedArrayBuffersSupported() ? SharedArrayBuffer : ArrayBuffer;\n\t\tconst newArray = new type( new bufferType( ceilToFourByteStride( size * type.BYTES_PER_ELEMENT ) ) );\n\t\tif ( this.array ) {\n\n\t\t\tnewArray.set( this.array, 0 );\n\n\t\t}\n\n\t\tthis.array = newArray;\n\n\t}\n\n\texpand() {\n\n\t\tconst { array, expansionFactor } = this;\n\t\tthis.setSize( array.length * expansionFactor );\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 for a set\n// of groups. The set of attributes is kept for each group and are expected to be the\n// same buffer type.\nexport class TypedAttributeData {\n\n\tconstructor() {\n\n\t\tthis.groupAttributes = [ {} ];\n\t\tthis.groupCount = 0;\n\n\t}\n\n\t// returns the buffer type for the given attribute\n\tgetType( name ) {\n\n\t\treturn this.groupAttributes[ 0 ][ name ].type;\n\n\t}\n\n\tgetItemSize( name ) {\n\n\t\treturn this.groupAttributes[ 0 ][ name ].itemSize;\n\n\t}\n\n\tgetNormalized( name ) {\n\n\t\treturn this.groupAttributes[ 0 ][ name ].normalized;\n\n\t}\n\n\tgetCount( index ) {\n\n\t\tif ( this.groupCount <= index ) {\n\n\t\t\treturn 0;\n\n\t\t}\n\n\t\tconst pos = this.getGroupAttrArray( 'position', index );\n\t\treturn pos.length / pos.itemSize;\n\n\t}\n\n\t// returns the total length required for all groups for the given attribute\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\tgetGroupAttrSet( index = 0 ) {\n\n\t\t// TODO: can this be abstracted?\n\t\t// Return the exiting group set if necessary\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 refAttrSet = 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 refAttrSet ) {\n\n\t\t\t\tconst refAttr = refAttrSet[ key ];\n\t\t\t\tconst newAttr = new TypeBackedArray( refAttr.type );\n\t\t\t\tnewAttr.itemSize = refAttr.itemSize;\n\t\t\t\tnewAttr.normalized = refAttr.normalized;\n\t\t\t\tnewAttrSet[ key ] = newAttr;\n\n\t\t\t}\n\n\t\t}\n\n\t\treturn groupAttributes[ index ];\n\n\t}\n\n\t// Get the raw array for the group set of data\n\tgetGroupAttrArray( name, index = 0 ) {\n\n\t\t// throw an error if we've never\n\t\tconst { groupAttributes } = this;\n\t\tconst referenceAttrSet = groupAttributes[ 0 ];\n\t\tconst referenceAttr = referenceAttrSet[ 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.getGroupAttrSet( index )[ name ];\n\n\t}\n\n\t// initializes an attribute array with the given name, type, and size\n\tinitializeArray( name, type, itemSize, normalized ) {\n\n\t\tconst { groupAttributes } = this;\n\t\tconst referenceAttrSet = groupAttributes[ 0 ];\n\t\tconst referenceAttr = referenceAttrSet[ name ];\n\t\tif ( referenceAttr ) {\n\n\t\t\tif ( referenceAttr.type !== type ) {\n\n\t\t\t\tfor ( let i = 0, l = groupAttributes.length; i < l; i ++ ) {\n\n\t\t\t\t\tconst arr = groupAttributes[ i ][ name ];\n\t\t\t\t\tarr.setType( type );\n\t\t\t\t\tarr.itemSize = itemSize;\n\t\t\t\t\tarr.normalized = normalized;\n\n\t\t\t\t}\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\tconst arr = new TypeBackedArray( type );\n\t\t\t\tarr.itemSize = itemSize;\n\t\t\t\tarr.normalized = normalized;\n\t\t\t\tgroupAttributes[ i ][ name ] = arr;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\t// Clear all the data\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\t// Remove the given key\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\t// Reset the datasets completely\n\treset() {\n\n\t\tthis.groupAttributes = [];\n\t\tthis.groupCount = 0;\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 REVERSE_SUBTRACTION = 2;\nexport const INTERSECTION = 3;\nexport const DIFFERENCE = 4;\n\n// guaranteed non manifold results\nexport const HOLLOW_SUBTRACTION = 5;\nexport const HOLLOW_INTERSECTION = 6;\n","import { Ray, Matrix4, DoubleSide, Vector3, Vector4, Triangle, Line3 } from 'three';\nimport { IntersectionMap } from '../IntersectionMap.js';\nimport {\n\tADDITION,\n\tSUBTRACTION,\n\tREVERSE_SUBTRACTION,\n\tINTERSECTION,\n\tDIFFERENCE,\n\tHOLLOW_SUBTRACTION,\n\tHOLLOW_INTERSECTION,\n} from '../constants.js';\nimport { isTriDegenerate } from '../utils/triangleUtils.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\nconst FLOATING_COPLANAR_EPSILON = 1e-14;\n\nlet _debugContext = null;\nexport function setDebugContext( debugData ) {\n\n\t_debugContext = debugData;\n\n}\n\nexport function getHitSide( tri, bvh ) {\n\n\ttri.getMidpoint( _ray.origin );\n\ttri.getNormal( _ray.direction );\n\n\tconst hit = bvh.raycastFirst( _ray, DoubleSide );\n\tconst hitBackSide = Boolean( hit && _ray.direction.dot( hit.face.normal ) > 0 );\n\treturn hitBackSide ? BACK_SIDE : FRONT_SIDE;\n\n}\n\nexport function getHitSideWithCoplanarCheck( 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 ( ! isTriDegenerate( triangleA ) && ! isTriDegenerate( triangleB ) ) {\n\n\t\t\t\t// due to floating point error it's possible that we can have two overlapping, coplanar triangles\n\t\t\t\t// that are a _tiny_ fraction of a value away from each other. If we find that case then check the\n\t\t\t\t// distance between triangles and if it's small enough consider them intersecting.\n\t\t\t\tlet intersected = triangleA.intersectsTriangle( triangleB, _edge, true );\n\t\t\t\tif ( ! intersected ) {\n\n\t\t\t\t\tconst pa = triangleA.plane;\n\t\t\t\t\tconst pb = triangleB.plane;\n\t\t\t\t\tconst na = pa.normal;\n\t\t\t\t\tconst nb = pb.normal;\n\n\t\t\t\t\tif ( na.dot( nb ) === 1 && Math.abs( pa.constant - pb.constant ) < FLOATING_COPLANAR_EPSILON ) {\n\n\t\t\t\t\t\tintersected = true;\n\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tif ( intersected ) {\n\n\t\t\t\t\tlet va = a.geometry.boundsTree.resolveTriangleIndex( ia );\n\t\t\t\t\tlet vb = b.geometry.boundsTree.resolveTriangleIndex( ib );\n\t\t\t\t\taIntersections.add( va, vb );\n\t\t\t\t\tbIntersections.add( vb, va );\n\n\t\t\t\t\tif ( _debugContext ) {\n\n\t\t\t\t\t\t_debugContext.addEdge( _edge );\n\t\t\t\t\t\t_debugContext.addIntersectingTriangles( ia, triangleA, ib, triangleB );\n\n\t\t\t\t\t}\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\tattributeData,\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 attributeData ) {\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 = attributeData[ 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\tattributeData,\n\tinvert = false,\n) {\n\n\tappendAttributeFromIndex( i0, attributes, matrixWorld, normalMatrix, attributeData, invert );\n\tappendAttributeFromIndex( invert ? i2 : i1, attributes, matrixWorld, normalMatrix, attributeData, invert );\n\tappendAttributeFromIndex( invert ? i1 : i2, attributes, matrixWorld, normalMatrix, attributeData, 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 REVERSE_SUBTRACTION:\n\n\t\t\tif ( invert ) {\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} else {\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}\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\n\t\tcase HOLLOW_SUBTRACTION:\n\t\t\tif ( ! invert && ( hitSide === FRONT_SIDE || hitSide === COPLANAR_OPPOSITE ) ) {\n\n\t\t\t\treturn ADD_TRI;\n\n\t\t\t}\n\n\t\t\tbreak;\n\t\tcase HOLLOW_INTERSECTION:\n\t\t\tif ( ! invert && ( hitSide === BACK_SIDE || hitSide === COPLANAR_ALIGNED ) ) {\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\tnormalMa