three-bvh-csg
Version:
A fast, flexible, dynamic CSG implementation on top of three-mesh-bvh
402 lines (252 loc) • 9.45 kB
JavaScript
import { TypeBackedArray } from '../TypeBackedArray.js';
import { Vector3, Vector4, BufferAttribute } from 'three';
const _vec3 = new Vector3();
const _vec3_0 = new Vector3();
const _vec3_1 = new Vector3();
const _vec3_2 = new Vector3();
const _vec4 = new Vector4();
const _vec4_0 = new Vector4();
const _vec4_1 = new Vector4();
const _vec4_2 = new Vector4();
function getBarycoordValue( a, b, c, barycoord, target, normalize = false, invert = false ) {
target.set( 0, 0, 0, 0 )
.addScaledVector( a, barycoord.x )
.addScaledVector( b, barycoord.y )
.addScaledVector( c, barycoord.z );
if ( normalize ) {
target.normalize();
}
if ( invert ) {
target.multiplyScalar( - 1 );
}
return target;
}
function pushItemSize( vec, itemSize, target ) {
switch ( itemSize ) {
case 1:
target.push( vec.x );
break;
case 2:
target.push( vec.x, vec.y );
break;
case 3:
target.push( vec.x, vec.y, vec.z );
break;
case 4:
target.push( vec.x, vec.y, vec.z, vec.w );
break;
}
}
class AttributeData extends TypeBackedArray {
get count() {
return this.length / this.itemSize;
}
constructor( ...args ) {
super( ...args );
this.itemSize = 1;
this.normalized = false;
}
}
export class GeometryBuilder {
constructor() {
this.attributeData = {};
this.groupIndices = [];
this.forwardIndexMap = new Map();
this.invertedIndexMap = new Map();
this.interpolatedFields = {};
}
initFromGeometry( referenceGeometry, relevantAttributes ) {
this.clear();
// initialize and clear unused data from the attribute buffers and vice versa
const { attributeData } = this;
const refAttributes = referenceGeometry.attributes;
for ( let i = 0, l = relevantAttributes.length; i < l; i ++ ) {
const key = relevantAttributes[ i ];
const refAttr = refAttributes[ key ];
const type = refAttr.array.constructor;
if ( ! attributeData[ key ] ) {
attributeData[ key ] = new AttributeData( type );
}
attributeData[ key ].setType( type );
attributeData[ key ].itemSize = refAttr.itemSize;
attributeData[ key ].normalized = refAttr.normalized;
}
for ( const key in attributeData.attributes ) {
if ( ! relevantAttributes.includes( key ) ) {
attributeData.delete( key );
}
}
}
// init and cache all the attribute data for the given indices so we can use it to append interpolated attribute data
initInterpolatedAttributeData( geometry, matrix, normalMatrix, i0, i1, i2 ) {
const { attributeData, interpolatedFields } = this;
const { attributes } = geometry;
for ( const key in attributeData ) {
const attr = attributes[ key ];
if ( ! attr ) {
throw new Error( `CSG Operations: Attribute ${ key } not available on geometry.` );
}
// handle normals and positions specially because they require transforming
let v0, v1, v2;
if ( key === 'position' ) {
v0 = _vec3_0.fromBufferAttribute( attr, i0 ).applyMatrix4( matrix );
v1 = _vec3_1.fromBufferAttribute( attr, i1 ).applyMatrix4( matrix );
v2 = _vec3_2.fromBufferAttribute( attr, i2 ).applyMatrix4( matrix );
} else if ( key === 'normal' ) {
v0 = _vec3_0.fromBufferAttribute( attr, i0 ).applyNormalMatrix( normalMatrix );
v1 = _vec3_1.fromBufferAttribute( attr, i1 ).applyNormalMatrix( normalMatrix );
v2 = _vec3_2.fromBufferAttribute( attr, i2 ).applyNormalMatrix( normalMatrix );
} else if ( key === 'tangent' ) {
v0 = _vec3_0.fromBufferAttribute( attr, i0 ).transformDirection( matrix );
v1 = _vec3_1.fromBufferAttribute( attr, i1 ).transformDirection( matrix );
v2 = _vec3_2.fromBufferAttribute( attr, i2 ).transformDirection( matrix );
} else {
v0 = _vec4_0.fromBufferAttribute( attr, i0 );
v1 = _vec4_1.fromBufferAttribute( attr, i1 );
v2 = _vec4_2.fromBufferAttribute( attr, i2 );
}
if ( ! interpolatedFields[ key ] ) {
interpolatedFields[ key ] = [ v0.clone(), v1.clone(), v2.clone() ];
} else {
const fields = interpolatedFields[ key ];
fields[ 0 ].copy( v0 );
fields[ 1 ].copy( v1 );
fields[ 2 ].copy( v2 );
}
}
}
// push data from the given barycoord onto the geometry
appendInterpolatedAttributeData( group, barycoord, index = null, invert = false ) {
const { groupIndices, attributeData, interpolatedFields, forwardIndexMap, invertedIndexMap } = this;
while ( groupIndices.length <= group ) {
groupIndices.push( new AttributeData( Uint32Array ) );
}
const indexMap = invert ? invertedIndexMap : forwardIndexMap;
const indexData = groupIndices[ group ];
if ( index !== null && indexMap.has( index ) ) {
indexData.push( indexMap.get( index ) );
} else {
indexMap.set( index, attributeData.position.count );
indexData.push( attributeData.position.count );
for ( const key in interpolatedFields ) {
// handle normals and positions specially because they require transforming
const arr = attributeData[ key ];
const isDirection = key === 'normal' || key === 'tangent';
const invertVector = invert && isDirection;
const itemSize = arr.itemSize;
const [ v0, v1, v2 ] = interpolatedFields[ key ];
getBarycoordValue( v0, v1, v2, barycoord, _vec4, isDirection, invertVector );
pushItemSize( _vec4, itemSize, arr );
}
}
}
// append the given vertex index from the source geometry to this one
appendIndexFromGeometry( geometry, matrix, normalMatrix, group, index, invert = false ) {
const { groupIndices, attributeData, forwardIndexMap, invertedIndexMap } = this;
while ( groupIndices.length <= group ) {
groupIndices.push( new AttributeData( Uint32Array ) );
}
const indexMap = invert ? invertedIndexMap : forwardIndexMap;
const indexData = groupIndices[ group ];
if ( index !== null && indexMap.has( index ) ) {
indexData.push( indexMap.get( index ) );
} else {
indexMap.set( index, attributeData.position.count );
indexData.push( attributeData.position.count );
const { attributes } = geometry;
for ( const key in attributeData ) {
const arr = attributeData[ key ];
const attr = attributes[ key ];
if ( ! attr ) {
throw new Error( `CSG Operations: Attribute ${ key } not available on geometry.` );
}
// specially handle the position and normal attributes because they require transforms
const itemSize = attr.itemSize;
if ( key === 'position' ) {
_vec3.fromBufferAttribute( attr, index ).applyMatrix4( matrix );
arr.push( _vec3.x, _vec3.y, _vec3.z );
} else if ( key === 'normal' ) {
_vec3.fromBufferAttribute( attr, index ).applyNormalMatrix( normalMatrix );
if ( invert ) {
_vec3.multiplyScalar( - 1 );
}
arr.push( _vec3.x, _vec3.y, _vec3.z );
} else if ( key === 'tangent' ) {
_vec3.fromBufferAttribute( attr, index ).transformDirection( matrix );
if ( invert ) {
_vec3.multiplyScalar( - 1 );
}
arr.push( _vec3.x, _vec3.y, _vec3.z );
} else {
_vec4.fromBufferAttribute( attr, index );
pushItemSize( _vec4, itemSize, arr );
}
}
}
}
buildGeometry( target, groupOrder ) {
let needsDisposal = false;
const { groupIndices, attributeData } = this;
const { attributes, index } = target;
for ( const key in attributeData ) {
const arr = attributeData[ key ];
const { type, itemSize, normalized, length, count } = arr;
const buffer = arr.array.buffer;
let attr = attributes[ key ];
if ( ! attr || attr.count < count || attr.array.type !== type ) {
// create the attribute if it doesn't exist yet
attr = new BufferAttribute( new type( length ), itemSize, normalized );
target.setAttribute( key, attr );
needsDisposal = true;
}
// copy the data
attr.array.set( new type( buffer, 0, length ), 0 );
attr.needsUpdate = true;
}
// remove or update the index appropriately
const indexCount = groupIndices.reduce( ( v, arr ) => arr.count + v, 0 );
if ( ! target.index || index.count < indexCount || index.array.type !== Uint32Array ) {
target.setIndex( new BufferAttribute( new Uint32Array( indexCount ), 1 ) );
needsDisposal = true;
}
// initialize the groups
target.clearGroups();
let offset = 0;
for ( let i = 0, l = Math.min( groupOrder.length, groupIndices.length ); i < l; i ++ ) {
const { index, materialIndex } = groupOrder[ i ];
const { count } = groupIndices[ index ];
const buffer = groupIndices[ index ].array.buffer;
if ( count !== 0 ) {
target.index.array.set( new Uint32Array( buffer, 0, count ), offset );
target.addGroup( offset, count, materialIndex );
offset += count;
}
}
// update the draw range
target.setDrawRange( 0, offset );
// remove the bounds tree if it exists because its now out of date
// TODO: can we have this dispose in the same way that a brush does?
// TODO: why are half edges and group indices not removed here?
target.boundsTree = null;
target.boundingBox = null;
target.boundingSphere = null;
if ( needsDisposal ) {
target.dispose();
}
}
clearIndexMap() {
this.forwardIndexMap.clear();
this.invertedIndexMap.clear();
}
clear() {
const { groupIndices, attributeData } = this;
this.interpolatedFields = {};
for ( const key in attributeData ) {
attributeData[ key ].clear();
}
groupIndices.forEach( arr => {
arr.clear();
} );
this.clearIndexMap();
}
}