@@ -402,44 +402,51 @@ function buildHullTube(points, radius, resolution, closed, keepSpheres = false,
402402 return { manifold : hull , spheres } ;
403403}
404404
405- function rebuildSolidFromManifold ( target , manifold , faceMap ) {
406- const rebuilt = Solid . _fromManifold ( manifold , faceMap ) ;
407-
408- // Copy authoring buffers and metadata without clobbering THREE.Object3D fields
409- target . _numProp = rebuilt . _numProp ;
410- target . _vertProperties = rebuilt . _vertProperties ;
411- target . _triVerts = rebuilt . _triVerts ;
412- target . _triIDs = rebuilt . _triIDs ;
413- target . _vertKeyToIndex = new Map ( rebuilt . _vertKeyToIndex ) ;
414-
415- target . _idToFaceName = new Map ( rebuilt . _idToFaceName ) ;
416- target . _faceNameToID = new Map ( rebuilt . _faceNameToID ) ;
417- target . _faceMetadata = new Map ( rebuilt . _faceMetadata ) ;
418- target . _edgeMetadata = new Map ( rebuilt . _edgeMetadata ) ;
419-
420- target . _manifold = rebuilt . _manifold ;
421- target . _dirty = false ;
422- target . _faceIndex = null ;
423- return target ;
424- }
425-
426- function distanceToSegmentSquared ( p , a , b ) {
427- const ab = b . clone ( ) . sub ( a ) ;
428- const ap = p . clone ( ) . sub ( a ) ;
429- const t = THREE . MathUtils . clamp ( ap . dot ( ab ) / ab . lengthSq ( ) , 0 , 1 ) ;
430- const closest = a . clone ( ) . addScaledVector ( ab , t ) ;
431- return p . distanceToSquared ( closest ) ;
432- }
433-
434- function minDistanceToPolyline ( points , polyline ) {
435- if ( ! Array . isArray ( polyline ) || polyline . length < 2 ) return Infinity ;
436- let minSq = Infinity ;
405+ function buildPolylineSegmentCache ( polyline ) {
406+ if ( ! Array . isArray ( polyline ) || polyline . length < 2 ) return [ ] ;
407+ const segments = [ ] ;
437408 for ( let i = 0 ; i < polyline . length - 1 ; i ++ ) {
438409 const a = polyline [ i ] ;
439410 const b = polyline [ i + 1 ] ;
440- minSq = Math . min ( minSq , distanceToSegmentSquared ( points , a , b ) ) ;
411+ if ( ! a || ! b ) continue ;
412+ const ax = a . x ;
413+ const ay = a . y ;
414+ const az = a . z ;
415+ const dx = b . x - ax ;
416+ const dy = b . y - ay ;
417+ const dz = b . z - az ;
418+ const lenSq = dx * dx + dy * dy + dz * dz ;
419+ segments . push ( { ax, ay, az, dx, dy, dz, lenSq } ) ;
420+ }
421+ return segments ;
422+ }
423+
424+ function minDistanceToPolylineSq ( px , py , pz , segments , breakAtSq = Infinity ) {
425+ if ( ! Array . isArray ( segments ) || segments . length === 0 ) return Infinity ;
426+ let minSq = Infinity ;
427+ for ( let i = 0 ; i < segments . length ; i ++ ) {
428+ const seg = segments [ i ] ;
429+ if ( ! seg ) continue ;
430+ if ( seg . lenSq <= EPS_SQ ) continue ;
431+ const apx = px - seg . ax ;
432+ const apy = py - seg . ay ;
433+ const apz = pz - seg . az ;
434+ let t = ( apx * seg . dx + apy * seg . dy + apz * seg . dz ) / seg . lenSq ;
435+ if ( t < 0 ) t = 0 ;
436+ else if ( t > 1 ) t = 1 ;
437+ const cx = seg . ax + ( seg . dx * t ) ;
438+ const cy = seg . ay + ( seg . dy * t ) ;
439+ const cz = seg . az + ( seg . dz * t ) ;
440+ const ddx = px - cx ;
441+ const ddy = py - cy ;
442+ const ddz = pz - cz ;
443+ const distSq = ddx * ddx + ddy * ddy + ddz * ddz ;
444+ if ( distSq < minSq ) {
445+ minSq = distSq ;
446+ if ( minSq <= breakAtSq ) break ;
447+ }
441448 }
442- return Math . sqrt ( minSq ) ;
449+ return minSq ;
443450}
444451
445452function relabelFaces ( solid , pathPoints , startNormal , endNormal , outerRadius , innerRadius , closed , faceTag ) {
@@ -455,9 +462,12 @@ function relabelFaces(solid, pathPoints, startNormal, endNormal, outerRadius, in
455462
456463 const nStart = startNormal ? startNormal . clone ( ) . normalize ( ) : null ;
457464 const nEnd = endNormal ? endNormal . clone ( ) . normalize ( ) : null ;
458- const startOffset = nStart ? nStart . dot ( pathPoints [ 0 ] ) : 0 ;
459- const endOffset = nEnd ? nEnd . dot ( pathPoints [ pathPoints . length - 1 ] ) : 0 ;
465+ const pathStart = pathPoints [ 0 ] ;
466+ const pathEnd = pathPoints [ pathPoints . length - 1 ] ;
467+ const startOffset = nStart ? nStart . dot ( pathStart ) : 0 ;
468+ const endOffset = nEnd ? nEnd . dot ( pathEnd ) : 0 ;
460469 const capTol = Math . max ( outerRadius * 1e-2 , 1e-5 ) ;
470+ const capReachSq = ( outerRadius + capTol ) * ( outerRadius + capTol ) ;
461471
462472 const idOuter = solid . _getOrCreateID ( `${ faceTag } _Outer` ) ;
463473 const idInner = innerRadius > 0 ? solid . _getOrCreateID ( `${ faceTag } _Inner` ) : idOuter ;
@@ -467,8 +477,15 @@ function relabelFaces(solid, pathPoints, startNormal, endNormal, outerRadius, in
467477 const newIDs = new Array ( triCount ) ;
468478 const vp = solid . _vertProperties ;
469479 const tv = solid . _triVerts ;
470- const polyline = pathPoints ;
471480 const innerOuterThreshold = innerRadius > 0 ? ( innerRadius + outerRadius ) * 0.5 : outerRadius * 0.5 ;
481+ const innerOuterThresholdSq = innerOuterThreshold * innerOuterThreshold ;
482+ const segmentCache = innerRadius > 0 ? buildPolylineSegmentCache ( pathPoints ) : null ;
483+ const nStartX = nStart ? nStart . x : 0 ;
484+ const nStartY = nStart ? nStart . y : 0 ;
485+ const nStartZ = nStart ? nStart . z : 0 ;
486+ const nEndX = nEnd ? nEnd . x : 0 ;
487+ const nEndY = nEnd ? nEnd . y : 0 ;
488+ const nEndZ = nEnd ? nEnd . z : 0 ;
472489
473490 for ( let t = 0 ; t < triCount ; t ++ ) {
474491 const i0 = tv [ t * 3 + 0 ] * 3 ;
@@ -477,18 +494,36 @@ function relabelFaces(solid, pathPoints, startNormal, endNormal, outerRadius, in
477494 const cx = ( vp [ i0 + 0 ] + vp [ i1 + 0 ] + vp [ i2 + 0 ] ) / 3 ;
478495 const cy = ( vp [ i0 + 1 ] + vp [ i1 + 1 ] + vp [ i2 + 1 ] ) / 3 ;
479496 const cz = ( vp [ i0 + 2 ] + vp [ i1 + 2 ] + vp [ i2 + 2 ] ) / 3 ;
480- const centroid = new THREE . Vector3 ( cx , cy , cz ) ;
481497
482498 let assigned = idOuter ;
483- const distToStart = centroid . distanceTo ( pathPoints [ 0 ] ) ;
484- const distToEnd = centroid . distanceTo ( pathPoints [ pathPoints . length - 1 ] ) ;
485- if ( ! closed && nStart && Math . abs ( nStart . dot ( centroid ) - startOffset ) <= capTol && distToStart <= outerRadius + capTol ) {
486- assigned = idCapStart ;
487- } else if ( ! closed && nEnd && Math . abs ( nEnd . dot ( centroid ) - endOffset ) <= capTol && distToEnd <= outerRadius + capTol ) {
488- assigned = idCapEnd ;
489- } else if ( innerRadius > 0 ) {
490- const dist = minDistanceToPolyline ( centroid , polyline ) ;
491- assigned = dist <= innerOuterThreshold ? idInner : idOuter ;
499+ if ( ! closed && nStart ) {
500+ const planeDistStart = Math . abs ( ( nStartX * cx + nStartY * cy + nStartZ * cz ) - startOffset ) ;
501+ if ( planeDistStart <= capTol ) {
502+ const dsx = cx - pathStart . x ;
503+ const dsy = cy - pathStart . y ;
504+ const dsz = cz - pathStart . z ;
505+ if ( ( dsx * dsx + dsy * dsy + dsz * dsz ) <= capReachSq ) {
506+ assigned = idCapStart ;
507+ }
508+ }
509+ }
510+ if ( assigned === idOuter && ! closed && nEnd ) {
511+ const planeDistEnd = Math . abs ( ( nEndX * cx + nEndY * cy + nEndZ * cz ) - endOffset ) ;
512+ if ( planeDistEnd <= capTol ) {
513+ const dex = cx - pathEnd . x ;
514+ const dey = cy - pathEnd . y ;
515+ const dez = cz - pathEnd . z ;
516+ if ( ( dex * dex + dey * dey + dez * dez ) <= capReachSq ) {
517+ assigned = idCapEnd ;
518+ }
519+ }
520+ }
521+ if ( assigned === idOuter && innerRadius > 0 ) {
522+ const distSq = minDistanceToPolylineSq ( cx , cy , cz , segmentCache , innerOuterThresholdSq ) ;
523+ assigned = distSq <= innerOuterThresholdSq ? idInner : idOuter ;
524+ }
525+ if ( assigned !== idOuter && assigned !== idInner && assigned !== idCapStart && assigned !== idCapEnd ) {
526+ assigned = idOuter ;
492527 }
493528 newIDs [ t ] = assigned ;
494529 }
@@ -824,13 +859,18 @@ export class Tube extends Solid {
824859
825860 if ( inner > 0 ) {
826861 const { manifold : innerManifold , spheres : innerSpheres } = buildHullTube ( cleanPoints , inner , segs , isClosed , keepSpheres , trimPlanes ) ;
827-
828- const outerSolid = Solid . _fromManifold ( outerManifold , new Map ( [ [ 0 , `${ faceTag } _Outer` ] ] ) ) ;
829- const innerSolid = Solid . _fromManifold ( innerManifold , new Map ( [ [ 0 , `${ faceTag } _Inner` ] ] ) ) ;
830- finalSolid = outerSolid . subtract ( innerSolid ) ;
831- try { outerSolid . free ( ) ; } catch { }
832- try { innerSolid . free ( ) ; } catch { }
833- try { if ( innerManifold && typeof innerManifold . delete === 'function' ) innerManifold . delete ( ) ; } catch { }
862+ let differenceManifold = null ;
863+ try {
864+ differenceManifold = outerManifold . subtract ( innerManifold ) ;
865+ } finally {
866+ if ( outerManifold && outerManifold !== differenceManifold ) {
867+ try { if ( typeof outerManifold . delete === 'function' ) outerManifold . delete ( ) ; } catch { }
868+ }
869+ if ( innerManifold && innerManifold !== differenceManifold ) {
870+ try { if ( typeof innerManifold . delete === 'function' ) innerManifold . delete ( ) ; } catch { }
871+ }
872+ }
873+ finalSolid = Solid . _fromManifold ( differenceManifold , new Map ( [ [ 0 , `${ faceTag } _Outer` ] ] ) ) ;
834874 if ( keepSpheres ) {
835875 this . debugSphereSolids = [
836876 ...( this . debugSphereSolids || [ ] ) ,
@@ -845,11 +885,8 @@ export class Tube extends Solid {
845885 }
846886 }
847887
848- let relabeled = relabelFaces ( finalSolid , cleanPoints , startNormal , endCutNormal , radius , inner , isClosed , faceTag ) ;
849- // Ensure we have a manifold to copy from; if rebuild failed, fall back
850- const manifoldForCopy = relabeled ?. _manifold || finalSolid . _manifold ;
851- const faceMapForCopy = relabeled ?. _idToFaceName || finalSolid . _idToFaceName ;
852- rebuildSolidFromManifold ( this , manifoldForCopy , faceMapForCopy ) ;
888+ const relabeled = relabelFaces ( finalSolid , cleanPoints , startNormal , endCutNormal , radius , inner , isClosed , faceTag ) ;
889+ copySolidState ( this , relabeled || finalSolid ) ;
853890 this . name = name ;
854891 this . params . closed = isClosed ;
855892
0 commit comments