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

.. code-block:: js

	class MeasureBetweenPointsOperator {
		constructor(hwv) {
			this._hwv = hwv;
		} 

		onMouseMove(event) {
		
		}
		
		onMouseDown(event) {
		
		}
	}

Include *js/MeasureOperator.js* in your HTML page as seen here:

.. code-block:: html

    <script type="module">
        import * as Communicator from "/js/hoops/hoops-web-viewer.mjs";
        import { Menu } from "/js/Menu.js";
        import { SelectOperator } from "/js/SelectOperator.js";
        ...
    </script>


Handling the "onMouseDown" event
================================

Next, let's start filling in the :doc:`onMouseDown </api_ref/viewing/interfaces/Communicator.Operator.Operator>` 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 :doc:`pickFromPoint() </api_ref/viewing/classes/Communicator.View>`. As a first step, we are going to print the position to the console via ``console.log(...)``.

.. code-block:: js

	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 *index.html*:

.. code-block:: 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.

.. code-block:: js

	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.

.. code-block:: js

	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 :doc:`projectPoint() </api_ref/viewing/classes/Communicator.View>` on the view object (for details, see :doc:`Selection and Picking </prog_guide/viewing/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:

.. code-block:: js

	constructor(hwv) {
		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 :doc:`MarkupManager </api_ref/viewing/classes/Communicator.MarkupManager>`. Update the code of ``onMouseDown`` in *MeasureOperator.js* to the following (we will implement the ``DistanceMarkup`` class shortly):

.. code-block:: js

	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, this._hwv.view);
				} 
			  
				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 :doc:`MarkupManager </api_ref/viewing/classes/Communicator.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:

.. code-block:: js

	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(this._hwv.view);
		}
	}

Every user-defined markup object needs to derive from the :doc:`MarkupItem </api_ref/viewing/classes/Communicator.Markup.MarkupItem>` class. On every update, the :doc:`MarkupManager </api_ref/viewing/classes/Communicator.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:

.. code-block:: js

	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 :doc:`pickFromPoint() </api_ref/viewing/classes/Communicator.View>` function into 2D space. This is accomplished by the :doc:`projectPoint() </api_ref/viewing/classes/Communicator.View>` 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 :doc:`drawCircle() </api_ref/viewing/interfaces/Communicator.Markup.MarkupRenderer>` 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 :doc:`Programming Guide </prog_guide/viewing/markup/markup-basics>`.


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.

.. image:: images/measurement_operator.png

You will notice that the operator "snaps" to each visible edge within a given proximity. This behavior can be controlled either by configuring the :doc:`pickFromPoint </api_ref/viewing/classes/Communicator.View>` function with the :doc:`PickConfig </api_ref/viewing/classes/Communicator.PickConfig>` class or by modifying the pick tolerance via :doc:`SelectionManager.setPickTolerance </api_ref/viewing/classes/Communicator.SelectionManager>`.


Displaying line and distance
============================

To improve our operator, we can draw a line between the two points, as well as indicate the distance between the points as text. Update ``draw`` with the following:

.. code-block:: js

	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:

.. code-block:: js

	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(this._hwv.view);
			}
		});
	}

As you can see, we handle selection similar to the :doc:`onMouseDown </api_ref/viewing/interfaces/Communicator.Operator.Operator>` 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:

.. code-block:: js

	// 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 sub-assemblies, 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:

.. code-block:: js

	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:

.. code-block:: js

	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:

.. code-block:: js

	// 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 :doc:`Programming Guide </prog_guide/viewing/operators/custom-operators>`.
