33 BufferGeometry ,
44 ClampToEdgeWrapping ,
55 Color ,
6+ CylinderGeometry ,
67 FileLoader ,
78 Float32BufferAttribute ,
89 Group ,
@@ -15,14 +16,21 @@ import {
1516 MeshStandardMaterial ,
1617 MirroredRepeatWrapping ,
1718 NearestFilter ,
19+ Quaternion ,
1820 RepeatWrapping ,
21+ SphereGeometry ,
1922 TextureLoader ,
20- SRGBColorSpace
23+ SRGBColorSpace ,
24+ Vector3
2125} from 'three' ;
2226import { unzipSync } from '../libs/fflate.module.js' ;
27+ import { mergeGeometries } from '../utils/BufferGeometryUtils.js' ;
2328
2429const COLOR_SPACE_3MF = SRGBColorSpace ;
2530
31+ const NS_BEAM_LATTICE = 'http://schemas.microsoft.com/3dmanufacturing/beamlattice/2017/02' ;
32+ const NS_BEAM_LATTICE_BALLS = 'http://schemas.microsoft.com/3dmanufacturing/beamlattice/balls/2020/07' ;
33+
2634/**
2735 * A loader for the [3D Manufacturing Format (3MF)](https://3mf.io/specification/) format.
2836 *
@@ -38,6 +46,7 @@ const COLOR_SPACE_3MF = SRGBColorSpace;
3846 * - Texture 2D Groups
3947 * - Color Groups (Vertex Colors)
4048 * - Metallic Display Properties (PBR)
49+ * - Beam Lattice
4150 *
4251 * ```js
4352 * const loader = new ThreeMFLoader();
@@ -604,10 +613,87 @@ class ThreeMFLoader extends Loader {
604613 meshData [ 'triangleProperties' ] = triangleProperties ;
605614 meshData [ 'triangles' ] = new Uint32Array ( triangles ) ;
606615
616+ const beamLatticeNode = meshNode . getElementsByTagNameNS ( NS_BEAM_LATTICE , 'beamlattice' ) [ 0 ] ;
617+
618+ if ( beamLatticeNode !== undefined ) {
619+
620+ meshData [ 'beamlattice' ] = parseBeamLatticeNode ( beamLatticeNode ) ;
621+
622+ }
623+
607624 return meshData ;
608625
609626 }
610627
628+ function parseBeamLatticeNode ( beamLatticeNode ) {
629+
630+ const beamLatticeData = {
631+ radius : parseFloat ( beamLatticeNode . getAttribute ( 'radius' ) ) , // required
632+ minLength : parseFloat ( beamLatticeNode . getAttribute ( 'minlength' ) ) ,
633+ cap : beamLatticeNode . getAttribute ( 'cap' ) || 'sphere' ,
634+ ballMode : beamLatticeNode . getAttributeNS ( NS_BEAM_LATTICE_BALLS , 'ballmode' ) || 'none' ,
635+ ballRadius : parseFloat ( beamLatticeNode . getAttributeNS ( NS_BEAM_LATTICE_BALLS , 'ballradius' ) ) ,
636+ beams : [ ] ,
637+ balls : [ ]
638+ } ;
639+
640+ // clipping requires boolean operations which are not supported ('clipping' is a legacy name of 'clippingmode')
641+
642+ const clippingMode = beamLatticeNode . getAttribute ( 'clippingmode' ) || beamLatticeNode . getAttribute ( 'clipping' ) ;
643+
644+ if ( clippingMode !== null && clippingMode !== 'none' ) {
645+
646+ console . warn ( 'THREE.3MFLoader: Beam lattice clipping is not supported. The lattice is rendered unclipped.' ) ;
647+
648+ }
649+
650+ const beamNodes = beamLatticeNode . getElementsByTagNameNS ( NS_BEAM_LATTICE , 'beam' ) ;
651+
652+ for ( let i = 0 ; i < beamNodes . length ; i ++ ) {
653+
654+ const beamNode = beamNodes [ i ] ;
655+
656+ const beamData = {
657+ v1 : parseInt ( beamNode . getAttribute ( 'v1' ) , 10 ) , // required
658+ v2 : parseInt ( beamNode . getAttribute ( 'v2' ) , 10 ) // required
659+ } ;
660+
661+ // optional
662+
663+ const r1 = beamNode . getAttribute ( 'r1' ) ;
664+ const r2 = beamNode . getAttribute ( 'r2' ) ;
665+
666+ if ( r1 !== null ) beamData [ 'r1' ] = parseFloat ( r1 ) ;
667+ if ( r2 !== null ) beamData [ 'r2' ] = parseFloat ( r2 ) ;
668+
669+ beamLatticeData . beams . push ( beamData ) ;
670+
671+ }
672+
673+ const ballNodes = beamLatticeNode . getElementsByTagNameNS ( NS_BEAM_LATTICE_BALLS , 'ball' ) ;
674+
675+ for ( let i = 0 ; i < ballNodes . length ; i ++ ) {
676+
677+ const ballNode = ballNodes [ i ] ;
678+
679+ const ballData = {
680+ vindex : parseInt ( ballNode . getAttribute ( 'vindex' ) , 10 ) // required
681+ } ;
682+
683+ // optional
684+
685+ const r = ballNode . getAttribute ( 'r' ) ;
686+
687+ if ( r !== null ) ballData [ 'r' ] = parseFloat ( r ) ;
688+
689+ beamLatticeData . balls . push ( ballData ) ;
690+
691+ }
692+
693+ return beamLatticeData ;
694+
695+ }
696+
611697 function parseComponentsNode ( componentsNode ) {
612698
613699 const components = [ ] ;
@@ -1211,6 +1297,111 @@ class ThreeMFLoader extends Loader {
12111297
12121298 }
12131299
1300+ function buildLatticeMesh ( meshData ) {
1301+
1302+ const beamLatticeData = meshData [ 'beamlattice' ] ;
1303+ const vertices = meshData [ 'vertices' ] ;
1304+
1305+ const geometries = [ ] ;
1306+
1307+ const p1 = new Vector3 ( ) ;
1308+ const p2 = new Vector3 ( ) ;
1309+ const direction = new Vector3 ( ) ;
1310+ const up = new Vector3 ( 0 , 1 , 0 ) ;
1311+ const quaternion = new Quaternion ( ) ;
1312+ const matrix = new Matrix4 ( ) ;
1313+
1314+ // beams
1315+
1316+ const beams = beamLatticeData . beams ;
1317+ const openEnded = beamLatticeData . cap !== 'butt' ;
1318+
1319+ // spheres for caps and balls are collected per vertex so joints are only built once
1320+
1321+ const sphereRadii = new Map ( ) ;
1322+
1323+ for ( let i = 0 ; i < beams . length ; i ++ ) {
1324+
1325+ const beam = beams [ i ] ;
1326+
1327+ p1 . fromArray ( vertices , beam . v1 * 3 ) ;
1328+ p2 . fromArray ( vertices , beam . v2 * 3 ) ;
1329+
1330+ const length = p1 . distanceTo ( p2 ) ;
1331+
1332+ if ( length < beamLatticeData . minLength ) continue ; // consumers must ignore beams shorter than "minlength"
1333+
1334+ const r1 = ( beam . r1 !== undefined ) ? beam . r1 : beamLatticeData . radius ;
1335+ const r2 = ( beam . r2 !== undefined ) ? beam . r2 : r1 ;
1336+
1337+ const geometry = new CylinderGeometry ( r2 , r1 , length , 8 , 1 , openEnded ) ;
1338+
1339+ direction . subVectors ( p2 , p1 ) . divideScalar ( length ) ;
1340+ quaternion . setFromUnitVectors ( up , direction ) ;
1341+
1342+ matrix . makeRotationFromQuaternion ( quaternion ) ;
1343+ matrix . setPosition ( p1 . add ( p2 ) . multiplyScalar ( 0.5 ) ) ;
1344+
1345+ geometry . applyMatrix4 ( matrix ) ;
1346+ geometries . push ( geometry ) ;
1347+
1348+ if ( openEnded ) {
1349+
1350+ // approximate "sphere" and "hemisphere" caps with a sphere per beam end
1351+
1352+ sphereRadii . set ( beam . v1 , Math . max ( r1 , sphereRadii . get ( beam . v1 ) || 0 ) ) ;
1353+ sphereRadii . set ( beam . v2 , Math . max ( r2 , sphereRadii . get ( beam . v2 ) || 0 ) ) ;
1354+
1355+ }
1356+
1357+ if ( beamLatticeData . ballMode === 'all' ) {
1358+
1359+ sphereRadii . set ( beam . v1 , Math . max ( beamLatticeData . ballRadius , sphereRadii . get ( beam . v1 ) || 0 ) ) ;
1360+ sphereRadii . set ( beam . v2 , Math . max ( beamLatticeData . ballRadius , sphereRadii . get ( beam . v2 ) || 0 ) ) ;
1361+
1362+ }
1363+
1364+ }
1365+
1366+ // balls
1367+
1368+ const balls = beamLatticeData . balls ;
1369+
1370+ for ( let i = 0 ; i < balls . length ; i ++ ) {
1371+
1372+ const ball = balls [ i ] ;
1373+ const r = ( ball . r !== undefined ) ? ball . r : beamLatticeData . ballRadius ;
1374+
1375+ sphereRadii . set ( ball . vindex , Math . max ( r , sphereRadii . get ( ball . vindex ) || 0 ) ) ;
1376+
1377+ }
1378+
1379+ for ( const [ vindex , radius ] of sphereRadii ) {
1380+
1381+ const geometry = new SphereGeometry ( radius , 8 , 6 ) ;
1382+
1383+ p1 . fromArray ( vertices , vindex * 3 ) ;
1384+ geometry . translate ( p1 . x , p1 . y , p1 . z ) ;
1385+
1386+ geometries . push ( geometry ) ;
1387+
1388+ }
1389+
1390+ //
1391+
1392+ const geometry = mergeGeometries ( geometries ) ;
1393+
1394+ const material = new MeshPhongMaterial ( {
1395+ name : Loader . DEFAULT_MATERIAL_NAME ,
1396+ color : 0xffffff
1397+ } ) ;
1398+
1399+ const mesh = new Mesh ( geometry , material ) ;
1400+
1401+ return mesh ;
1402+
1403+ }
1404+
12141405 function buildMeshes ( resourceMap , meshData , objects , modelData , textureData , objectData ) {
12151406
12161407 const keys = Object . keys ( resourceMap ) ;
@@ -1329,6 +1520,16 @@ class ThreeMFLoader extends Loader {
13291520 const resourceMap = analyzeObject ( meshData , objectData ) ;
13301521 const meshes = buildMeshes ( resourceMap , meshData , objects , modelData , textureData , objectData ) ;
13311522
1523+ if ( meshData [ 'beamlattice' ] !== undefined ) {
1524+
1525+ const latticeMesh = buildLatticeMesh ( meshData ) ;
1526+
1527+ if ( objectData . name ) latticeMesh . name = objectData . name ;
1528+
1529+ meshes . push ( latticeMesh ) ;
1530+
1531+ }
1532+
13321533 for ( let i = 0 , l = meshes . length ; i < l ; i ++ ) {
13331534
13341535 group . add ( meshes [ i ] ) ;
0 commit comments