three-bvh-csg
Version:
A fast, flexible, dynamic CSG implementation on top of three-mesh-bvh
182 lines (117 loc) • 4.28 kB
JavaScript
import { Line3, Vector3, Plane } from 'three';
// tolerance for considering a clipped segment degenerate (zero-length)
const CLIP_EPSILON = 1e-10;
// tolerance for treating a denominator as zero (segment parallel to edge)
const PARALLEL_EPSILON = 1e-15;
// tolerance for considering two triangle normals as parallel
const COPLANAR_NORMAL_EPSILON = 1e-10;
// tolerance for considering two parallel triangles as lying on the same plane
const COPLANAR_DISTANCE_EPSILON = 1e-10;
const _tempLine = new Line3();
const _inputSeg = new Line3();
const _dir = new Vector3();
const _edgeDelta = new Vector3();
const _edgeNormal = new Vector3();
const _edgePlane = new Plane();
const _normalA = new Vector3();
const _normalB = new Vector3();
// returns true if two triangles are coplanar (parallel normals and same plane distance)
export function isTriangleCoplanar( triA, triB ) {
triA.getNormal( _normalA );
triB.getNormal( _normalB );
const dot = _normalA.dot( _normalB );
if ( Math.abs( 1.0 - Math.abs( dot ) ) >= COPLANAR_NORMAL_EPSILON ) {
return false;
}
// test if plane constant is within tolerance
const dA = _normalA.dot( triA.a );
const dB = _normalA.dot( triB.a );
return Math.abs( dA - dB ) < COPLANAR_DISTANCE_EPSILON;
}
// Clips a line segment to the interior of a coplanar triangle using the Cyrus–Beck algorithm
// generalized to 3D half-planes.
// Reference: Cyrus & Beck, "Generalized two- and three-dimensional clipping"
// Returns the target Line3 with clipped endpoints, or null if entirely outside.
function clipSegmentToTriangle( segment, tri, normal, target ) {
let tMin = 0;
let tMax = 1;
segment.delta( _dir );
const verts = [ tri.a, tri.b, tri.c ];
for ( let i = 0; i < 3; i ++ ) {
const v0 = verts[ i ];
const v1 = verts[ ( i + 1 ) % 3 ];
// build the inward-facing edge plane
_edgeDelta.subVectors( v1, v0 );
_edgeNormal.crossVectors( normal, _edgeDelta );
_edgePlane.setFromNormalAndCoplanarPoint( _edgeNormal, v0 );
// signed distance of segment start from the edge plane
const dist = _edgePlane.distanceToPoint( segment.start );
// rate of change of distance along segment direction
const denom = _edgePlane.normal.dot( _dir );
if ( Math.abs( denom ) < PARALLEL_EPSILON ) {
// segment parallel to edge — entirely inside or outside this half-plane
if ( dist < - CLIP_EPSILON ) {
return null;
} else {
continue;
}
}
const t = - dist / denom;
if ( denom > 0 ) {
// segment enters the plane at t from the negative side
tMin = Math.max( tMin, t );
} else {
// segment exits the plane at t from the positive side
tMax = Math.min( tMax, t );
}
// edge is outside the triangle
if ( tMin > tMax + CLIP_EPSILON ) {
return null;
}
}
// segment is degenerate
if ( tMax - tMin < CLIP_EPSILON ) {
return null;
}
segment.at( tMin, target.start );
segment.at( tMax, target.end );
return target;
}
// Computes the edges of the intersection polygon between two coplanar triangles.
// The boundary consists of segments from both triangles' edges clipped to the other's interior.
// Returns the number of segments written into target.
export function getCoplanarIntersectionEdges( triA, triB, target ) {
let count = 0;
triA.getNormal( _normalA );
triB.getNormal( _normalB );
// clip triB's edges against triA
const bVerts = [ triB.a, triB.b, triB.c ];
for ( let i = 0; i < 3; i ++ ) {
_inputSeg.start.copy( bVerts[ i ] );
_inputSeg.end.copy( bVerts[ ( i + 1 ) % 3 ] );
const result = clipSegmentToTriangle( _inputSeg, triA, _normalA, _tempLine );
if ( result !== null ) {
if ( count >= target.length ) {
target.push( new Line3() );
}
target[ count ].copy( result );
count ++;
}
}
// clip triA's edges against triB
const aVerts = [ triA.a, triA.b, triA.c ];
for ( let i = 0; i < 3; i ++ ) {
_inputSeg.start.copy( aVerts[ i ] );
_inputSeg.end.copy( aVerts[ ( i + 1 ) % 3 ] );
const result = clipSegmentToTriangle( _inputSeg, triB, _normalB, _tempLine );
if ( result !== null ) {
if ( count >= target.length ) {
target.push( new Line3() );
}
target[ count ].copy( result );
count ++;
}
}
// returns the number of segments generated
return count;
}