Add component selection

Summary

In this chapter, we will walk through adding a component from the preview viewer to the total assembly.

Concepts

  • Managing the assembly tree nodes

  • Tracking selected components

  • Swapping components of the same type


Tracking component selections

From our HTML template, we can see that there are several buttons for working with our bicycle build and assembly of selected components. In this section, we will focus on the “Add to Build” button. Since we expect this button to exhibit some behavior when clicked, we will set up an onclick event listener in our setEventListeners() function from earlier.

document.getElementById("add-to-build-btn").onclick = () => {
};

The first thing we will want to do is ensure that we even have a selected component to add to the build. If no component has been selected yet (by clicking on a model thumbnail), we should inform the user to make a selection first. We can determine if a selection has been made or not by checking to see if any of the class variables relating to model selection have been defined (these variable are set when a user clicks a thumbnail to make a selection).

if (!this._componentType || !this._selectedComponent || !this._selectedComponentName) {
        alert("No component has been selected to add to build. Please select a component to add.");
        return;
}

Assuming a component selection has been made, we want this selection to be added to an overall “build” profile, which will track each particular component selection for each component category in the build. For example, if a frame has previously been added to the build, we cannot add another frame and have two frames in the build, rather, we would need to replace the previous frame selection with the new one. The same concept applies to the fork, wheels, etc.

To track our build selections, we will make a Map and use the _componentType category as the key and the _selectedComponent choice as the value. When we “add to build”, we will update this Map. We will also require the user to start by selecting a “frame” component, since all other components will attach to the frame. Remember to declare a new Map in your constructor, so we can populate it when the “Add to Build” button is clicked.

In your constructor…

this._buildSelections = new Map();

In your setEventListeners() function…

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;
        }
}

Organizing the assembly tree

In HOOPS Communicator, when the model structure is ready, there is an absolute root node for the Model class that all other nodes stem from. In our component preview viewer, we simply added a node off this root node, and loaded the model to that node without much more thought. In this viewer, we will create a node off the root node for each component type. When a new component selection is made for that component type, we will load that model into its appropriate child node (with the absolute root being the parent). If there is no component type child node yet (it was the first selection of a particular component type), we will create the node. To determine whether a node exists yet or not, we will assign node names when creating the nodes, and check against them.

Let’s walk step by step how to handle this.

First, we determine the name of the node from the _componentType value. We will also gather all children of the absoluteRootNode so we can compare the names of these children to our local node name. If there are no children, and the user has selected a frame, then this is the first selection. Create a new node using the node name and load the selected component into this node. The code should go into the add-to-build-click event listener.

const nodeName = "Model-" + this._componentType;
let componentSubtrees = model.getNodeChildren(model.getAbsoluteRootNode());

// 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`);
}

If the length of children is not zero, we need to query the children node names to see if the node we want to load into already exists. If it doesn’t, create the node.

// 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);
                                promiseArray.push(model.loadSubtreeFromScsFile(modelNodeId, `/data/scs/${this._selectedComponent}.scs`));
                                return;
                        })
                }
        }

        if (!nodeExists) {
          const modelNodeId = model.createNode(null, nodeName);
          this._viewer.model.loadSubtreeFromScsFile(modelNodeId, `/data/scs/${this._selectedComponent}.scs`);
        }
}

If the node exists, we note its nodeId, delete the node, then recreate the node with the same nodeId and name. Then load the model into this node. We will store any calls that return promises in a promise array (the reason for this will be clear in the next chapter). If we were to just simply load the model using the existing nodeId, it would append the model as a child, rather than replace the model.

The last item we must incorporate in this callback is to update the DOM with our selections in the Configuration Details pane. Our element IDs were assigned so we can do this with one call for any component type.

document.getElementById(`breakdown-${this._componentType}`).innerHTML = this._selectedComponentName;

If you run your application, load and add a component from each component category to your build, you will see the selections populate in the configuration details and the model part brought into the main assembly viewer. However, you will also notice our individual parts, which come from different modelers and sources, are added with their own transforms and with no relation to each other. We will address this in the next section.

../../_images/product-configurator-configuration-details.png