Advanced operator concepts
Summary
In this section, we discuss how to create a more advanced multi-stage operator that consumes various mouse events, and requires a more complex interaction. We also demonstrate how to manage markup data.
Concepts
Multi-stage operator design
Creating and displaying markup
Picking
Measurement units
Creating the measure operator class
We start once again with an empty JS document called js/MeasureOperator.js and then derive a new operator from the base operator interface. To review, we will implement our custom operator by completing the same three steps from the previous section:
Create the custom operator class, which will be extended from Communicator.Operator
Add an event handler to the event of choice (in this case, onMouseDown)
Register the custom operator with the Web Viewer
Let’s create an operator that will measure the distance between two points. Create a new file called js/MeasureOperator.js with the following code:
class MeasureBetweenPointsOperator {
constructor(hwv) {
super();
this._hwv = hwv;
}
onMouseMove(event) {
}
onMouseDown(event) {
super.onMouseDown(event);
}
}
Include js/MeasureOperator.js in your HTML page as seen here:
<script type="text/javascript" src="js/hoops/hoops_web_viewer.js"></script>
<script type="text/javascript" src="js/Menu.js"></script>
<script type="text/javascript" src="js/SelectOperator.js"></script>
<script type="text/javascript" src="js/MeasureOperator.js"></script>
Handling the “onMouseDown” event
Next, let’s start filling in the onMouseDown event. Our operator should measure the distance between two points on a model. Both of these points will be chosen based on mouse clicks of the user.
The snippet of code below looks very similar to the code we wrote for the previous operator. Now, instead of highlighting the entity the user has clicked on, we are determining the selection position from the selectionItem
returned by pickFromPoint(). As a first step, we are going to print the position to the console via console.log(...)
.
onMouseDown(event) {
var config = new Communicator.PickConfig(Communicator.SelectionMask.Face | Communicator.SelectionMask.Line);
this._hwv.selectionManager.clear();
this._hwv.view.pickFromPoint(event.getPosition(), config).then((selectionItem) => {
if (selectionItem.getNodeId() != null) {
var position = selectionItem.getPosition();
console.log(position);
}
});
}
Registering the new operator
Let’s add this new operator to our menu and register it in gettingstarted.html:
<select id="operatorType">
<option value="Orbit">Orbit</option>
<option value="Area Select">Area Select</option>
<option value="Select">Select</option>
<option value="Measure">Measure</option>
</select>
Add the code below to the top of the ``_initEvents function in the Menu.js file.
this.measureOperator = new MeasureBetweenPointsOperator(this._hwv);
this.measureOperatorId = this._hwv.registerCustomOperator(this.measureOperator);
Finally, update the conditionals in customOperatorSelect.onclick()
inside the Menu.js file.
if (customOperatorSelect.value === "Area Select") {
this._hwv.operatorManager.push(Communicator.OperatorId.AreaSelect);
}
else if (customOperatorSelect.value === "Select") {
this._hwv.operatorManager.push(this.selectOperatorId);
}
else if (customOperatorSelect.value === "Measure") {
this._hwv.operatorManager.push(this.measureOperatorId);
}
Bring up your JavaScript console in your browser and run this code. After performing a measurement, you will notice that the returned position is in 3D world space. If you need the 2D position of the pick location within the window you can use projectPoint() on the view object (for details, see Selection and Picking).
Creating a markup element
One way to mark the selected position in the Web Viewer component is to add a markup element. In order to do this, we need to create a new variable holding the markup element in the constructor. Update the constructor in MeasureOperator.js with the code below:
constructor(hwv) {
super();
this._hwv = hwv;
this._activeIndication = null;
}
When the user picks a point on the model, we will create a new markup element and register it with the MarkupManager. Update the code of onMouseDown
in MeasureOperator.js to the following (we will implement the DistanceMarkup
class shortly):
onMouseDown(event) {
var config = new Communicator.PickConfig(Communicator.SelectionMask.Face | Communicator.SelectionMask.Line);
this._hwv.selectionManager.clear();
this._hwv.view.pickFromPoint(event.getPosition(), config).then((selectionItem) => {
if (selectionItem.getNodeId() !== null) {
var position = selectionItem.getPosition();
var markupManager = this._hwv.markupManager;
if (this._activeIndication === null) {
this._activeIndication = new DistanceMarkup(this._hwv, position);
markupManager.registerMarkup(this._activeIndication);
}
else {
this._activeIndication.point2 = position;
this._activeIndication.finalize();
this._activeIndication = null;
}
}
});
}
Markup elements in the Web Viewer are rendered as SVG elements on top of the 3D window. Therefore, they will always be drawn in front of the geometry. Additionally, they must be manually transformed on every camera change if they are meant to be anchored to specific points on the model.
We know that if the user has clicked on the model but no markup element exists yet, we need to create a new markup element that will hold the geometry for this markup operation. This element needs to be registered with the MarkupManager (we will define that object next). If it already exists, we know that the first point has already been defined and we need to set the second point of the operation to finish the markup.
Defining the markup class
Now, let’s look at how we define our markup object class. Add the following class to the MeasureOperator.js file:
class DistanceMarkup extends Communicator.Markup.MarkupItem {
constructor(hwv, point) {
super();
this._hwv = hwv;
this.point1 = point;
this.point2 = null;
this._isFinalized = false;
}
draw() {
// Draw at the 'click' locations
}
finalize() {
this._isFinalized = true;
this._hwv.markupManager.refreshMarkup();
}
}
Every user-defined markup object needs to derive from the MarkupItem class. On every update, the MarkupManager cycles through all registered markup items and will execute their draw method.
You will notice we are defining two points, which are the start and endpoints of the measurement. The first point is already set in the constructor when the markup object is getting created. The finalize
method indicates the operator has completed, and the markup is not being edited anymore.
Implementing the draw method
Now let’s look at the draw method:
draw() {
var view = this._hwv.view;
if (this.point1 !== null) {
// draw the first point
var circle = new Communicator.Markup.Shapes.Circle();
var point3d = view.projectPoint(this.point1);
circle.set(Communicator.Point2.fromPoint3(point3d), 2.0);
this._hwv.markupManager.getRenderer().drawCircle(circle);
if (this.point2 !== null) {
// draw the second point
point3d = view.projectPoint(this.point2);
circle.set(Communicator.Point2.fromPoint3(point3d), 2.0);
this._hwv.markupManager.getRenderer().drawCircle(circle);
}
}
}
For now, we just want to draw two points at the “click” locations. To do this we pick the appropriate markup shape element (in this case: circle).
Note
Markup is always defined in 2D space so we need to project the 3D points that are returned in the pickFromPoint() function into 2D space. This is accomplished by the projectPoint() function which takes a 3D position and translates it into a 2D position based on the current camera.
This point is then be used to define the center of the circle. In this case, we choose a fixed radius of 2
. This is enough to draw a circle with otherwise default attributes via the drawCircle() function. If we have already defined the second point, we can draw this point as well.
For more details on the markup capabilities of the HOOPS Web Viewer component, please refer to the Programming Guide.
Running the application
Let’s run our code and see the result. After selecting the operator, two individual clicks will select the start and endpoint of the measurement. After that, the operator starts over and a new measurement is created.
You will notice that the operator “snaps” to each visible edge within a given proximity. This behavior can be controlled either by configuring the pickFromPoint function with the PickConfig class or by modifying the pick tolerance via SelectionManager.setPickTolerance.
Displaying line and distance
To improve our operator, we can draw a line between the two points, as well as indicate the disctance between the points as text. Update draw
with the following:
draw() {
var view = this._hwv.view;
if (this.point1 !== null) {
// draw the first point
var circle = new Communicator.Markup.Shapes.Circle();
var point3d = view.projectPoint(this.point1);
circle.set(Communicator.Point2.fromPoint3(point3d), 2.0);
this._hwv.markupManager.getRenderer().drawCircle(circle);
if (this.point2 !== null) {
// draw the second point
point3d = view.projectPoint(this.point2);
circle.set(Communicator.Point2.fromPoint3(point3d), 2.0);
this._hwv.markupManager.getRenderer().drawCircle(circle);
// draw a line between the points
var line = new Communicator.Markup.Shapes.Line();
var point3d1 = view.projectPoint(this.point1);
var point3d2 = view.projectPoint(this.point2);
line.setP1(point3d1);
line.setP2(point3d2);
this._hwv.markupManager.getRenderer().drawLine(line);
// add a label
var text = new Communicator.Markup.Shapes.Text();
text.setFillColor(Communicator.Color.red());
var midpoint = new Communicator.Point3((point3d1.x+point3d2.x)/2, (point3d1.y+point3d2.y)/2,(point3d1.z+point3d2.z)/2);
text.setText(Communicator.Point3.subtract(this.point2,this.point1).length().toFixed(2));
text.setPosition(midpoint);
this._hwv.markupManager.getRenderer().drawText(text);
}
}
}
Now, on the first click, a circle will be drawn to the first point. On the second click, another circle will be drawn with a line spanning the distance. Text markup will should appear at the calculated midpoint of the line to show the distance.
Drawing the markup on mouse move
To make the operator more interactive, it would be nice if the markup would be drawn while the user moves the mouse and not only after the second click. For that, we need to also handle the mouse move events in the operator:
onMouseMove(event) {
if (this._activeIndication === null) {
return;
}
var config = new Communicator.PickConfig(Communicator.SelectionMask.Face | Communicator.SelectionMask.Line);
this._hwv.selectionManager.clear();
this._hwv.view.pickFromPoint(event.getPosition(), config).then((selectionItem) => {
if (selectionItem.getNodeId() !== null) {
var position = selectionItem.getPosition();
this._activeIndication.point2 = position;
this._hwv.markupManager.refreshMarkup();
}
});
}
As you can see, we handle selection similar to the onMouseDown event, fill out the second point and then force a refresh of the markup which normally only gets redrawn if the camera changes.
Different style before finalizing
Also, let’s draw the line a bit differently if the markup is not yet finalized. Modify the code in the draw()
function from the MeasureOperator.js file to the following:
// draw a line between the points
var line = new Communicator.Markup.Shapes.Line();
var point3d1 = view.projectPoint(this.point1);
var point3d2 = view.projectPoint(this.point2);
line.setP1(point3d1);
line.setP2(point3d2);
if (!this._isFinalized) {
line.setStrokeWidth(5);
}
this._hwv.markupManager.getRenderer().drawLine(line);
Handling model units
One issue we haven’t addressed is the unit of measurement. Without retrieving the so-called “unit multiplier” from a model, the distance we calculated is unitless.
Note
The measurement units for a model are usually created by the author of the CAD model in the original CAD application. In some cases, units can also vary per model tree node for different subassemblies, so they have to be retrieved on a per-node basis.
Change this._activeIndication = new DistanceMarkup(this._hwv, position);
in the MeasureOperator::onMouseDown()
function to the line below:
this._activeIndication = new DistanceMarkup(this._hwv, position, this._hwv.model.getNodeUnitMultiplier(selectionItem.getNodeId()));
We need to add this unit value as an additional parameter to the DistanceMarkup constructor:
constructor(hwv, point, unit) {
super();
this._hwv = hwv;
this.point1 = point;
this.point2 = null;
this._unit = unit;
this._isFinalized = false;
}
Then, apply the units to the text by modifying the end of the draw
function:
// add a label
var text = new Communicator.Markup.Shapes.Text();
text.setFillColor(Communicator.Color.red());
var midpoint = new Communicator.Point3((point3d1.x + point3d2.x) / 2, (point3d1.y + point3d2.y) / 2, (point3d1.z + point3d2.z) / 2);
var length = Communicator.Point3.subtract(this.point2, this.point1).length().toFixed(2) * this._unit;
text.setText(length + "mm");
text.setPosition(midpoint);
this._hwv.markupManager.getRenderer().drawText(text);
We have now given you an overview of the basic building blocks for writing a more advanced operator similar to our internal measurement or markup operators. For more information please see the Programming Guide.