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


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.

Let’s start by creating a new file in src/js called instanceOperator.js and making a class instanceOperator. Since this is a custom operator, we will inherit from the generic Communicator.Operator.Operator class, then fill in the functionality we need. As we saw with the transformOperator, we will want to make sure our viewers are synced with any operations performed, so lets pass the viewSync syncHelper object as an argument to our operator. 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.

export default class instanceOperator extends Communicator.Operator.Operator {
constructor(viewSync) {
super();
this._viewSync = viewSync;
this._mainViewer = viewSync.getMainViewer();
this._attachedViewers = viewSync.getAttachedViewers();
this._ptDown = Communicator.Point2.zero();
this._currentNodes = [];
this._nodePosZ = 0;
}
}

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, we will use the onMouseDown() and onMouseUp event callbacks in the Operator class. We use both to ensure that the user is clicking and not dragging the mouse. If the onMouseDown() position is equal to the 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)) {
this._insertModeMouseUp(position);
}
};

You can see we abstract the insertion into a new function called _insertModeMouseUp. 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.

insertModeMouseUp(ePosition) {
const config = new Communicator.PickConfig(Communicator.SelectionMask.Face);
this._mainViewer.view.pickFromPoint(ePosition, 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");
}
});
};

We make 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()));
}
this._mainViewer.model.createMeshInstance(mid.pop())
.then(nodeId => {
this._viewSync.setNeedsUpdate(true);
});
this._attachedViewers.map(viewer => {
viewer.model.createMeshInstance(mid.pop())
.then(nodeId => {
this._viewSync.setNeedsUpdate(true);
});
});
});
});
});
};

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

There is one more member function we will write in our instanceOperator class, but let’s revist app.js for now to instantiate and register our new operator.

Follow the same steps we did to create and register the transformOperator before. First, we include our module.

import '../css/tutorial-am.css';
import printingPlane from "./printingPlane.js";
import transformOperator from "./transformOperator.js";
import instanceOperator from "./instanceOperator";
import syncHelper from "./syncHelper";

Then, we create and register our operator on the main viewer.

// Create custom operators and register them with the main webviewer
this._instanceOp = new instanceOperator(this._viewSync);
this._instanceHandle = mainViewer.registerCustomOperator(this._instanceOp);
this._transformOp = new transformOperator(this._viewSync);
this._transformHandle = mainViewer.registerCustomOperator(this._transformOp);
this.setEventListeners();

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

Revisiting the instanceOperator.js file, 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 should be able to instance nodes in the scene.