Advanced Markup

In addition to the default markup provided, you can create your own customized markup. You will need a class for your markup item that extends the Markup.MarkupItem class. Typically, you will also want an operator that can interact with the model and respond to mouse events to add your markup to the scene. Additionally, you may want your markup included with the default markup when exportMarkup() is called, which will require a custom markup item manager class to serialize and deserialize your markup items.

Custom markup item

Creating a custom markup item class that extends the markup item base class allows you to customize its behavior and appearance. Typically, markup items will be registered with the markup manager, which will manage calling the functions on the markup item like draw, remove, hit, etc.

  1. draw() – The draw function is called when the markup item should be redrawn. When the camera changes, the scene is redrawn, and elements in the SVG layer are removed. If the markup is drawn with SVG, the markup renderer can be used to draw the markup. The markup renderer supports drawing predefined shapes (circle, rectangle, polyline, polygon, text box, etc.). If your markup involves non-SVG elements, you will need to manage those separately. For example, 3D geometry will not be removed when the camera changes, HTML elements may need to have position updated if the position depends on the markup location, etc.

  2. remove() – Any cleanup that needs to be done when the markup item is removed should be done here. To remove a markup element, remove() should not be explicitly called. Instead, the markup should be unregistered with the markup manager using the unregisterMarkup() function. For SVG elements, there is no extra cleanup required (after the markup is unregistered with the markup manager, the SVG will no longer be rendered). If there are additional non-SVG elements that require removal, you will need to manage those separately. For example, removing 3D geometry or HTML elements.

  3. hit() – The hit function is called with a 2D screen space coordinate to determine if the markup element is at that coordinate. You will need to implement this function to be able to select your markup. It is up to you to determine what counts as a ‘hit’ for your markup item. Note: The markup renderer has useful functions for determining the size of text and text boxes (measureTextBox() and measureText()).

  4. toJson() – The toJson() function creates a JSON object with the markup data. Only data that is required to reconstruct the markup item needs to be included in the JSON object.

Custom markup item operator

Creating an operator for our markup item will allow us to place and interact with our markup in the scene. The operator will create the markup item and register it with the markup manager. If there is any interaction with the markup item after it has been created, the operator will handle that interaction as well.

Custom markup item manager

A markup item manager typically will store a list of markup items created. The markup item manager can be registered with the markup manager. This allows the markup manager to include the custom markup item with the default markup items exported when exportMarkup() is called. A markup item manager needs to extend the MarkupTypeManager. The two functions that are required to be implemented are exportMarkup() and loadData().

  1. exportMarkup() – The exportMarkup() function creates an array of JSON objects. Each object corresponds to one markup item.

  2. loadData() – This function takes an array of JSON objects and uses them to reconstruct each markup item. It returns a promise with an array of Booleans indicating if each markup item was successfully constructed.

Example

We will create a custom callout markup item. When the model is selected, a callout with the part name will display. The callout will be drawn with SVG, and consist of an arrow, a line along the selection normal, and a text box with the part name.

../../../_images/advanced_markup_example.png

Custom callout markup item

Create a CustomMarkupItem class that derives from the Communicator.Markup.MarkupItem class. The markup item class will store data about our markup item. We will need to implement the draw() function, which will render our markup in the scene, and the hit() function which will allow our markup to be selected.

    export class CustomMarkupItem extends Markup.MarkupItem {
        private _viewer: WebViewer;
        private _nodeId: NodeId;
        private _nodeName: string | null;
        private _selectionPosition: Point3;
        private _normal: Point3;
        private _length: number;

        constructor(
            viewer: WebViewer,
            nodeId: NodeId,
            nodeName: string,
            selectionPosition: Point3,
            normal: Point3,
            length: number,
        ) {
            super();
            this._viewer = viewer;
            this._nodeId = nodeId;
            this._nodeName = nodeName;
            this._selectionPosition = selectionPosition.copy();
            this._normal = normal.copy();
            this._length = length;
        }
    }

Our callout markup item will have a line with an arrow pointing to the part that was selected and a text box containing the name of the part. The line and arrow can be drawn with a Markup.Shape.Polyline, and the textbox will use a Markup.Shape.TextBox. In the constructor, we will create the polyline and text box, and set the styles that will be used.


            this._lineGeometryShape.setStrokeWidth(2);
            this._lineGeometryShape.setStrokeColor(
                this._viewer.measureManager.getMeasurementColor(),
            );

            this._lineGeometryShape.setStartEndcapType(Markup.Shape.EndcapType.Arrowhead);

            this._textBox.getBoxPortion().setFillOpacity(1);
            this._textBox.getBoxPortion().setFillColor(new Color(255, 255, 255));
            this._textBox.setTextString(this._nodeName);

Add the draw function to the custom markup item class. The draw function is used to render SVG to the screen. It is called each time the markup needs to be rendered. We will use the markup manager’s Markup.MarkupRender to draw the SVG. The selection position is a world space coordinate, and will need to be converted to screen space to render the polyline. The box position will be calculated in world space, and also converted to screen space. Then the selection position and box position are used to update the polyline points and box position. Finally, the renderer draws the SVG.

        public draw() {
            const renderer = this._viewer.markupManager.getRenderer();

            const view = this._viewer.view;

            this._lineGeometryShape.clearPoints();
            this._lineGeometryShape.pushPoint(
                Point2.fromPoint3(view.projectPoint(this._selectionPosition)),
            );

            const secondPoint = this._selectionPosition
                .copy()
                .add(this._normal.copy().scale(this._length));
            const secondPointProjected = Point2.fromPoint3(view.projectPoint(secondPoint));
            this._lineGeometryShape.pushPoint(secondPointProjected);

            this._textBox.setPosition(secondPointProjected);

            renderer.drawPolyline(this._lineGeometryShape);
            renderer.drawTextBox(this._textBox);
        }

The hit function allows the markup item to be selected. It takes a 2D screen space point and decides if the markup item has been ‘hit’. In this case, we will use the bounding of the text box, and declare a hit if the point is inside.

        public hit(point: Point2): boolean {
            const measurement = this._viewer.markupManager
                .getRenderer()
                .measureTextBox(this._textBox);

            const position = this._textBox.getPosition();

            if (point.x < position.x) return false;
            if (point.x > position.x + measurement.x) return false;
            if (point.y < position.y) return false;
            if (point.y > position.y + measurement.y) return false;

            return true;
        }

The next step in adding the markup to the scene will be creating an operator. The operator will create the markup, and update the length of the callout as the mouse is moved. In order to calculate the new length of the callout, the operator needs to have access to the selection position and normal, and set the markup length property. Add the following getters and setters to the custom markup item.

        public getPosition(): Point3 {
            return this._selectionPosition.copy();
        }

        public getNormal(): Point3 {
            return this._normal.copy();
        }

        public setLength(length: number): void {
            this._length = length;
        }

Custom markup operator

Create a CustomMarkupOperator class that derives from the Communicator.Operator.Operator class.

    export class CustomMarkupOperator implements Operator.Operator {
        private _viewer: WebViewer;

        constructor(viewer: WebViewer) {
            this._viewer = viewer;
        }

        public onMouseDown(event: Communicator.Event.MouseInputEvent): void {
            // Mouse down
        }

        public onMouseMove(event: Communicator.Event.MouseInputEvent): void {
            // Mouse move
        }

        public onMouseUp(event: Communicator.Event.MouseInputEvent): void {
            // Mouse up
        }
    }

The first function, addMarkupItem(), accepts a 2D screen position coordinate and performs a selection on the model at that coordinate. If the selection is successful, it creates a callout markup at that selection position in world space. When onMouseDown is called, it will use the event parameter to get the position, and call addMarkupItem() with it. We will also need to add to class variables to keep track of the state, handled, and activeMarkupItem variables. The handled variable is used to prevent other operators on the stack from interacting with the scene at the same time we are dragging to create our markup item. The activeMarkupItem variable is used to keep track of the most recently created markup item.

        private async _addMarkupItem(position: Point2): Promise<void> {
            const model = this._viewer.model;

            const config = new PickConfig(SelectionMask.Face);
            const selectionItem = await this._viewer.view.pickFromPoint(position, config);
            if (selectionItem === null || !!selectionItem.overlayIndex()) {
                return;
            }

            const nodeId = selectionItem.getNodeId();
            const selectionPosition = selectionItem.getPosition();
            const faceEntity = selectionItem.getFaceEntity();

            if (nodeId === null || selectionPosition === null || faceEntity === null) {
                return;
            }

            const parentNodeId = model.getNodeParent(nodeId);
            if (parentNodeId === null) {
                return;
            }

            const nodeName = model.getNodeName(nodeId);
            if (nodeName === null) {
                return;
            }

            const normal = faceEntity.getNormal();

            this._activeMarkupItem = new CustomMarkupItem(
                this._viewer,
                nodeId,
                nodeName,
                selectionPosition,
                normal,
                0,
            );
            this._viewer.markupManager.registerMarkup(this._activeMarkupItem);

            this._handled = true;
        }

The next function, updateActiveMarkupItem, takes a 2D screen position coordinate and updates the markup length. To do this, it creates a line along the view plane normal at the screen position, and finds the closest point on the line from the selection position along the selection normal using the utility function lineLineIntersect. The markup length is then updated.

        private _updateActiveMarkupItem(position: Point2): void {
            if (this._activeMarkupItem === null) {
                return;
            }

            const p1 = this._activeMarkupItem.getPosition();
            const p2 = Point3.add(this._activeMarkupItem.getNormal(), p1);
            const p3 = this._viewer.view.unprojectPoint(position, 0);
            const p4 = this._viewer.view.unprojectPoint(position, 0.5);

            if (p3 === null || p4 === null) {
                return;
            }

            const intersection = Util.lineLineIntersect(p1, p2, p3, p4);
            if (intersection === null) {
                return;
            }

            const length = Point3.subtract(intersection, p1).length();
            this._activeMarkupItem.setLength(length);
            this._viewer.markupManager.refreshMarkup();
        }

The onMouseDown(), onMouseMove(), and onMouseUp() functions allow an operator to interact with mouse events. We will use these functions to create our markup item on mouse down, and update it on mouse move. In onMouseDown, we see if a markup item is under the cursor. If so, we will adjust the length of the selected markup item, otherwise, we will use the mouse position to add a markup item if the model selection is successful. If successful, the handled variable will be true, and activeMarkupItem will contain the created markup item. In onMouseMove, we will use the mouse position to update the length of the markup item line segment. In onMouseUp, we set the activeMarkupItem to null, as we no longer want to update it afterwards.

        public onMouseDown(event: Event.MouseInputEvent): void {
            this._handled = false;

            const markup = this._viewer.markupManager.pickMarkupItem(event.getPosition());
            if (markup instanceof CustomMarkupItem) {
                this._activeMarkupItem = markup;
                this._handled = true;
            } else {
                this._addMarkupItem(event.getPosition());
            }
        }

        public onMouseMove(event: Event.MouseInputEvent): void {
            event.setHandled(this._handled);

            if (this._activeMarkupItem !== null && this._handled) {
                this._updateActiveMarkupItem(event.getPosition());
            }
        }

        public onMouseUp(event: Event.MouseInputEvent): void {
            event.setHandled(this._handled);
            this._activeMarkupItem = null;
        }

HTML page

Create an HTML page with an instance of the viewer. Make sure to include the CustomMarkupExample.js and CustomMarkupOperator.js files.

Create an instance of our custom markup operator and push it onto the operator stack.

    const operatorManager = viewer.operatorManager;
    const markupOperator = new CustomMarkupOperator(viewer);
    const markupOperatorHandle = operatorManager.registerCustomOperator(markupOperator);
    operatorManager.push(markupOperatorHandle);

Markup callbacks

There are several callbacks that are triggered when markup is created, deleted, or manipulated.

Line markup

Line markup items are 3D geometry that is drawn in the scene. It is exported by default with the markup manager exportMarkup() function. The following callbacks relate to line creation, deletion, and loading from exported markup data:

  • lineCreated

  • lineDeleted

  • lineLoaded

Measurement

Measurement markup items are drawn as 2D SVG and redrawn each time the camera changes to appear 3D. The following callbacks relate to measurement creation, deletion, visibility, and updating values:

  • measurementBegin

  • measurementCreated

  • measurementDeleted

  • measurementHidden

  • measurementShown

  • measurementLoaded

  • measurementValueSet

Redline

Redline markup is a 2D markup drawn as SVG that is associated with a camera and markup view. If the camera changes, any redline shown will be hidden. The following callbacks relate to redline creation, deletion, and updating position / text:

  • redlineCreated

  • redlineDeleted

  • redlineUpdated