Incorporating Model Relationships
Summary
In this chapter, we will load external data that specifies the transforms relating our parts as a whole assembly.
Concepts
Incorporating external data
Dealing with scaling and units
Transforming nodes in the scene
Using “fetch”
As we saw in the previous chapter, it can be difficult to manage many individual parts from different sources coming together into one model assembly tree. The parts have no relation to one another and could potentially be modeled in different units or scales. However, we can address this by defining these relationships ourselves, then bringing in that data to help place each part in the correct spot. By using identical naming between the _componentType
values and the .JSON database keys, we can use the loaded model name to request the part specific database information. Since this data is static, we only need to retrieve it once. We will fetch this data in our constructor and store it in a class member Map
structure so it can be referenced throughout our application.
In your constructor…
this._frameAttachPoints = new Map();
fetch("/data/attachPoints.json")
.then((resp) => {
if (resp.ok) {
resp.json()
.then((data) => {
let nodeData = data.NodeData;
let numEntries = nodeData.length;
for (let i = 0; i < numEntries; ++i) {
this._frameAttachPoints.set(nodeData[i].modelName, nodeData[i]);
}
});
}
else {
alert("No JSON data for this Model was found.");
}
});
Scaling correctly
HOOPS Communicator comes with a feature that detects if models have different units between them. If HOOPS Communicator detects this, it will automatically change all subsequent models to be consistent with the units of the original model. This causes the changed model (now with new units) to scale to the scene. For more information see Working with units.
If we have models using different units, we don’t necessarily want to change those units if they were designed to appropriate size. To avoid unwanted scaling, since our parts were originally designed to proper scale, we will disable automatic scaling in our modelStructureReady
callbacks.
this._viewer.setCallbacks({
modelStructureReady: () => {
this._viewer.model.setEnableAutomaticUnitScaling(false);
},
sceneReady: () => {
// Enable backfaces
this._viewer.view.setBackfacesVisible(true);
// Set Background color for viewer
this._viewer.view.setBackgroundColor(new Communicator.Color(33, 33, 33), new Communicator.Color(175, 175, 175));
// Enable nav cube and axis triad
this._viewer.view.getAxisTriad().enable();
this._viewer.view.getNavCube().enable();
this._viewer.view.getNavCube().setAnchor(Communicator.OverlayAnchor.LowerRightCorner);
},
selectionArray: (selectionEvents) => {
// Reserved for later use
}
}); // End Callbacks
Make sure to do this in both viewers callbacks!
Apply the transform data
Let’s take a quick look at the raw data actually brought in from our attachPoints.json file.
"modelName": "jabberpeggy",
"frame": "Bradford Jabberpeggy",
"fork": {
"0": 0.010808990032881011,
"1": 0.005201822807360547,
"2": 0.9999280507986311,
"3": 0,
"4": 0.9561399678008847,
"5": 0.2926700285023288,
"6": -0.011858178197744228,
"7": 0,
"8": -0.2927106552693163,
"9": 0.9561993492237525,
"10": -0.001810203270530976,
"11": 0,
"12": 962.951082707413,
"13": 152.81618107051088,
"14": -0.14437163790563545,
"15": 1
},
Here we can see how the data is laid out. We identify the model the transforms apply to. Because all components attach to the frame, we base all other transforms from the frame. Each component then has its component type, and the specific transform for where to move it on that particular frame. We did not do this for each and every part, but enough so that you get the idea. Each component type has 16 values defining its transform matrix. We can then use these 16 values and the createFromArray()
API to build the transform matrix.
We only need to construct these matrices when adding a new component to the build assembly viewer, so we can construct the matrix when the user is “adding to build”. Let’s revisit the onclick
callback for the “Add to Build” button.
In the “Add to Build” button callback…
// Build the transform matrix for the part to place it in the right spot when added
let rawMatData = this._frameAttachPoints.get(frameBase)[this._componentType];
let transformMatrix = this._componentType === "frame" ? null : Communicator.Matrix.createFromArray(Object.values(rawMatData));
We are using the frame name to identify the object data and retrieving the _componentType
matrix from that data. We can then use this transform when we load a node into the scene. However, things become difficult if you change the main reference point – the frame. Changing the frame will change all the component matrices. Therefore, when the frame selection changes and other components have been chosen, we need to update the attach points and transforms for all those components. This operation could take a slightly longer time, so in order to make it appear like it is all happening at once, we will hide the scene, wait for the operations to complete, then reveal the scene again.
if (this._componentType === "frame") {
promiseArray.push(model.setNodesVisibility([model.getAbsoluteRootNode()], false));
let componentSubtrees = model.getNodeChildren(model.getAbsoluteRootNode());
// Frame selection change - update the component attach points
for (let nodeId of componentSubtrees) {
let nodeName = model.getNodeName(nodeId);
let nodeType = nodeName.slice(6);
if (nodeType === "frame") continue;
let rawMatData = this._frameAttachPoints.get(frameBase)[nodeType];
let transformMatrix = Communicator.Matrix.createFromArray(Object.values(rawMatData));
promiseArray.push(model.setNodeMatrix(nodeId, transformMatrix));
}
Promise.all(promiseArray).then(() => {
this._viewer.view.setBoundingCalculationIgnoresInvisible(false);
this._viewer.view.fitWorld(0)
.then( () => model.setNodesVisibility([model.getAbsoluteRootNode()], true));
})
}
We also need to revisit our createNode
calls within this function from earlier. You now need to pass in the built transformation matrix when creating the node, so the model loads with that transform. After these modifications, your “Add to Build” callback function should look like this:
document.getElementById("add-to-build-btn").onclick = () => {
if (!this._componentType || !this._selectedComponent || !this._selectedComponentName) {
alert("No component has been selected to add to build. Please select a component to add.");
return;
}
let model = this._viewer.model;
this._buildSelections.set(this._componentType, this._selectedComponent);
let frameBase = this._buildSelections.get("frame");
if (frameBase === undefined) {
alert("Please select a frame before adding other components to your build.");
return;
}
const nodeName = "Model-" + this._componentType;
let componentSubtrees = model.getNodeChildren(model.getAbsoluteRootNode());
// Build the transform matrix for the part to place it in the right spot when added
let rawMatData = this._frameAttachPoints.get(frameBase)[this._componentType];
let transformMatrix = this._componentType === "frame" ? null : Communicator.Matrix.createFromArray(Object.values(rawMatData));
// First time frame is selected
if (componentSubtrees.length === 0 && this._componentType === "frame") {
const modelNodeId = model.createNode(null, nodeName);
model.loadSubtreeFromScsFile(modelNodeId, `/data/scs/${this._selectedComponent}.scs`);
}
// For all other components, identify if the same type component has already been added.
// If so, delete the existing node, and load the new node into the same nodeId and name.
// Otherwise, create a new node off the absolute root
else {
let nodeExists = false;
for (let nodeId of componentSubtrees) {
if (model.getNodeName(nodeId) === nodeName) {
nodeExists = true;
model.deleteNode(nodeId).then(() => {
let promiseArray = []
const modelNodeId = model.createNode(null, nodeName, nodeId, transformMatrix);
promiseArray.push(model.loadSubtreeFromScsFile(modelNodeId, `/data/scs/${this._selectedComponent}.scs`));
if (this._componentType === "frame") {
promiseArray.push(model.setNodesVisibility([model.getAbsoluteRootNode()], false));
let componentSubtrees = model.getNodeChildren(model.getAbsoluteRootNode());
// Frame selection change - update the component attach points
for (let nodeId of componentSubtrees) {
let nodeName = model.getNodeName(nodeId);
let nodeType = nodeName.slice(6);
if (nodeType === "frame") continue;
let rawMatData = this._frameAttachPoints.get(frameBase)[nodeType];
let transformMatrix = Communicator.Matrix.createFromArray(Object.values(rawMatData));
promiseArray.push(model.setNodeMatrix(nodeId, transformMatrix));
}
Promise.all(promiseArray).then(() => {
this._viewer.view.setBoundingCalculationIgnoresInvisible(false);
this._viewer.view.fitWorld(0)
.then( () => model.setNodesVisibility([model.getAbsoluteRootNode()], true));
})
}
return;
})
}
}
if (!nodeExists) {
const modelNodeId = model.createNode(null, nodeName, null, transformMatrix);
this._viewer.model.loadSubtreeFromScsFile(modelNodeId, `/data/scs/${this._selectedComponent}.scs`);
}
}
document.getElementById(`breakdown-${this._componentType}`).innerHTML = this._selectedComponentName;
}
If you reload your application and make selections for each of the component types, you should see them attaching to the appropriate spots of the bicycle (again, not all parts were given transform data).