# 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.