Model Loading and Aggregation
Summary
One of the advantages of using the HOOPS Web Viewer component is that it easily allows for aggregating multiple models into an existing scene. This means that instead of having to convert complex assemblies during authoring time their structure can be generated in your application on-the-fly and passed to the Web Viewer during a running viewing session. This functionality is also useful for configurator scenarios or to combine multiple federated BIM models.
In this section, we explore how to switch between different models and how to aggregate multiple models into the same scene.
Concepts
- Switching between models
- Loading multiple models
- ID mapping with multiple models
- Client-side XML loading
Add UI for Model Switching
Let’s start by modifying our UI to support switching between multiple models. As with the color, opacity, and texture controls from the previous two sections, there’s no packaged component for this, so we add a plain <select>. For now, let’s just switch between the two SCS files we already have in our project. Add this to index.html:
Note
If you’re continuing directly from Geometry and Texture Creation, <hoops-web-viewer> currently has no endpointUri and so no model loaded - that’s fine, selecting an option below will load one.
<div id="SwitchModels">
<h4>Switch Models</h4>
<div>
<label for="modelname">Model: </label>
<select id="modelname">
<option value="data/landinggear.scs">landinggear</option>
<option value="data/microengine.scs">microengine</option>
</select>
</div>
</div>
Handle the change Event
In src/main.ts, we add functionality to handle the change event, reusing the webViewer and modelTreeEl references from Interacting with the Model Tree:
const modelNameSelect = document.getElementById('modelname') as HTMLSelectElement;
modelNameSelect.addEventListener('change', () => {
const model = webViewer.model;
model.clear().then(() => {
const rootNode = model.getAbsoluteRootNode();
const modelName = modelNameSelect.value;
model.loadSubtreeFromScsFile(rootNode, modelName).then(() => {
modelTreeEl.model = model;
});
});
});
The code above retrieves the name of the model from the DOM element and switches to it with the loadSubtreeFromScsFile function which takes the node into which the model should be loaded as well as the name of the scs file to load. We also must clear out any existing model data first with the clear function.
The loadSubtreeFromScsFile function also returns a promise which indicates if the model has been successfully loaded. After that promise has returned we reassign modelTreeEl.model to refresh the packaged model tree component we set up in Interacting with the Model Tree - reassigning it (even to the same model object) forces the component to redraw against its current contents.
You will notice that we have to perform the loadSubtreeFromScsFile call within the promise returned by clear. This is because only after that promise has resolved has the clear function completed its operation. Making other calls related to the model tree before the promise has resolved can lead to unexpected behavior. This principle applies to other functions as well that return promises.
Adding Option for Loading Two Models
Now let’s add two models to the scene at once. For that, we start by adding another option to the switch model <select> element.
<select id="modelname">
<option value="data/landinggear.scs">landinggear</option>
<option value="data/microengine.scs">microengine</option>
<option value="combined">combined</option>
</select>
Loading Two Models
In the change event handler we now need to add a special case to handle that option:
modelNameSelect.addEventListener('change', () => {
const model = webViewer.model;
model.clear().then(() => {
const modelName = modelNameSelect.value;
const rootNode = model.getAbsoluteRootNode();
if (modelName === 'combined') {
const nodeid1 = model.createNode(rootNode, 'landinggear');
const nodeid2 = model.createNode(rootNode, 'microengine');
const loads = [
model.loadSubtreeFromScsFile(nodeid1, 'data/landinggear.scs'),
model.loadSubtreeFromScsFile(nodeid2, 'data/microengine.scs'),
];
Promise.all(loads).then(() => {
modelTreeEl.model = model;
});
} else {
model.loadSubtreeFromScsFile(rootNode, modelName).then(() => {
modelTreeEl.model = model;
});
}
});
});
In the code above we start by creating two empty nodes as children of the root node of the model. This is the place where we will put the two models we are about to load. We could have also added both models to the root node but this approach allows us to control more precisely where the models are inserted into the tree.
After those nodes have been created we can then kick off the loading of the two models. For maximum performance, we want to load both models simultaneously but still need to know when both of them are ‘ready’ so that we can refresh the model tree at that time. We do that by collecting those calls in an array and executing them simultaneously with Promise.all() which resolves once both promises have succeeded. For more information on promises, please see the Programming Guide.
Handling Overlapping Node IDs
You might ask how node IDs are handled if multiple models are loaded into a scene with overlapping node IDs. That is an important question because node IDs in the Web Viewer component are persistent, meaning they don’t change between subsequent model loads.
Note
The packaged <hoops-model-tree> component we’re using doesn’t expose a hook for customizing what each row displays, so - unlike the previous version of this tutorial - we won’t be injecting the node ID text directly into the tree. Instead, let’s log it to the console from the click handler we already wired up in Interacting with the Model Tree, which demonstrates the same underlying concept.
modelTreeEl.addEventListener('hoops-model-tree-node-click', (event: CustomEvent<{ nodeId: number }>) => {
webViewer.selectionManager.clear(false);
webViewer.selectionManager.add(Selection.SelectionItem.create(event.detail.nodeId), false);
const offset = webViewer.model.getNodeIdOffset(event.detail.nodeId);
const realNodeId = event.detail.nodeId - offset;
console.log(`Node ${event.detail.nodeId} - original ID: ${realNodeId}, offset: ${offset}`);
});
Load the combined model from the previous section and click through a few nodes in the tree with your browser console open. You’ll notice that node IDs are for the most part sequential, starting from 0 - except for the microengine (the second model that has been loaded), whose IDs are much higher. This is because they are “offset” by a fixed amount: every model that is loaded after the first model receives this offset to avoid node IDs clashing when multiple models are loaded. When you try to connect your user data or business logic with a model it can be necessary for your workflow to retrieve the original node IDs for the model before this offset has been applied. The code above uses getNodeIdOffset to retrieve that offset value and subtracts it from the node ID to calculate the original one.
Generating XML Model Structure File
You have now learned how to switch between models and add multiple models to the Web Viewer programmatically. There is another method for combining models which is to provide an XML file with the relevant model structure information. This is a convenient way to build up a complex assembly and pass its model structure information, along with the associated parts to the HOOPS Web Viewer component in a single function call.
Note
A shattered assembly is an assembly in which all individual parts are represented as SC models (in our case, .scs files) associated with an XML file containing the product structure. When updating parts within this assembly, only the individual .scs files need to be regenerated, not the whole assembly.
Let’s start by generating a “shattered” assembly from a CAD model. View the shattered workflow here.
We are taking one of our assembly models from the package and converting it in shattered mode. For that, simply modify the batch file found in authoring/converter/example/sample.[bat|sh] to the following:
:: This is an evaluation license key which will expire on May 31, 2019
set CONVERTER_LICENSE_KEY="your_license_key"
IF NOT EXIST output mkdir output
:: Prepare the shattered version of a Pro/E Assembly
..\bin\<platform>\converter --input "_data\micro_engine\_micro engine.CATProduct" --prepare_shattered_scs_parts "output\microengine_shattered" --prepare_shattered_xml "output\microengine.xml" --license %CONVERTER_LICENSE_KEY%
Be sure the update the platform in the path provided. Depending on the version of HOOPS Visualize Web you have installed, you may need to remove the single quotes from the license string.
This command-line creates a shattered version of the assembly and places the resulting SCS file into the microengine_shattered directory of the data directory in the authoring\converter\example\output directory. It will also generate an XML file containing the product structure of the assembly, which can be used to load this shattered model into the Web Viewer.
Below is a small snippet of that XML file:
<!--HC 7.0-->
<Root>
<ModelFile>
<ProductOccurence Id="0" Name="_micro engine" ExchangeId="" Behaviour="1" Children="1" Unit="1.000000" IsPart="false"/>
<ProductOccurence Id="1" Name="Product1" ExchangeId="" LayerId="65535" Style="65535" Behaviour="1" FilePath="_data\micro_engine\_micro engine.CATProduct" Children="2 3 4 5 6 7 8 9 10 11 12 13 14 15 16" IsPart="false"/>
<ProductOccurence Id="2" Name="HOUSING(HOUSING.1)" ExchangeId="" LayerId="65535" Style="0" Behaviour="1" FilePath="_data\micro_engine\housing .CATPart" InstanceRef="17" IsPart="false">
<Transformation RelativeTransfo="1 0 -0 0 -0 1 0 0 0 0 1 0 21.5 -0.5 -0.310000002384185791 1"/>
</ProductOccurence>
<ProductOccurence Id="17" Name="" ExchangeId="" Behaviour="1" IsPart="false">
<ExternalModel Name="housing .CATPart" Unit="0.000000">
<BoundingBox Min="-21.959007 0.155000 -71.000000" Max="21.959007 57.845001 0.309999"/>
</ExternalModel>
</ProductOccurence>
The content of this XML contains the complete top-level product structure information including attributes, matrices and user data. All the geometry data (including part-specific product structure information) is stored in the associated scs files which are referenced by the ExternalModel tags.
While the example-workflow converter creates this file for you from an existing CAD Assembly, in other scenarios (e.g., a PLM application that wants to display different variants of a model) it might be necessary for your application to generate the XML file on the fly from your data.
Add XML Loading Option
Let’s place this XML file into the top level folder of your application alongside the microengine_shattered directory containing the .scs files we have just created. In addition, we will add another loading case to our <select> element for this XML file:
<select id="modelname">
<option value="data/landinggear.scs">landinggear</option>
<option value="data/microengine.scs">microengine</option>
<option value="combined">combined</option>
<option value="XML">XML</option>
</select>
Load via XML File
Next, let’s add the new code for loading the XML file to the change event handler in src/main.ts:
modelNameSelect.addEventListener('change', () => {
const model = webViewer.model;
model.clear().then(() => {
const rootNode = model.getAbsoluteRootNode();
const modelName = modelNameSelect.value;
if (modelName === 'combined') {
const nodeid1 = model.createNode(rootNode, 'landinggear');
const nodeid2 = model.createNode(rootNode, 'microengine');
const loads = [
model.loadSubtreeFromScsFile(nodeid1, 'data/landinggear.scs'),
model.loadSubtreeFromScsFile(nodeid2, 'data/microengine.scs'),
];
Promise.all(loads).then(() => {
modelTreeEl.model = model;
});
} else if (modelName === 'XML') {
model
.loadSubtreeFromScsXmlFile(rootNode, 'microengine.xml', (name) => `microengine_shattered/${name}`)
.then(() => {
modelTreeEl.model = model;
});
} else {
model.loadSubtreeFromScsFile(rootNode, modelName).then(() => {
modelTreeEl.model = model;
});
}
});
});
Above, we are using another variant of the loadSubtree() function that pulls the XML file from the webserver and uses it to create the assembly by building up the product structure of the model on the client and loading the referenced SCS files.
The third parameter to the loadSubtreeFromScsXmlFile function is a callback that allows us to modify or extend the model name before it gets passed to the loader. We use it here to point the loading code to the appropriate directory where the SCS files are stored relative to the root directory. This path information is not included in the XML file generated by Converter.