25 Aug 2017

Customize viewer context menu

Default blog image

A couple of customers asked me how to customize the viewer context menu to add extra menu items to the default menu or create their owned menu without default menu items.

Add new items to the existing context menu is quite easy, Forge viewer allows users add menu items via a simple API function call to register your owned menu items to the context menu. All you need is doing something like the following codes, this code snippet is demonstrating how to add two menu items. One is called "Override color of selected elements to the red" which will be shown when some model elements are selected, another is called "Clear overridden color" which will be shown while nothing is selected. 

viewer.registerContextMenuCallback(  'MyChangingColorMenuItems', ( menu, status ) => {
    if( status.hasSelected ) {
        menu.push({
            title: 'Override color of selected elements to the red',
            target: () => {
                const selSet = this.viewer.getSelection();
                this.viewer.clearSelection();

                const color = new THREE.Vector4( 255 / 255, 0, 0, 1 );
                for( let i = 0; i < selSet.length; i++ ) {
                    this.viewer.setThemingColor( selSet[i], color );
                }
            }
        });
    } else {
        menu.push({
            title: 'Clear overridden color',
            target: () => {
                this.viewer.clearThemingColors();
            }
        });
    }
});

After executing above codes, you will see two items in the viewer context menus like following snapshots:

1. Menu item Override color of selected elements to the red will be shown when some elements are selected.

Menu item "Override color of selected elements to the red"

2. Menu item Clear overridden color will be shown when nothing is selected.

Menu item "Clear overridden color"

To remove it is also simple to call this function:

viewer.unregisterContextMenuCallback( 'MyChangingColorMenuItems' );

And normally, the above codes will be put in a viewer extension for flexible usage. Here  is the full example:

class MyMenuItemExtension extends Autodesk.Viewing.Extension {
  constructor( viewer, options ) {
    super( viewer, options );

    this.onBuildingContextMenuItem = this.onBuildingContextMenuItem.bind( this );
  }

  get menuId() {
    return 'MyColorContextMenu';
  }

  onBuildingContextMenuItem( menu, status ) {
    if( status.hasSelected ) {
      menu.push({
        title: 'Override color of selected elements to red',
        target: () => {
          const selSet = this.viewer.getSelection();
          this.viewer.clearSelection();

          // Change color of selected elements to the red
          const color = new THREE.Vector4( 255 / 255, 0, 0, 1 );
          for( let i = 0; i < selSet.length; i++ ) {
            this.viewer.setThemingColor( selSet[i], color );
          }
        }
      });

    } else {
      menu.push({
        title: 'Clear overridden corlor',
        target: () => {
          this.viewer.clearThemingColors();
        }
      });
    }
  }

  load() {
    // Add my owned menu items
    this.viewer.registerContextMenuCallback(
      this.menuId,
      this.onBuildingContextMenuItem
    );

    return true;
  }

  unload() {
    // Remove all menu items added from this extension
    this.viewer.unregisterContextMenuCallback( this.menuId );

    return true;
  }
}

Autodesk.Viewing.theExtensionManager.registerExtension( 'DemoMenuExtension', MyMenuItemExtension );

If the above codes are not matched your needs, you might consider writing an owned context menu without default items, and it is simple, too. For example, I want to display different menu items for different elements on the default context menu of the viewer. All I need is to extend Autodesk.Viewing.Extensions.ViewerObjectContextMenu, and add hitTest logic in my context menu's buildMenu function to get the dbId selected by the mouse right clicking.

class MyContextMenu extends Autodesk.Viewing.Extensions.ViewerObjectContextMenu {
  constructor( viewer ) {
    super( viewer );
  }

  isWall( dbId ) {
    //Logics for determining if selected element is wall or not.
    return new Promise( ( resolve, reject ) => {
        $.get(
            '/api/walls/' + dbId,
            ( response ) => {
                if( response && response.id != 0 ) {
                    return resolve( true );
                }
                return resolve( false );
            }
        )
        .error( ( error ) => reject( error ) );
    });
  }

  async buildMenu( event, status ) {
    // Get defulat menu items from the super class
    const menu = super.buildMenu( event, status );

    // Do hitTest to get dbIds
    const viewport = this.viewer.container.getBoundingClientRect();
    const canvasX = event.clientX - viewport.left;
    const canvasY = event.clientY - viewport.top;

    const result = this.viewer.impl.hitTest( canvasX, canvasY, false );

    if( !result || !result.dbId ) return menu;

    let isWall = false;
    try {
        isWall = await this.isWall( result.dbId );
    } catch ( error ) {
        isWall = false;
    }

    if( status.hasSelected && isWall ) {
      menu.push({
          title: 'Show current surface temperature map',
          target: () => {
              $.post(
                    '/api/walls/temperature',
                    ( response ) => {
                        ViewerUtil.showWallTemperatureMap( response.values );
                    }
              );
          }
      });
    }

    return menu;
   }

   /**
    * @override
    */
   async show( event ) {
    const numSelected = this.viewer.getSelectionCount();
    const visibility = this.viewer.getSelectionVisibility();
    const status = {
      numSelected: numSelected,
      hasSelected: ( numSelected > 0 ),
      hasVisible: visibility.hasVisible,
      hasHidden: visibility.hasHidden
    };
    const menu = await this.buildMenu( event, status );

    this.viewer.runContextMenuCallbacks( menu, status );

    if( menu && menu.length > 0 ) {
      this.contextMenu.show( event, menu );
    }
   }
}

class MyContextMenuExtension extends Autodesk.Viewing.Extension {
    constructor( viewer, options ) {
        super( viewer, options );
    }

    load() {
        // Use my owned context menu.
        this.viewer.setContextMenu( new MyContextMenu( this.viewer ) );
        return true;
    }

    unload() {
        // Restore default context menu
        this.viewer.setContextMenu( new Autodesk.Viewing.Extensions.ViewerObjectContextMenu( this.viewer ) );
        return true;
    }
}

Autodesk.Viewing.theExtensionManager.registerExtension( 'DemoWallMenuExtension', MyContextMenuExtension );

And you will see the Show current surface temperature item on your context menu:

Menu Item "Show current surface temperature"

Instead of Autodesk.Viewing.Extensions.ViewerObjectContextMenu inheritance, the MyContextMenu class has to extend Autodesk.Viewing.UI.ObjectContextMenu if you don't want to show default menu items. After modifying, MyContextMenu will show "Show current water temperature" item only while mouse right clicking on elements.

class MyContextMenu extends Autodesk.Viewing.UI.ObjectContextMenu {
  
  // .....

  async buildMenu( event, status ) {
    // Do hitTest to get dbIds
    const viewport = this.viewer.container.getBoundingClientRect();
    const canvasX = event.clientX - viewport.left;
    const canvasY = event.clientY - viewport.top;

    const result = this.viewer.impl.hitTest( canvasX, canvasY, false );

    if( !result || !result.dbId ) return;

    let isWall = false;
    try {
        isWall = await this.isWall( result.dbId );
    } catch ( error ) {
        isWall = false;
    }

    if( status.hasSelected && isWall ) {
      menu.push({
          title: 'Show current surface temperature',
          target: () => {
              $.post(
                    '/api/walls/temperature',
                    ( response ) => {
                        ViewerUtil.showWallTemperatureMap( response.values );
                    }
              );
          }
      });
    }

    return menu;
   }
}

Then, the viewer context menu will show Show current surface temperature item on the screen only.

Show custom items only

Related Article