Custom Operators

Many developers wish to provide functionality in addition to what is available in the standard set of HOOPS Visualize operators. You can create a custom operator by subclassing HPS::Operator. Your subclass should then implement the virtual methods required to achieve the desired functionality. A Visualize operator can override any of the mouse, keyboard, or touch-screen handlers simultaneously.

The example below shows how to write an operator to detect when the user clicks the left mouse button while holding the control key.

Step 1: Infrastructure

The first step to processing input events is to make a subclass for your operator and implement OnMouseDown. In this example. we’ll use the generic class name MyCustomOperator. The MouseState is passed in to your method by Visualize and contains information related to the event.

bool MyCustomOperator::OnMouseDown(HPS::MouseState const & in_state)
{
        // implementation details go here - see Step 2

        return true;
}

The return value here is important when multiple operators are active. Multiple operators live in an operator stack (see this section for more information). The operator that was attached to the view most recently receives the input event first, and has the option to process and consume the event or pass it on to the next operator. When returning true, the event is consumed. Returning false means Visualize should pass this event on to the next operator in the stack.

Step 2: Get the Information Provided by the HPS::MouseState.

When handing a mouse event, HOOPS Visualize will pass a HPS::MouseState object to your hander function. If you need to detect, for example, when the user has clicked the left mouse button while holding the CTRL key, you could use the following code:

if (in_state.GetButtons().Left())
{
        if (in_state.GetModifierKeys().Control())
        {
                // perform your logic
        }
}

IMPORTANT: The State objects passed to the event handlers (for example, HPS::MouseState) all describe the state of the device at the time the event is processed. Subsequently, if you try to handle a “mouse button up” event, and inspect the HPS::MouseState, you will find no buttons are active (since the event is processed AFTER the button has come up). If you need this information, you should get the original event from the HPS::MouseState object by calling MouseState::GetActiveEvent().

Visualize supports mice of up to 5 buttons. The code example above only tests for LeftButton, however, the RightButton and MiddleButton enums are also present. The 4th and 5th mouse buttons use the XButton1 and XButton2 enums.

Updating the Display in Response to User Input

Certain events, such as those generated by a mouse move, a touch move, or a timer tick, are often generated rapidly in succession. If your operator includes a display update as part of its logic, you should use synchronous updates to avoid the appearance of the display “lagging behind” the events. Synchronous updates are generated using update notifier.

Interpreting Touch-Screen Events

HPS::Operator has three overridable methods associated with touch-screen events, OnTouchUp, OnTouchDown, and OnTouchMove. They are handled in the same fashion as the mouse events in the code snippets above. There is no touch-drag event in Visualize. If you need to interpret a touch-drag event, you would need to handle OnTouchDown, set a flag, and then examine the state of the flag in OnTouchMove.

Often, a user will interact with a touch screen using more than one finger. For this reason, examining all touch locations is important. For example, one common action is to pinch two fingers together to zoom out of a scene. For this operation, your operator might compute the distance between the fingers to determine if it is increasing or decreasing, and also compute the midpoint so that the scene could be zoomed around the proper 3D point. Visualize provides methods to get the information needed to complete this action:

    bool OnTouchDown(HPS::TouchState const& in_state)
    {
        // get the number of current touches
        size_t numTouches = in_state.GetTouchCount();

        if (numTouches > 0) {
            // get the Touch objects themselves
            TouchArray touchArray = in_state.GetTouches();

            // get location in window space for first touch object
            Touch touch = touchArray.at(0);
            WindowPoint windowPoint = touch.Location;

            return true;
        }

        return false;
    }

Avoid Camera Pitfalls When Working With Operators

An operator is designed to act on a View object. As the view is backed by a segment, any transformations applied to the scene by the operator are ultimately made manifest by changing the segment’s camera. Recall that a camera is an attribute of a segment. As such, it is inherited by subsegments until another segment overrides the camera with its own. Thus, if there happens to be a second camera further down in the segment tree, the geometry viewed by that camera will not be transformed by the current operator. If you try to use an operator and find it is not affecting the scene the way you expect, inspect the segment hierarchy for other cameras that may be overriding the effect of the operator transformation.

Mouse Operator Example

To sum up everything in this section, we’ll create a custom operator that can be used to create a sphere. The user clicks to set the center point and drags to a different spot to set the radius.

    class SphereOperator: public Operator {
      public:
        SphereOperator(Canvas c): canvas(c) {}

        /* member functions inherited from Operator */
        bool OnMouseDown(MouseState const& in_state);
        bool OnMouseUp(MouseState const& in_state);
        bool OnMouseMove(MouseState const& in_state);
        void OnViewAttached(HPS::View const& in_attached_view);

        using Operator::GetName;
        UTF8 GetName();

      private:
        bool ConstructSphereOutline(Point currentPosition);
        float ComputeDistance(Point p1, Point p2);

        Canvas canvas;
        SegmentKey temporaryGeometry; /* segment used for drawing the sphere outline */

        Point centerPoint; /* the center of the sphere */
        LineKey radiusLine; /* a line going from the center of the sphere to its perimeter */
        CircleKey circle; /* a circle representing the outline of the sphere */
        MarkerKey centerMarker; /* marker placed on the center of the sphere*/

        bool operatorStarted;
    };

    /* The OnViewAttached function is executed when the CreateSphere operator is attached to the View
     * It is responsible for setting up the temporary segment used for drawing the sphere outline */
    void SphereOperator::OnViewAttached(HPS::View const& in_attached_view)
    {
        operatorStarted = false;

        Model model = canvas.GetAttachedLayout().GetAttachedView().GetAttachedModel();
        if (model.Type() == HPS::Type::None) {
            model = HPS::Factory::CreateModel();
            canvas.GetAttachedLayout().GetAttachedView().AttachModel(model);
        }

        temporaryGeometry = model.GetSegmentKey().Subsegment();

        temporaryGeometry.GetVisibilityControl().SetLines(true).SetMarkers(true).SetFaces(false).SetEdges(true);

        temporaryGeometry.GetMaterialMappingControl().SetLineColor(RGBAColor(0, 0, 1, 1)).SetMarkerColor(RGBAColor(1, 0, 0, 1));

        temporaryGeometry.GetMarkerAttributeControl().SetSymbol("plus").SetSize(0.3f);

        temporaryGeometry.GetCameraControl()
            .SetProjection(Camera::Projection::Stretched)
            .SetPosition(Point(0, 0, -1))
            .SetTarget(Point(0, 0, 0))
            .SetUpVector(Vector(0, 1, 0))
            .SetField(2, 2);

        temporaryGeometry.GetDrawingAttributeControl().SetOverlay(HPS::Drawing::Overlay::Default);

        return;
    }

    /* The OnMouseDown function is executed when any mouse buttons are pressed
     * If the button pressed is the LEFT button, the center of the sphere is saved */
    bool SphereOperator::OnMouseDown(MouseState const& in_state)
    {
        if (in_state.GetActiveEvent().CurrentButton.Left()) {
            centerPoint = in_state.GetLocation();
            operatorStarted = true;
        }
        return true;
    }

    /* The OnMouseMove function is executed when the mouse is moved
     * If the button LEFT button is pressed while the mouse is being moved, the sphere outline
     * is constructed */
    bool SphereOperator::OnMouseMove(MouseState const& in_state)
    {
        if (IsMouseTriggered(in_state) && operatorStarted)
            return ConstructSphereOutline(in_state.GetLocation());
        return false;
    }

    /* The OnMouseUp function is executed when any mouse button goes up
     * If the button that went up was the LEFT button, the temporary geometry is deleted
     * and a sphere is inserted under the model, in its own segment */
    bool SphereOperator::OnMouseUp(MouseState const& in_state)
    {
        if (in_state.GetActiveEvent().CurrentButton.Left() && operatorStarted) {
            operatorStarted = false;

            radiusLine.Delete();
            centerMarker.Delete();
            circle.Delete();
            temporaryGeometry.Flush(HPS::Search::Type::Geometry, HPS::Search::Space::Subsegments);

            SegmentKey sphere = GetAttachedView().GetAttachedModel().GetSegmentKey().Subsegment();
            sphere.GetMaterialMappingControl().SetFaceColor(RGBAColor(0, 1, 0, 1));

            SprocketPath sprkPath(canvas,
                                  canvas.GetAttachedLayout(),
                                  canvas.GetAttachedLayout().GetAttachedView(),
                                  canvas.GetAttachedLayout().GetAttachedView().GetAttachedModel());
            KeyPath path = sprkPath.GetKeyPath();
            path = sphere + path;

            Point drawPosition0, drawPosition1;
            path.ConvertCoordinate(Coordinate::Space::Window, centerPoint, Coordinate::Space::World, drawPosition0);
            path.ConvertCoordinate(Coordinate::Space::Window, in_state.GetLocation(), Coordinate::Space::World, drawPosition1);
            sphere.InsertSphere(drawPosition0, ComputeDistance(drawPosition0, drawPosition1));

            GetAttachedView().Update();
        }
        return true;
    }

    /* The ConstructSphereOutline function draws the outline of the sphere, which consists in a marker
     * positioned on the center, the sphere radius, and a circle
     * This geometry is drawn under the View, in Overlay */
    bool SphereOperator::ConstructSphereOutline(Point currentPosition)
    {
        radiusLine.Delete();
        centerMarker.Delete();
        circle.Delete();

        SprocketPath sprkPath(canvas,
                              canvas.GetAttachedLayout(),
                              canvas.GetAttachedLayout().GetAttachedView(),
                              canvas.GetAttachedLayout().GetAttachedView().GetAttachedModel());
        KeyPath path = sprkPath.GetKeyPath();
        path = temporaryGeometry + path;

        Point drawPosition0, drawPosition1;
        path.ConvertCoordinate(Coordinate::Space::Window, centerPoint, Coordinate::Space::World, drawPosition0);
        path.ConvertCoordinate(Coordinate::Space::Window, currentPosition, Coordinate::Space::World, drawPosition1);

        centerMarker = temporaryGeometry.InsertMarker(Point(drawPosition0));
        radiusLine = temporaryGeometry.InsertLine(drawPosition0, drawPosition1);
        circle = temporaryGeometry.InsertCircle(drawPosition0, ComputeDistance(drawPosition0, drawPosition1), Vector(0, 0, 1));
        GetAttachedView().Update();
        return true;
    }

    /* The ComputeDistance function returns the distance between two points */
    float SphereOperator::ComputeDistance(Point p0, Point p1)
    {
        return sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y) + (p1.z - p0.z) * (p1.z - p0.z));
    }

Touch-Screen Operator Example

This example accomplishes the same task as the example about creating a sphere, but is oriented toward developers of touch-screen applications. The user clicks to set the center point and drags to a different spot to set the radius.

    class SphereOperator: public Operator {
      public:
        SphereOperator(Canvas c): canvas(c) {}

        /* member functions inherited from Operator */
        bool OnTouchDown(TouchState const& in_state);
        bool OnTouchUp(TouchState const& in_state);
        bool OnTouchMove(TouchState const& in_state);
        void OnViewAttached(HPS::View const& in_attached_view);
        using Operator::GetName;
        UTF8 GetName();

      private:
        bool ConstructSphereOutline(Point currentPosition);
        float ComputeDistance(Point p1, Point p2);

        Canvas canvas;
        SegmentKey temporaryGeometry; /* segment used for drawing the sphere outline */

        Point centerPoint; /* the center of the sphere */
        LineKey radiusLine; /* a line going from the center of the sphere to its perimeter */
        CircleKey circle; /* a circle representing the outline of the sphere */
        MarkerKey centerMarker; /* marker placed on the center of the sphere*/

        bool operatorStarted;
    };

    /* The OnViewAttached function is executed when the CreateSphere operator is attached to the View
     * It is responsible for setting up the temporary segment used for drawing the sphere outline */
    void SphereOperator::OnViewAttached(HPS::View const& in_attached_view)
    {
        operatorStarted = false;

        Model model = canvas.GetAttachedLayout().GetAttachedView().GetAttachedModel();
        if (model.Type() == HPS::Type::None) {
            model = HPS::Factory::CreateModel();
            canvas.GetAttachedLayout().GetAttachedView().AttachModel(model);
        }

        temporaryGeometry = model.GetSegmentKey().Subsegment();

        temporaryGeometry.GetVisibilityControl().SetLines(true).SetMarkers(true).SetFaces(false).SetEdges(true);

        temporaryGeometry.GetMaterialMappingControl().SetLineColor(RGBAColor(0, 0, 1, 1)).SetMarkerColor(RGBAColor(1, 0, 0, 1));

        temporaryGeometry.GetMarkerAttributeControl().SetSymbol("plus").SetSize(0.3f);

        temporaryGeometry.GetCameraControl()
            .SetProjection(Camera::Projection::Stretched)
            .SetPosition(Point(0, 0, -1))
            .SetTarget(Point(0, 0, 0))
            .SetUpVector(Vector(0, 1, 0))
            .SetField(2, 2);

        temporaryGeometry.GetDrawingAttributeControl().SetOverlay(HPS::Drawing::Overlay::Default);

        return;
    }

    /* The OnTouchDown function is executed when any touches occur
     * If we receive a one-finger touch, the center of the sphere is saved */
    bool SphereOperator::OnTouchDown(TouchState const& in_state)
    {
        TouchArray touches = in_state.GetActiveEvent().Touches;

        if (touches.size() == 1) {
            centerPoint = touches[0].Location;
            operatorStarted = true;
        }
        return true;
    }

    /* The OnTouchMove function is executed when the touch is moved
     * If one finger is being used, the sphere outline is constructed */
    bool SphereOperator::OnTouchMove(TouchState const& in_state)
    {
        TouchArray touches = in_state.GetActiveEvent().Touches;

        if (touches.size() == 1 && operatorStarted)
            return ConstructSphereOutline(touches[0].Location);
        return false;
    }

    /* The OnTouchUp function is executed when any touches go up
     * If one touch goes up, the temporary geometry is deleted
     * and a sphere is inserted under the model, in its own segment */
    bool SphereOperator::OnTouchUp(TouchState const& in_state)
    {
        TouchArray upTouches = in_state.GetActiveEvent().Touches;

        if (upTouches.size() == 1 && operatorStarted) {
            operatorStarted = false;

            radiusLine.Delete();
            centerMarker.Delete();
            circle.Delete();
            temporaryGeometry.Flush(Search::Type::Geometry, Search::Space::Subsegments);

            SegmentKey sphere = GetAttachedView().GetAttachedModel().GetSegmentKey().Subsegment();
            sphere.GetMaterialMappingControl().SetFaceColor(RGBAColor(0, 1, 0, 1));

            SprocketPath sprkPath(canvas,
                                  canvas.GetAttachedLayout(),
                                  canvas.GetAttachedLayout().GetAttachedView(),
                                  canvas.GetAttachedLayout().GetAttachedView().GetAttachedModel());
            KeyPath path = sprkPath.GetKeyPath();
            path = sphere + path;

            Point drawPosition0, drawPosition1;
            path.ConvertCoordinate(Coordinate::Space::Window, centerPoint, Coordinate::Space::World, drawPosition0);
            path.ConvertCoordinate(Coordinate::Space::Window, upTouches[0].Location, Coordinate::Space::World, drawPosition1);
            sphere.InsertSphere(drawPosition0, ComputeDistance(drawPosition0, drawPosition1));

            GetAttachedView().Update();
        }
        return true;
    }

    /* The ConstructSphereOutline function draws the outline of the sphere, which consists in a marker
     * positioned on the center, the sphere radius, and a circle
     * This geometry is drawn under the View, in Overlay */
    bool SphereOperator::ConstructSphereOutline(Point currentPosition)
    {
        radiusLine.Delete();
        centerMarker.Delete();
        circle.Delete();

        SprocketPath sprkPath(canvas,
                              canvas.GetAttachedLayout(),
                              canvas.GetAttachedLayout().GetAttachedView(),
                              canvas.GetAttachedLayout().GetAttachedView().GetAttachedModel());
        KeyPath path = sprkPath.GetKeyPath();
        path = temporaryGeometry + path;

        Point drawPosition0, drawPosition1;
        path.ConvertCoordinate(Coordinate::Space::Window, centerPoint, Coordinate::Space::World, drawPosition0);
        path.ConvertCoordinate(Coordinate::Space::Window, currentPosition, Coordinate::Space::World, drawPosition1);

        centerMarker = temporaryGeometry.InsertMarker(Point(drawPosition0));
        radiusLine = temporaryGeometry.InsertLine(drawPosition0, drawPosition1);
        circle =
            temporaryGeometry.InsertCircle(drawPosition0, ComputeDistance(drawPosition0, drawPosition1), HPS::Vector(0, 0, 1));
        GetAttachedView().Update();
        return true;
    }

    /* The ComputeDistance function returns the distance between two points */
    float SphereOperator::ComputeDistance(Point p0, Point p1)
    {
        return sqrt((p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y) + (p1.z - p0.z) * (p1.z - p0.z));
    }