Instancing
In this part, we will walk through building our own custom “instance operator”. This will be a custom operator that copies geometry.
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.ts file has been provided with the code for our InstanceOperator
class. Our custom operator class will implement the Operators.OperatorBase
interface within HOOPS Communicator to our specific needs. We will create member variables for where the user clicked, the current nodes we want to instance, and the Z position of the inserted nodes.
constructor(hwv: WebViewer) {
super(hwv, hwv.view);
this._mainViewer = hwv;
this._ptDown = 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 tutorial, 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 mouse down position is equal to the mouse up position, we can insert the node into the scene.
onMouseDown(event: MouseInputEvent) {
this._ptDown.assign(event.getPosition());
}
onMouseUp(event: MouseInputEvent) {
const position = event.getPosition();
if (position.equals(this._ptDown)) {
const config = new PickConfig(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.
async _insertGeometry(position: Point2) {
const meshIds = await this._mainViewer.model.getMeshIds(this._currentNodes);
meshIds.forEach(async (meshId, index) => {
const color = await this._mainViewer.model.getNodeEffectiveFaceColor(this._currentNodes[index], 0);
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;
return this._mainViewer.model.createMeshInstance(
new MeshInstanceData(
meshId,
netMatrix,
"Node " + this._currentNodes + " Instance",
color,
Color.black())
);
});
}
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: number[]) {
this._currentNodes = this._gatherChildLeafNodes(nodeIds);
this._mainViewer.model.getNodesBounding(this._currentNodes).then((box) => {
this._nodePosZ = box.max.z - box.min.z;
});
}
With this, we have the application logic to instance nodes in the scene. But we still need to instantiate the operator itself and connect it to the UI button. Instancing is handled below:
const instanceOp = new InstanceOperator(this.hwv);
const handle = this.hwv.registerCustomOperator(instanceOp);
const instanceBtn = document.getElementById("instance-button")!;
Now, let’s connect it to the UI:
instanceBtn.onclick = () => {
// Use the button to push and pop the operator from the operator stack
if (instanceBtn.innerHTML.includes("Instance Part")) {
// Gather nodes to be instanced
let nodeIds: number[] = [];
const selectionItems = this.hwv.selectionManager.getResults();
selectionItems.map((selection) => {
nodeIds.push(selection.getNodeId());
});
if (selectionItems.length !== 0) {
instanceBtn.innerHTML = "Disable Instancing";
instanceOp.setNodesToInstance(nodeIds);
// Remove the selection operator from the stack while instancing
this.hwv.view.operatorManager.push(handle);
this.hwv.view.operatorManager.remove(OperatorId.Select);
this.hwv.selectionManager.setHighlightNodeSelection(false);
this.hwv.selectionManager.setHighlightFaceElementSelection(false);
this.hwv.selectionManager.setPickTolerance(0);
}
else {
alert(
"Try Again. Please first select nodes from the model to instance!"
);
}
}
else {
instanceBtn.innerHTML = "Instance Part";
// Remove the instance operator from the stack and reenable selection and highlighting
this.hwv.selectionManager.clear();
this.hwv.view.operatorManager.remove(handle);
this.hwv.view.operatorManager.push(OperatorId.Select);
this.hwv.selectionManager.setHighlightNodeSelection(true);
this.hwv.selectionManager.setHighlightFaceElementSelection(true);
}
};