@@ -272,6 +272,124 @@ function resamplePolyline3(points, sampleCount) {
272272 return out ;
273273}
274274
275+ function pointDistanceToAxis ( point , axisPoint , axisDir ) {
276+ if ( ! point || ! axisPoint || ! axisDir ) return null ;
277+ const offset = point . clone ( ) . sub ( axisPoint ) ;
278+ const axial = axisDir . clone ( ) . multiplyScalar ( offset . dot ( axisDir ) ) ;
279+ return offset . sub ( axial ) . length ( ) ;
280+ }
281+
282+ function firstPointOnFaceGrid ( points ) {
283+ if ( ! Array . isArray ( points ) ) return null ;
284+ for ( const point of points ) {
285+ if ( ! Array . isArray ( point ) || point . length < 3 ) continue ;
286+ return new THREE . Vector3 ( point [ 0 ] , point [ 1 ] , point [ 2 ] ) ;
287+ }
288+ return null ;
289+ }
290+
291+ function buildBendCylindricalMetadata ( { facePoints, axisStart, axisEnd, axisDir, fallbackRadius = null } ) {
292+ if ( ! axisStart ?. isVector3 || ! axisEnd ?. isVector3 || ! axisDir ?. isVector3 ) return null ;
293+ const axisCenter = axisStart . clone ( ) . add ( axisEnd ) . multiplyScalar ( 0.5 ) ;
294+ const samplePoint = firstPointOnFaceGrid ( facePoints ) ;
295+ const sampledRadius = samplePoint ? pointDistanceToAxis ( samplePoint , axisStart , axisDir ) : null ;
296+ const radius = Number . isFinite ( sampledRadius ) ? sampledRadius : fallbackRadius ;
297+ const height = axisStart . distanceTo ( axisEnd ) ;
298+ if ( ! Number . isFinite ( radius ) || radius <= EPS ) return null ;
299+ return {
300+ type : "cylindrical" ,
301+ radius,
302+ height : Number . isFinite ( height ) ? height : 0 ,
303+ axis : [ axisDir . x , axisDir . y , axisDir . z ] ,
304+ center : [ axisCenter . x , axisCenter . y , axisCenter . z ] ,
305+ } ;
306+ }
307+
308+ function circumcenter2 ( a , b , c ) {
309+ const ax = toFiniteNumber ( a ?. [ 0 ] ) ;
310+ const ay = toFiniteNumber ( a ?. [ 1 ] ) ;
311+ const bx = toFiniteNumber ( b ?. [ 0 ] ) ;
312+ const by = toFiniteNumber ( b ?. [ 1 ] ) ;
313+ const cx = toFiniteNumber ( c ?. [ 0 ] ) ;
314+ const cy = toFiniteNumber ( c ?. [ 1 ] ) ;
315+ const d = 2 * ( ( ax * ( by - cy ) ) + ( bx * ( cy - ay ) ) + ( cx * ( ay - by ) ) ) ;
316+ if ( Math . abs ( d ) <= EPS ) return null ;
317+
318+ const aSq = ( ax * ax ) + ( ay * ay ) ;
319+ const bSq = ( bx * bx ) + ( by * by ) ;
320+ const cSq = ( cx * cx ) + ( cy * cy ) ;
321+ return [
322+ ( ( aSq * ( by - cy ) ) + ( bSq * ( cy - ay ) ) + ( cSq * ( ay - by ) ) ) / d ,
323+ ( ( aSq * ( cx - bx ) ) + ( bSq * ( ax - cx ) ) + ( cSq * ( bx - ax ) ) ) / d ,
324+ ] ;
325+ }
326+
327+ function fitCircularEdgePolyline2 ( polyline ) {
328+ const points = Array . isArray ( polyline ) ? polyline : [ ] ;
329+ if ( points . length < 3 ) return null ;
330+ const first = points [ 0 ] ;
331+ const mid = points [ ( points . length / 2 ) | 0 ] ;
332+ const last = points [ points . length - 1 ] ;
333+ const center = circumcenter2 ( first , mid , last ) ;
334+ if ( ! center ) return null ;
335+
336+ const radius = Math . hypot ( first [ 0 ] - center [ 0 ] , first [ 1 ] - center [ 1 ] ) ;
337+ if ( ! ( Number . isFinite ( radius ) && radius > EPS ) ) return null ;
338+
339+ const startAngle = Math . atan2 ( first [ 1 ] - center [ 1 ] , first [ 0 ] - center [ 0 ] ) ;
340+ const endAngle = Math . atan2 ( last [ 1 ] - center [ 1 ] , last [ 0 ] - center [ 0 ] ) ;
341+ let delta = endAngle - startAngle ;
342+ while ( delta <= - Math . PI ) delta += Math . PI * 2 ;
343+ while ( delta > Math . PI ) delta -= Math . PI * 2 ;
344+ if ( Math . abs ( delta ) <= THREE . MathUtils . degToRad ( 1 ) ) return null ;
345+
346+ const tolerance = Math . max ( 1e-4 , radius * 1e-3 ) ;
347+ for ( const point of points ) {
348+ const sampleRadius = Math . hypot ( point [ 0 ] - center [ 0 ] , point [ 1 ] - center [ 1 ] ) ;
349+ if ( Math . abs ( sampleRadius - radius ) > tolerance ) return null ;
350+ }
351+
352+ return { center, radius } ;
353+ }
354+
355+ function buildFlatWallCylindricalMetadata ( { edge, placementMatrix, thickness } ) {
356+ const fit = fitCircularEdgePolyline2 ( edge ?. polyline ) ;
357+ if ( ! fit ) return null ;
358+ const axisDir = new THREE . Vector3 ( 0 , 0 , 1 ) . transformDirection ( placementMatrix ) . normalize ( ) ;
359+ const axisCenter = makeMidplaneWorldPoint ( placementMatrix , fit . center ) ;
360+ const height = Math . abs ( toFiniteNumber ( thickness , 0 ) ) ;
361+ if ( ! ( height > EPS ) ) return null ;
362+ return {
363+ type : "cylindrical" ,
364+ radius : fit . radius ,
365+ height,
366+ axis : [ axisDir . x , axisDir . y , axisDir . z ] ,
367+ center : [ axisCenter . x , axisCenter . y , axisCenter . z ] ,
368+ } ;
369+ }
370+
371+ function addCylindricalFaceCenterline ( { solid, faceName, metadata } ) {
372+ if ( ! solid || ! faceName || ! metadata || metadata . type !== "cylindrical" ) return ;
373+ const axis = Array . isArray ( metadata . axis ) ? metadata . axis : null ;
374+ const center = Array . isArray ( metadata . center ) ? metadata . center : null ;
375+ const height = Math . abs ( toFiniteNumber ( metadata . height , 0 ) ) ;
376+ if ( ! axis || axis . length !== 3 || ! center || center . length !== 3 || ! ( height > EPS ) ) return ;
377+
378+ const axisDir = new THREE . Vector3 ( axis [ 0 ] , axis [ 1 ] , axis [ 2 ] ) ;
379+ if ( axisDir . lengthSq ( ) <= EPS ) return ;
380+ axisDir . normalize ( ) ;
381+
382+ const axisCenter = new THREE . Vector3 ( center [ 0 ] , center [ 1 ] , center [ 2 ] ) ;
383+ const halfHeight = height * 0.5 ;
384+ const start = axisCenter . clone ( ) . addScaledVector ( axisDir , - halfHeight ) ;
385+ const end = axisCenter . clone ( ) . addScaledVector ( axisDir , halfHeight ) ;
386+ solid . addAuxEdge ( `${ faceName } :CENTERLINE` , [ quantizePoint3 ( start ) , quantizePoint3 ( end ) ] , {
387+ centerline : true ,
388+ materialKey : "OVERLAY" ,
389+ polylineWorld : false ,
390+ } ) ;
391+ }
392+
275393function buildBendLookup ( tree ) {
276394 const lookup = new Map ( ) ;
277395 const visitFlat = ( flat ) => {
@@ -408,7 +526,13 @@ function addFlatPlacementToSolid({ solid, placement, featureID, thickness, edgeC
408526 const topB = outerOffset + next ;
409527 addTriangleIfValid ( solid , sideFace , topPoints [ topA ] , bottomPoints [ topA ] , topPoints [ topB ] ) ;
410528 addTriangleIfValid ( solid , sideFace , topPoints [ topB ] , bottomPoints [ topA ] , bottomPoints [ topB ] ) ;
529+ const cylindricalMeta = buildFlatWallCylindricalMetadata ( {
530+ edge : mappedEdge ,
531+ placementMatrix : placement . matrix ,
532+ thickness,
533+ } ) ;
411534 solid . setFaceMetadata ( sideFace , {
535+ ...( cylindricalMeta || { } ) ,
412536 flatId : flat . id ,
413537 edgeId,
414538 edgeSignature : signature || null ,
@@ -420,6 +544,7 @@ function addFlatPlacementToSolid({ solid, placement, featureID, thickness, edgeC
420544 edgeSignature : signature || null ,
421545 } ,
422546 } ) ;
547+ if ( cylindricalMeta ) addCylindricalFaceCenterline ( { solid, faceName : sideFace , metadata : cylindricalMeta } ) ;
423548 }
424549
425550 for ( let holeIndex = 0 ; holeIndex < holeEntries . length ; holeIndex += 1 ) {
@@ -520,7 +645,9 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
520645 const axis = bendPlacement . axisEnd . clone ( ) . sub ( bendPlacement . axisStart ) ;
521646 if ( axis . lengthSq ( ) <= EPS ) return ;
522647 const axisDir = axis . normalize ( ) ;
523- const axisOrigin = bendPlacement . axisStart . clone ( ) ;
648+ const axisStart = bendPlacement . axisStart . clone ( ) ;
649+ const axisEnd = bendPlacement . axisEnd . clone ( ) ;
650+ const axisOrigin = axisStart . clone ( ) ;
524651 const sampleCount = Math . max ( 2 , parentEdgeWorld . length , childEdgeWorld . length ) ;
525652 const parentEdge = resamplePolyline3 ( parentEdgeWorld , sampleCount ) ;
526653 const childEdge = resamplePolyline3 ( childEdgeWorld , sampleCount ) ;
@@ -561,6 +688,24 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
561688
562689 const lookup = bendLookup . get ( String ( bend . id ) ) || { } ;
563690 const parentEdgeId = lookup ?. parentEdgeId ?? null ;
691+ const midRadius = Math . max ( EPS , toFiniteNumber ( bendPlacement . midRadius , 0 ) ) ;
692+ const fallbackOuterRadius = midRadius + halfT ;
693+ const fallbackInnerRadius = Math . max ( EPS , midRadius - halfT ) ;
694+ const isFaceAOuter = bendPlacement . angleRad >= 0 ;
695+ const faceAMetadata = buildBendCylindricalMetadata ( {
696+ facePoints : top ,
697+ axisStart,
698+ axisEnd,
699+ axisDir,
700+ fallbackRadius : isFaceAOuter ? fallbackOuterRadius : fallbackInnerRadius ,
701+ } ) ;
702+ const faceBMetadata = buildBendCylindricalMetadata ( {
703+ facePoints : bottom ,
704+ axisStart,
705+ axisEnd,
706+ axisDir,
707+ fallbackRadius : isFaceAOuter ? fallbackInnerRadius : fallbackOuterRadius ,
708+ } ) ;
564709
565710 const faceA = makeBendFaceName ( featureID , bend . id , "A" ) ;
566711 const faceB = makeBendFaceName ( featureID , bend . id , "B" ) ;
@@ -602,6 +747,7 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
602747 }
603748
604749 solid . setFaceMetadata ( faceA , {
750+ ...( faceAMetadata || { } ) ,
605751 bendId : bend . id ,
606752 sheetMetalFaceType : "A" ,
607753 sheetMetal : {
@@ -616,6 +762,7 @@ function addBendPlacementToSolid({ solid, bendPlacement, featureID, thickness, b
616762 } ,
617763 } ) ;
618764 solid . setFaceMetadata ( faceB , {
765+ ...( faceBMetadata || { } ) ,
619766 bendId : bend . id ,
620767 sheetMetalFaceType : "B" ,
621768 sheetMetal : {
0 commit comments