The instance operator

Summary

In this chapter, we will walk through building our own custom “instance operator”. This will be a custom operator that copies geometry.

Concepts

  • Inserting objects with the mouse

  • Gathering node dependencies

  • Instancing through mesh creation


A common additive manufacturing use case may be to replicate parts on the plane for printing multiple copies. To do this visually, we can instance selected nodes and allow the user to place them on the plane with the mouse.

The InstanceOperator.js file has been provided with the basic skeleton code for our InstanceOperator class. Our custom operator class will implement the Communicator.Operator interface within HOOPS Communicator to our specific needs. One of those needs is to make sure our viewers are synced with any operations performed, so lets pass the viewSync SyncHelper object to the constructor. We will also create member variables for where the user clicked, the current nodes we want to instance, and the Z position of the inserted nodes.

class InstanceOperator {
        constructor(viewSync) {
                this._viewSync = viewSync;
                this._mainViewer = viewSync.getMainViewer();
                this._attachedViewers = viewSync.getAttachedViewers();
                this._ptDown = Communicator.Point2.zero();
                this._currentNodes = [];
                this._nodePosZ = 0;
        }

        onMouseDown(event) { }
        onMouseMove(event) { }
        onMouseUp(event) { }
        onMousewheel(event) { }
        onTouchStart(event) { }
        onTouchMove(event) { }
        onTouchEnd(event) { }
        onKeyDown(event) { }
        onKeyUp(event) { }
        onDeactivate() { }
        onActivate() { }
        onViewOrientationChange() { }
        stopInteraction() { }
}

We want the user to be able to select a node, click on the plane, and insert the node where the user clicked. For this tutorial, we will use the Communicator.Operator.Operator.onMouseDown and Communicator.Operator.Operator.onMouseUp event callbacks in the Operator class. We use both to ensure that the user is clicking and not dragging the mouse. If the Communicator.Operator.Operator.onMouseDown position is equal to the Communicator.Operator.Operator.onMouseUp position, we can continue to insert the node into the scene.

onMouseDown(event) {
        this._ptDown.assign(event.getPosition());
}

onMouseUp(event) {
        const position = event.getPosition();
        if (position.equals(this._ptDown)) {
                const config = new Communicator.PickConfig(Communicator.SelectionMask.Face);
                this._mainViewer.view.pickFromPoint(position, config).then((selectionItem) => {
                        if (selectionItem.isEntitySelection() &&
                                this._mainViewer.model.getNodeName(selectionItem.getNodeId()) === "printingPlane") {
                                this._insertGeometry(selectionItem.getPosition());
                        }
                        else {
                                alert("Please select a point on the printing plane.");
                        }
                });
        }
}

This function will ensure that the user has picked a point on the PrintingPlane surface, and not elsewhere in world space. If the user did select the PrintingPlane, we continue to insert the geometry.

The InstanceOperator class will need another function called _insertGeometry, that uses the mouse click event position on the selection item to dictate where the geometry should be inserted.

To insert the geometry, we will follow the same general steps outlined in the Building A Basic Application tutorial. We will obtain the mesh IDs, then use those mesh IDs to set mesh instance data and create the mesh instance. The inserted mesh instance will return a new node ID, indicating to the viewSync object that we need to update our model nodes.

_insertGeometry(position) {
        this._mainViewer.model.getMeshIds(this._currentNodes).then(meshIds => {
                meshIds.forEach((meshId, index) => {
                        this._mainViewer.model.getNodeEffectiveFaceColor(this._currentNodes[index], 0).then(color => {
                                let netMatrix = this._mainViewer.model.getNodeNetMatrix(this._currentNodes[index]);
                                netMatrix.m[12] = position.x; // Add translation to the X-axis.
                                netMatrix.m[13] = position.y; // Add translation to the Y-axis.
                                netMatrix.m[14] = this._nodePosZ;
                                let mid = [];
                                let numInstances = this._attachedViewers.length + 1;
                                for (let i = 0; i < numInstances; ++i) {
                                        mid.push(new Communicator.MeshInstanceData(meshId, netMatrix, "Node " + this._currentNodes + " Instance", color, Communicator.Color.black()));
                                }

                                let meshInstancePromises = [this._mainViewer.model.createMeshInstance(mid.pop())];
                                this._attachedViewers.map(viewer => {
                                        meshInstancePromises.push(viewer.model.createMeshInstance(mid.pop()));
                                });

                                Promise.all(meshInstancePromises)
                                        .then( (nodeIds) => {
                                                let masterNode = nodeIds.shift();
                                                this._viewSync.setNodesMapping(masterNode, nodeIds);
                                        })
                        });
                });
        });
}

Notice we create Communicator.MeshInstancedata for each viewer – this is because once we use our Communicator.MeshInstanceData, it is unavailable for use again in another viewer, so we duplicate the object and pass it to each viewer.

Lets write our last member function, setNodesToInstance. This function gathers the selection node IDs, then recursively gathers and dependent leaf nodes we may need to instance alongside it.

setNodesToInstance(nodeIds) {
        this._currentNodes = this._gatherChildLeafNodes(nodeIds);
        this._mainViewer.model.getNodesBounding(this._currentNodes).then(box => {
                this._nodePosZ = box.max.z - box.min.z;
        });
}

_gatherChildLeafNodes(startNodes) {
        const model = this._mainViewer.model;
        let nodes = startNodes.slice();
        let leaves = [];
        for (let i = 0; i < nodes.length; ++i) {
                let node = nodes[i];
                let kids = model.getNodeChildren(node);

                if (kids.length === 0) {
                        leaves.push(node);
                }

                for (let j = 0; j < kids.length; j++) {
                        let kid = kids[j];
                        nodes.push(kid);
                }
        }

        return leaves;
}

With this, we are able to instance nodes in the scene.

Now that our InstanceOperator class is complete, we need to update app.js to instantiate and register the operator.

First, we create and register our operator on the main viewer. Update the provided configureOperators() function with the following snippet and verify the starter code invokes the function in the constructor() for main.

configureOperators() {
        let mainViewer = this._viewerList[0];
        let overheadViewer = this._viewerList[1];

        // Create custom operators and register them with the main webviewer
        this._instanceOp = new InstanceOperator(this._viewSync);
        this._instanceHandle = mainViewer.registerCustomOperator(this._instanceOp);

        // Disable operators in the overhead viewer
        overheadViewer.operatorManager.clear();
}

Our operator is now registered with the viewer, but the operator still needs to be hooked into the UI element provided. Let’s take a look at the instance-button event callback skeleton we wrote earlier. Once again, we want the user to make a node selection, click the “Instance Part” button, and then select a point on the plane to insert the geometry. When the user clicks the “Instance Part” button, we want to update the button text (so they can disable to operator), gather the user selection results, and then tell HC we want to instance those nodes. We will write a setNodesToInstance member function on our InstanceOperator class to handle this. Finally, we want to disable further selections, and push (or pop) our operator to/from the stack. We also disable highlighting so no weird highlighting results happen when interacting around the scene.

document.getElementById("instance-button").onclick = () => {
        // Use the button to push and pop the operator from the operator stack
        let elem = document.getElementById("instance-button");
        if (elem.innerHTML === "Instance Part") {
                // Gather nodes to be instanced
                let nodeIds = [];
                const selectionItems = mainViewer.selectionManager.getResults();
                selectionItems.map((selection) => {
                        nodeIds.push(selection.getNodeId());
                });
                if (selectionItems.length !== 0) {
                        elem.innerHTML = "Disable Instancing";
                        this._instanceOp.setNodesToInstance(nodeIds);
                        // Remove the selection operator from the stack while instancing
                        mainViewer.operatorManager.push(this._instanceHandle);
                        mainViewer.operatorManager.remove(Communicator.OperatorId.Select);
                        mainViewer.selectionManager.setHighlightNodeSelection(false);
                        mainViewer.selectionManager.setHighlightFaceElementSelection(false);
                        mainViewer.selectionManager.setPickTolerance(0);
                }
                else {
                        alert("Try Again. Please first select nodes from the model to instance!");
                }
        }
        else {
                elem.innerHTML = "Instance Part";
                // Remove the instance operator from the stack and reenable selection and highlighting
                mainViewer.selectionManager.clear();
                mainViewer.operatorManager.remove(this._instanceHandle);
                mainViewer.operatorManager.push(Communicator.OperatorId.Select);
                mainViewer.selectionManager.setHighlightNodeSelection(true);
                mainViewer.selectionManager.setHighlightFaceElementSelection(true);
        }
};

To use the new InstanceOperator first select a part, click on the “Instance Part” button, and begin clicking around the plane. Each click will create another instance of that node in both the “Main” and “Overhead” viewers. Click “Disable Instancing” to stop.