Custom Operators

Overview

In order for the user to interact with the scene, your application needs to respond to various input events that might be coming from the mouse, the keyboard, or a touch device. While you are free to handle user input yourself and make calls into the Web Viewer API in response to them, HOOPS Communicator provides the concept of “operators” for this purpose. An operator is a hook into the user input event loop with a predefined structure to respond to user input in a sensible way.

For example, one operator might handle a mouse click which selects geometry. Another might handle keystrokes. Communicator can have any number of operators active at the same time.

The Web Viewer offers a set of standard operators for the most common user interactions such as camera manipulation, selection, and inserting markup, but you can also write your own operators with completely custom functionality.

Creating custom operators

Custom operators are JavaScript objects that implement the Communicator.Operator interface. To write an operator you derive from the Operator base class and implement some of the methods related to the various events coming from the browser.

In the example below we define a simple operator that only responds to the onMouseDown() and onMouseUp() events:

    class SampleOperator implements Communicator.Operator.Operator {
        private _viewer: Communicator.WebViewer;
        private _partId: Communicator.PartId;

        public constructor(viewer: Communicator.WebViewer) {
            this._viewer = viewer;
            this._partId = null;
        }

        public onMouseDown(event: Communicator.Event.MouseInputEvent): void {
            const pickConfig = new Communicator.PickConfig(Communicator.SelectionMask.All);
            this._viewer.view.pickFromPoint(event.getPosition(), pickConfig).then((selection) => {
                if (selection.getSelectionType() !== Communicator.SelectionType.None) {
                    this._partId = selection.getNodeId();
                    this._viewer.model.setNodesOpacity([this._partId], 0.5);
                }
            });
        }

        public onMouseUp(event: Communicator.Event.MouseInputEvent): void {
            if (this._partId !== null) {
                this._viewer.model.setNodesOpacity([this._partId], 1.0);
                this._partId = null;
            }
        }

        public onMouseMove(event: Communicator.Event.MouseInputEvent): void {}
        public onMousewheel(event: Communicator.Event.MouseWheelInputEvent): void {}
        public onTouchStart(event: Communicator.Event.TouchInputEvent): void {}
        public onTouchMove(event: Communicator.Event.TouchInputEvent): void {}
        public onTouchEnd(event: Communicator.Event.TouchInputEvent): void {}
        public onKeyDown(event: Communicator.Event.KeyInputEvent): void {}
        public onKeyUp(event: Communicator.Event.KeyInputEvent): void {}
        public onDeactivate(): void {}
        public onActivate(): void {}
        public onViewOrientationChange(): void {}
        public stopInteraction(): void {}
    }

Every event handler receives an event object related to its type of event. For mouse events, an example would be the type of MouseInputEvent, for touch events, the type is TouchInputEvent. The event object has various member functions to retrieve the mouse location, button status, and other relevant information.

Operators can be as simple as the one above that turn an object transparent while the mouse is pressed, or they can be much more complex and have multiple stages like an operator that performs a measurement operation and then positions the resulting markup based on user input. It is up to you to decide how complex you want to make the interaction that is encapsulated by a single operator.

When using custom operators, you should await the call to the parent class’ function before returning from your implementation. This can be achieved either by adding await to the parent’s function call:

async onMouseMove(event) {
          await super.onMouseMove(event);
}

Or by returning the result of the parent’s function call:

async onMouseDown(event) {
         return super.onMouseDown(event);
}

For a list of all Event objects please see the API Reference Manual.

Registering an operator

Custom operators must be registered with the OperatorManager. Registering the operator makes it available but does not activate it.

    const myOperator = new SampleOperator(hwv);
    var myOperatorHandle = hwv.operatorManager.registerCustomOperator(myOperator);

Registering an operator returns an identifier which you can then use to activate the operator later. Each custom operator has a unique handle.

Activating an operator

There are two ways to activate a registered operator. The first one is to simply push it on the operator “stack”.

    hwv.operatorManager.push(myOperatorHandle);

The OperatorManager maintains a stack of operators that are all active at the same time and receive events based on their position in the stack with the operator pushed on the stack last getting to “see” the events first. When starting the Web Viewer this is what the operator stack looks like:

  1. Navigate

  2. Select

  3. CuttingPlane

  4. Handles

  5. NavCube

  6. AxisTriad

When using the push function your custom operator will be added to the end of the stack (position 6 in this case) and be first in line to receive events. You can then “pop” it from the stack at any time with the OperatorManager.pop() function.

You can also “replace” an existing operator on the stack at a specific position with OperatorManager.set():

    hwv.operatorManager.set(myOperatorHandle, 1);

In this case, the default select operator is replaced by your custom operator at position 1 in the stack. If you want to remove an operator from the stack without replacing it, you can call the OperatorManager.remove() function. Be aware that this function will modify the position of all operators above it in the stack.

If you prefer to start over and define your operator stack from scratch, you can use the OperatorManager.clear() function which completely empties the operator stack.

To retrieve a particular Operator object (to call functions on it, change settings, etc.) you can use the OperatorManager.getOperator() function. There can only be one object of a given type on the operator stack at a time, so you only need to pass the relevant operator ID to this function.

Event propagation

All operator events, such as OnMouseUp() and OnMouseDown() are sequentially propagated to all operators on the stack starting with the operator highest on the stack. This is so that multiple operators can be active at the same time and effectively run in parallel. Because of this, an operator must be carefully designed to not interfere with the behavior of other operators or the application must ensure that the operator is the only one active.

If an operator handles an event of a given type it often needs to “consume” the event meaning it needs to stop it from propagating down to the operators lower in the stack. In the case of the NavCubeOperator, for example, this is because the operator might have determined that the NavCube element has been clicked on. In that case, it makes no sense to send the mouse messages also to the Orbit operator which would result in an unexpected camera movement. Another example might be an operator only “interested” mouse events but letting all other event types (e.g. keyboard events) pass through.

To consume an event, simply call setHandled(true) on the event object in the function that processes the event. If this function is called all the operators lower in the stack will not see the event but instead, receive a stopInteraction() event indicating that their current action (e.g. orbiting the camera, moving the cutting plane, etc) should be canceled. You should make sure that your custom operator also handles the stopInteraction() event.

For a more advanced Operator Example please consult the Advanced operator concepts section in the Building a basic application tutorial.

Built-in operators

The Web Viewer supports several predefined operators in various categories. All those operators are available with fully readable source-code. We encourage you to browse through the source code to understand some of the more advanced design patterns when writing operator code.

Activating a predefined operator

The Communicator.OperatorId enum stores a list of all predefined operator IDs. To activate one of them, simply use one of the operator IDs with the push or set functions of the OperatorManager:

    hwv.operatorManager.set(Communicator.OperatorId.KeyboardWalk, 0);

As with the custom operators you need to be mindful of how an activated operator interacts with other operators already on the stack. For example, if you want to use one of the walk operators you should not just push it to the top of the stack but instead, replace the existing camera-related operator (Navigate in the default configuration) with it. If in doubt it is best to experiment to see how the predefined operators interact with each other and define your operator stack accordingly.

Operator categories

The built-in operators fall into a few major categories:

Camera

Selection

Markup

Measurement

Misc.

Please see the reference manual for more information on each operator category. Some of them will also be discussed throughout this Programming Guide.