29 Oct 2021

Viewer Extensions: Tips and Tricks (or Treats)

Default blog image

If you've already dipped your toes in Forge Viewer development, you are most likely familiar with the concept of "viewer extensions". If not, I'd suggest that you take a look at the Viewer Extensions tutorial on the https://learnforge.autodesk.io website which provides a good introduction into this topic. Simply put, viewer extensions are a neat (and recommended) way of encapsulating your custom viewer functionality in a reusable way. In this article we'll take a look at some of the hidden little features of the extension mechanism in Forge Viewer that you may not be aware of yet.

Extension loading

Extensions are typically just JavaScript files that define the extension class itself, and register it via the ExtensionManager's registerExtension method:

class PumpkinExtension extends Autodesk.Viewing.Extension {
    // ...
}

Autodesk.Viewing.theExtensionManager.registerExtension('PumpkinExtension', PumpkinExtension);

You can then drop the JavaScript file into your app's HTML in a <script> element, and load the extension in one of the following ways:

const viewer = new Autodesk.Viewing.GuiViewer3D(container, { extensions: ['PumpkinExtension'] });
  • By loading the extension to an existing viewer instance using the loadExtension method:
const viewer = new Autodesk.Viewing.GuiViewer3D(container);
viewer.loadExtension('PumpkinExtension');

What you may not know is that you can also register existing extensions from external URLs using the registerExternalExtension method, like so:

Autodesk.Viewing.theExtensionManager.registerExternalExtension('PumpkinExtension', 'https://cdn.jsdelivr.net/gh/petrbroz/forge-simple-viewer-nodejs@develop/public/PumpkinExtension.js');
const viewer = new Autodesk.Viewing.GuiViewer3D(container, { extensions: ['PumpkinExtension'] });

That way you don't have to worry about including anything into your HTML markup, and you can load extensions from remote sources.

Note that the extension JavaScript file must still register the specific extension class under a specific ID using the registerExtension method.

Passing options to extensions

In case an extension requires certain inputs to be passed to it, developers usually pass the inputs as an additional parameter to the loadExtension method:

viewer.loadExtension('PumpkinExtension', { msg: 'boo!' });

The object will then be passed as the 2nd argument to the extension's constructor:

// ...
constructor(viewer, options) {
    super(viewer, options);
    console.log(options); // { msg: 'boo!' }
}
// ...

Another approach is to include the extension ID in the viewer's options, and include your extension's options in there as well, like this:

const viewer = new Autodesk.Viewing.GuiViewer3D(container, {
    extensions: ['PumpkinExtension'],
    pumpkinExtOptions: { msg: 'boo!' }
});

This way, the extension will receive the complete viewer options in its constructor, and can cherry-pick its data as needed:

// ...
constructor(viewer, options) {
    super(viewer, options);
    console.log(options.pumpkinExtOptions); // { msg: 'boo!' }
}
// ...

Asynchronous loading and unloading

The load and unload methods of the base Extension class are typically overridden in specific extensions to handle any required setup and teardown (for example, loading dependencies, or creating/removing custom UI), returning true or false depending on whether the setup or teardown was successful.

What's important to note here is that these methods can be async. So if you need to handle any asynchronous task while the extension is being loaded (for example, loading another extension that this extension depends on, or loading a 3rd party dependency), you can do so right inside these methods, for example:

async load() {
    const loadScript = (src) => new Promise(function (resolve, reject) {
        const el = document.createElement('script');
        el.src = src;
        el.onload = resolve;
        el.onerror = reject;
        document.head.appendChild(el);
    });
    const loadCSS = (href) => new Promise(function (resolve, reject) {
        const el = document.createElement('link');
        el.rel = 'stylesheet';
        el.href = href;
        el.onload = resolve;
        el.onerror = reject;
        document.head.appendChild(el);
    });

    await Promise.all([
        this.viewer.loadExtension('SomeOtherExtension'),
        loadScript('https://unpkg.com/tabulator-tables@4.9.3/dist/js/tabulator.min.js'),
        loadCSS('https://unpkg.com/tabulator-tables@4.9.3/dist/css/tabulator.min.css')
    ]);

    return true;
}

Extension state

The viewer allows developers to save and restore its state using the getState and restoreState methods. When you're developing a viewer extension, perhaps you would like to include some kind of information specific to your extension in the viewer state object as well. And you know what? That is also possible. You just have to override the extension's getState and restoreState methods:

class PumpkinExtension extends Autodesk.Viewing.Extension {
// ...
    getState(state) {
        state['pumpkinExtensionState'] = state['pumpkinExtensionState'] || {};
        state['pumpkinExtensionState'].msg = 'boo!';
    }

    restoreState(state) {
        const extState = state['pumpkinExtensionState'] || {};
        this.msg = extState.msg || '';
    }
// ...
}

Isn't that neat? 🎃

Related Article