7. Extending the Handles Operator

Summary

In this chapter, we will walk through building our own custom transform operator. This will be a custom operator that inherits the built in handle operator and extends its functionality.

Concepts


A common use case for a 3D printing application is to offer the ability to position and transform parts around the printing plane. We have already demonstrated (in Section 4) how we can move any node in the scene by providing a transformation matrix to setNodeMatrix, but HOOPS Communicator offers much more for transforms through its built in HandleOperator. Handles are added scene elements that can update the position of your parts through user interaction, rather than needing to call setNodeMatrix programmatically. If you refer back to the first screenshot of our finished application, you can see handles (attached to the highlighted node) that allow us to translate and rotate our part.

If we wanted, we could certainly use the built-in HandleOperator as it is out of the box. This would be as simple as pushing the HandleOperator onto the operatorManager stack, which was covered in the Building A Basic Application tutorial. However, let's extend this operator by making a new class with additional capability.

Because we are going to be adding our own operator for use in HOOPS Communicator, we must create and register a custom operator for our WebViewer object. In your src/js directory, create a new file called transformOperator.js. This is where we will create our child class of the HandleOperator. Create a class called transfromOperator that inherits from the HandleOperator in HOOPS Communicator. Again, since we will be including this in our main class, we want to make sure the class is marked for export default.

export default class transformOperator extends Communicator.Operator.HandleOperator {
constructor(mainViewer) {
super(mainViewer);
this._mainViewer = mainViewer;
}
}

The HandleOperator constructor requires that the WebViewer object be passed to it, so we must ensure we also accept the WebViewer object as a parameter and call the super keyword to call the parent (HandleOperator) constructor. Lastly, we store the viewer as a class property.

Let's extend the operator so it displays the updated node matrix of the part as the user is transforming it via handles. For the built-in handles operator, as we click and drag one of the Handles, the onMouseMove event fires and sets the new transform matrix for the node. We want to keep this capability, as well as add additional functionality. No problem - simply create the onMouseMove member in your class, then use the super keyword again to call the parents onMouseMove event. Once this is done, we can add whatever additional functionality we want.

onMouseMove(event) {
super.onMouseMove(event);
// Additional functionality to be called onMouseMove
}

We have now ensured our handles did not lose any of its inherited functionality. Next we will write code to query the node matrix when the user is transforming a part. In the onMouseMove function, we will check the callback event to see if the mouse is dragging (moving while holding the mouse button down). If so, the user must be updating the transform, so query its position, and output it to the provided HTML table fields.

onMouseMove(event) {
super.onMouseMove(event);
if (this.isDragging()) {
const selectionItems = this._mainViewer.selectionManager.getResults();
let nodeIds = [];
selectionItems.map((selectionItem) => {
nodeIds.push(selectionItem.getNodeId());
}); transformOperator.setMatrixText(this._mainViewer.model.getNodeNetMatrix(nodeIds[0]));
}
}

We still need to implement the setMatrixText function, which will update text on the DOM. We will also use this method in our main application when making a selection to update the node matrix info for that selection. Because of the unchanging nature of this function, we can make this method static and accessible to the main class.

static setMatrixText(matrix) {
const ids = ['m11', 'm21', 'm31', 'm41',
'm12', 'm22', 'm32', 'm42',
'm13', 'm23', 'm33', 'm43',
'm14', 'm24', 'm34', 'm44'];
for (let [index, id] of ids.entries()) {
document.getElementById(id).innerHTML = matrix.m[index].toFixed(1);
}
}

While we are on the topic, go back to the app.js file and add a call to setMatrixText at the bottom of the selectionArray callback.

document.getElementById("model-file-name").innerHTML = modelFileName || "N/A";
document.getElementById("model-file-type").innerHTML = Communicator.FileType[modelFileFormat] || "N/A";
document.getElementById("node-id").innerHTML = nodeId.toString() || "Unknown";
document.getElementById("node-name").innerHTML = mainViewer.model.getNodeName(nodeId) || "Node Name Not Defined";
transformOperator.setMatrixText(mainViewer.model.getNodeNetMatrix(nodeId));

Since our transformOperator class is now in a state where we have added functionality, let's create and register the operator with the viewer. We can do this at any time during the lifecycle of the viewer, but we will choose to do it in the main class constructor when we instantiate the viewer.

First, we need to include the new module in our app.js file.

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

We need to instantiate the transformOperator class, then register the resulting operator object as a custom operator in HOOPS Communicator. Because we are limiting our interaction to the mainViewer object, we only need to pass and register the operator with the mainViewer. We will assign the returned values properties of the main class, so we can reference them when we build the UI. Add the following code to constructor:

this._transformOp = new transformOperator(mainViewer);
this._transformHandle = mainViewer.registerCustomOperator(this._transformOp);
// Disable Default Handle Operator - overwriting with custom one that inherits its functionality
mainViewer.operatorManager.remove(Communicator.OperatorId.Handle);

Notice that you should also remove the default handle operator from the stack, since we will be providing that functionality ourselves and do not want events to mix.

In the previous sections, you set up skeleton code to attach HOOPS Communicator operators and functionality to UI elements. In the app.js file, revisit the event listener for the handles-button element. This button will be used to push the transformOperator object onto the operator stack with the appropriate nodes to transform specified. We need to tell HOOPS Communicator which nodes we want the handles added to, and then explicitly show those handles. Both of these calls are inherited from the HandleOperator class. We will use the selectionManager to gather user selected nodes and query their node IDs to pass to the operator. If no selection was made, alert the user to make a selection.

document.getElementById("handles-button").onclick = () => {
// Need to gather the selected node IDs to know which nodes
// will be affected by the transformation
let nodeIds = [];
const selectionItems = mainViewer.selectionManager.getResults();
selectionItems.map((selectionItem) => {
nodeIds.push(selectionItem.getNodeId());
});
// Ensure the user has made a selection before trying to add handles
if (selectionItems.length !== 0) {
this._transformOp.addHandles(nodeIds);
this._transformOp.showHandles();
mainViewer.operatorManager.push(this._transformHandle);
}
else {
alert("Try Again. Please first select nodes from the model to transform!");
}
};

With this, you should be able to test the first implementation of the transformOperator class. Select a part from your scene and click "Show Handles". Handles should appear on your selected node. Click and drag one of the handles to transform your part and look to see how the node matrix information is updated.

You may have noticed that in the animation above, the second viewer did not update. This is because the onMouseMove event only fires for the viewer we are interacting with, and our overhead viewer has no knowledge of this event. However, we do have the information to update our overhead viewer and sync its state with the main viewer.