Model "aggregation" is the term we use to define multiple models being loaded into the same View & Data scene. The viewer API was designed initially with the ability to load a single model per scene, so some of this material is work in progress, keep on reading ...
Being able to load multiple models into a scene seems to be quite popular among the developers using View & Data API for commercial applications, and I believe for a reason: dynamically adding and removing already translated models in a scene can bring some very powerful features to your app, thinking about configurations, animations or more complex workflows for example. All my code samples below are relying on ES6 and async syntax, to leverage that it's also promisified. Let's get started.
Loading an extra model to your scene
I assume you already know how to load the first model into your scene. In order to add a second model to it, all you need is to know is it's viewable path and pass it to the viewer.loadModel method. Grabbing the viewable path from the URN and loading the model could look like that:
1 ///////////////////////////////////////////////////////////////// 2 // Returns viewable path from URN (needs matching token) 3 // 4 ///////////////////////////////////////////////////////////////// 5 getViewablePath: function(token, urn) { 6 7 return new Promise((resolve, reject)=> { 8 9 try { 10 11 Autodesk.Viewing.Initializer({ 12 accessToken: token 13 }, ()=> { 14 15 Autodesk.Viewing.Document.load( 16 'urn:' + urn, 17 (document)=> { 18 19 var rootItem = document.getRootItem(); 20 21 var geometryItems3d = Autodesk.Viewing.Document. 22 getSubItemsWithProperties( 23 rootItem, { 24 'type': 'geometry', 25 'role': '3d' }, 26 true); 27 28 var geometryItems2d = Autodesk.Viewing.Document. 29 getSubItemsWithProperties( 30 rootItem, { 31 'type': 'geometry', 32 'role': '2d' }, 33 true); 34 35 var got2d = (geometryItems2d && geometryItems2d.length > 0); 36 var got3d = (geometryItems3d && geometryItems3d.length > 0); 37 38 if(got2d || got3d) { 39 40 var pathCollection = []; 41 42 geometryItems2d.forEach((item)=>{ 43 44 pathCollection.push(document.getViewablePath(item)); 45 }); 46 47 geometryItems3d.forEach((item)=>{ 48 49 pathCollection.push(document.getViewablePath(item)); 50 }); 51 52 return resolve(pathCollection); 53 } 54 else { 55 56 return reject('no viewable content') 57 } 58 }, 59 (err)=> { 60 61 console.log('Error loading document... '); 62 63 //Autodesk.Viewing.ErrorCode 64 65 switch(err){ 66 67 // removed for clarity, see full sample 68 } 69 }); 70 }); 71 } 72 catch(ex){ 73 74 return reject(ex); 75 } 76 }); 77 }, 78 79 ///////////////////////////////////////////////////////////////// 80 // Loads model into current scene 81 // 82 ///////////////////////////////////////////////////////////////// 83 loadModel: function(path, opts) { 84 85 return new Promise(async(resolve, reject)=> { 86 87 function _onGeometryLoaded(event) { 88 89 viewer.removeEventListener( 90 Autodesk.Viewing.GEOMETRY_LOADED_EVENT, 91 _onGeometryLoaded); 92 93 return resolve(event.model); 94 } 95 96 viewer.addEventListener( 97 Autodesk.Viewing.GEOMETRY_LOADED_EVENT, 98 _onGeometryLoaded); 99 100 viewer.loadModel(path, opts, ()=> { 101 102 }, 103 (errorCode, errorMessage, statusCode, statusText)=> { 104 105 viewer.removeEventListener( 106 Autodesk.Viewing.GEOMETRY_LOADED_EVENT, 107 _onGeometryLoaded); 108 109 return reject({ 110 errorCode: errorCode, 111 errorMessage: errorMessage, 112 statusCode: statusCode, 113 statusText: statusText 114 }); 115 }); 116 }); 117 },
Transforming models
Most likely you will need to either translate, rotate and/or scale the other models you bring to the scene so they fit nicely together. The viewer.loadModel method take a placementTransform optional argument which is a THREE.Matrix4 transformation matrix:1 var loadOptions = { 2 placementTransform: //your THREE.Matrix4 goes here ... 3 } 4 5 var model = await API.loadModel( 6 path, 7 loadOptions);You can compose in the same matrix translation, rotation and scale. See THREE.Matrix4 compose method.
What if you need to transform a model after it's been added to the scene? In that case you need to apply the same transform to each of the model fragments, it's a bit more tricky:
1 ///////////////////////////////////////////////////////////////// 2 // Applies transform to specific model 3 // 4 ///////////////////////////////////////////////////////////////// 5 transformModel: function(model, transform) { 6 7 function _transformFragProxy(fragId){ 8 9 var fragProxy = viewer.impl.getFragmentProxy( 10 model, 11 fragId); 12 13 fragProxy.getAnimTransform(); 14 15 fragProxy.position = transform.translation; 16 17 fragProxy.scale = transform.scale; 18 19 //Not a standard three.js quaternion 20 fragProxy.quaternion._x = transform.rotation.x; 21 fragProxy.quaternion._y = transform.rotation.y; 22 fragProxy.quaternion._z = transform.rotation.z; 23 fragProxy.quaternion._w = transform.rotation.w; 24 25 fragProxy.updateAnimTransform(); 26 } 27 28 return new Promise(async(resolve, reject)=>{ 29 30 var fragCount = model.getFragmentList(). 31 fragments.fragId2dbId.length; 32 33 //fragIds range from 0 to fragCount-1 34 for(var fragId=0; fragId<fragCount; ++fragId){ 35 36 _transformFragProxy(fragId); 37 } 38 39 return resolve(); 40 }); 41 },
Fixing the Model Structure Panel behaviour
We now have multiple models into our scene, positioned and scale as we want, however some components of the viewer UI will start to behave impolitely. That's the case of the Model Structure Panel which will display only the hierarchy of the last model loaded. In order to fix that, you can switch programmatically the structure being used by the panel:
1 model.getObjectTree((instanceTree) =>{ 2 3 viewer.modelstructure.setModel(instanceTree); 4 });This is enough to get the correct structure tree, however selecting components from the tree will not work correctly as it does with a single model. In order to fix that, it would be required to completely redefine the behaviour of that component by using a custom structure panel which I haven't implemented so far. A simple example of a custom structure panel can be found here: Autodesk.ADN.Viewing.Extension.ModelStructurePanel
Fixing the Property Panel behaviour
The property panel will also stop to display properties of selected component, although at the time of this writing, this has been fixed into the dev version of the API. For the time being you can use that workaround. One reason is that the Autodesk.Viewing.SELECTION_CHANGED_EVENT is no longer fired but another event is used as replacement: Autodesk.Viewing.AGGREGATE_SELECTION_CHANGED_EVENT
This is how the event argument is looking:
We can use that event to fix the correct properties being passed to the property panel. Here is the code I came up with:
1 ///////////////////////////////////////////////////////////////// 2 // Aggregate SelectionChanged handler 3 // 4 ///////////////////////////////////////////////////////////////// 5 async function onAggregateSelectionChanged(event) { 6 7 if(event.selections && event.selections.length){ 8 9 var selection = event.selections[0]; 10 11 var model = selection.model; 12 13 await API.setCurrentModel(model); 14 15 var nodeId = selection.dbIdArray[0]; 16 17 setPropertyPanelNode(nodeId); 18 } 19 //no components selected -> display properties of root 20 else { 21 22 viewer.model.getObjectTree((instanceTree) =>{ 23 24 setPropertyPanelNode(instanceTree.rootId); 25 }); 26 } 27 } 28 29 function setPropertyPanelNode(nodeId) { 30 31 viewer.getProperties(nodeId, (result)=>{ 32 33 if(result.properties) { 34 35 var propertyPanel = viewer.getPropertyPanel(true); 36 37 propertyPanel.setNodeProperties(nodeId); 38 39 propertyPanel.setProperties(result.properties); 40 } 41 }); 42 }
Fixing the context menu options
The visibility options in the context menu will only work for the initial model, I guess because of the different selection behaviour. I believe using a custom context menu will be more or less straightforward to implement and then using custom methods to turn on or off the visibility of the selected nodes, based on which model they belong. I quickly tested the following functions and they work, however some extra logic is required to be able for example to select nodes from different models and switch their visibility through context menu options.
1 ///////////////////////////////////////////////////////////////// 2 // Hides node (if nodeOff = true completely hides the node) 3 // 4 ///////////////////////////////////////////////////////////////// 5 hideNode: function(model, dbIds, nodeOff=false) { 6 7 return new Promise((resolve, reject)=> { 8 9 dbIds = Array.isArray(dbIds) ? dbIds : [dbIds]; 10 11 model.getObjectTree((instanceTree)=> { 12 13 var vm = new Autodesk.Viewing.Private.VisibilityManager( 14 viewer.impl, 15 viewer.model); 16 17 dbIds.forEach((dbId)=> { 18 19 var node = instanceTree.dbIdToNode[dbId]; 20 21 vm.hide(node); 22 vm.setNodeOff(node, nodeOff); 23 }); 24 25 return resolve(); 26 }); 27 }); 28 }, 29 30 ///////////////////////////////////////////////////////////////// 31 // Shows node 32 // 33 ///////////////////////////////////////////////////////////////// 34 showNode: function(model, dbIds) { 35 36 return new Promise((resolve, reject)=> { 37 38 dbIds = Array.isArray(dbIds) ? dbIds : [dbIds]; 39 40 model.getObjectTree((instanceTree)=> { 41 42 var vm = new Autodesk.Viewing.Private.VisibilityManager( 43 viewer.impl, 44 viewer.model); 45 46 dbIds.forEach((dbId)=> { 47 48 var node = instanceTree.dbIdToNode[dbId]; 49 50 vm.setNodeOff(node, false); 51 vm.show(node); 52 }); 53 54 return resolve(); 55 }); 56 }); 57 }
That's all for now, if you have issues, comments or suggestions about that aggregation feature, we are happy to hear it, you can use this forum thread: Multiple URN's in one LMV instance
The complete code for that sample is available here: Autodesk.ADN.Viewing.Extension.ModelLoader
And this is how the the extension panel looks like and a link from where you can test it live.
Comments
You can follow this conversation by subscribing to the comment feed for this post.