10 Mar 2020

Multi-model refresher

The support for loading and aggregating multiple models into one scene in Forge Viewer has been around for a while now, but the documentation of this area might have been a bit scarce. Let's take a look at how you can make use of this feature, and how it can be integrated into existing Forge Viewer applications.

Loading models

A typical Forge application requests the manifest for a specific model URN using Autodesk.Viewing.Document.load(urn, onSuccessCallback, onErrorCallback), and then calls viewer.loadDocumentNode(doc, viewable) to load a specific viewable from that model in the viewer.

If you'd like to add another viewable instead of replacing the current one, the loadDocumentNode method accepts an additional argument - a generic object where you can specify the following loading options:

  • keepCurrentModels - optional boolean value specifying whether the currently loaded models should be kept
  • preserveView - optional boolean value specifying whether the current camera settings should be kept
  • placementTransform - optional THREE.Matrix4 object specifying the rotation and/or scale to be applied to the newly loaded geometry
  • globalOffset - optional THREE.Vector3 object specifying the offset to be applied to the newly loaded geometry

Here's an example function that could be used to add another model (its default viewable) into the scene, optionally with specific transformation and offset:

async function addViewable(viewer, urn, xform, offset) {
    return new Promise(function (resolve, reject) {
        function onDocumentLoadSuccess(doc) {
            const viewable = doc.getRoot().getDefaultGeometry();
            const options = {
                preserveView: true,
                keepCurrentModels: true
            };
            if (xform) {
                options.placementTransform = xform;
            }
            if (offset) {
                options.globalOffset = offset;
            }
            viewer.loadDocumentNode(doc, viewable, options)
                .then(resolve)
                .catch(reject);
        }
        function onDocumentLoadFailure(code) {
            reject(`Could not load document (${code}).`);
        }
        Autodesk.Viewing.Document.load('urn:' + urn, onDocumentLoadSuccess, onDocumentLoadFailure);
    });
}

Unloading models

To unload a specific model, the Viewer3D class provides two methods you can choose from, depending on how you're managing the state of your application. If you're keeping a list of all currently loaded viewables, use viewer.unloadDocumentNode(viewable), otherwise you can use viewer.unloadModel(model) to unload the actual in-memory representation of a specific model. The list of currently loaded models can be obtained via other Viewer3D methods such as viewer.getVisibleModels() or viewer.getHiddenModels().

Handling events

With the introduction of the multi-model support, the events emitted by the viewer had to be updated to provide additional information, for example, the SELECTION_CHANGED_EVENT had to include not only the list of IDs of the selected objects, but also the models they belonged to. To avoid breaking backwards compatibility, the dev. team decided to introduce new event types that should be used in multi-model scenarios. These can be recognized by the AGGREGATE_ prefix:

/**
 * Triggered when objects are selected or deselected.
 * Event payload:
 * {
 *   type: 'aggregateSelection',
 *   target: Viewer3D,
 *   selections: Array<{
 *     model: Model,
 *     nodeArray: Array<number>,
 *     dbIdArray: Array<number>,
 *     fragIdsArray: Array<number>
 *   }>
 * }
 */
Autodesk.Viewing.AGGREGATE_SELECTION_CHANGED_EVENT
 
/**
 * Triggered when objects are isolated or deisolated.
 * Event payload:
 * {
 *   type: 'aggregateIsolation',
 *   target: Viewer3D,
 *   isolation: Array<{
 *     model: Model,
 *     ids: Array<number> // dbIds
 *   }>
 * }
 */
Autodesk.Viewing.AGGREGATE_ISOLATION_CHANGED_EVENT
 
/**
 * Triggered when objects are shown or hidden.
 * Event payload:
 * {
 *   type: 'aggregateHidden',
 *   target: Viewer3D,
 *   hidden: Array<{
 *     model: Model,
 *     ids: Array<number> // dbIds
 *   }>
 * }
 */
Autodesk.Viewing.AGGREGATE_HIDDEN_CHANGED_EVENT
 
/** Triggered when user focuses camera on specific elements.
 * Event payload:
 * {
 *   type: 'aggregateFitToView',
 *   target: Viewer3D,
 *   immediate: boolean,
 *   selection: Array<{
 *     model: Model,
 *     selection: Array<number> // dbIds
 *   }>
 * }
 */
Autodesk.Viewing.AGGREGATE_FIT_TO_VIEW_EVENT

Calling viewer methods

As with the events, some of the Viewer3D methods also had to be updated to provide additional information, or to support more specific input. And even here, the development team decided to introduce new methods to prevent breaking backwards compatibility, and to distinguish them using the Aggregate keyword:

/**
 * Returns the selected items from all loaded models.
 * @param {function} [callback] - Optional callback to receive enumerated pairs of model and dbId
 * for each selected object. If no callback is given, an array of objects is returned.
 * @returns {object[]} An array of objects with a model and selectionSet properties for each model
 * that has selected items in the scene.
 */
Viewer3D.prototype.getAggregateSelection = function(callback) {...}
 
/**
 * Returns the isolated items from all loaded models.
 * @returns {object[]} An array of objects with a `model` and the isolated `ids` in that model.
 */
Viewer3D.prototype.getAggregateIsolation = function() {...}
 
/**
 * Returns the hidden nodes for all loaded models.
 * @returns {object[]} An array of objects with a `model` and the hidden `ids` in that model. 
 */
Viewer3D.prototype.getAggregateHiddenNodes = function() {...}

For other methods, a new argument was added, allowing you to specify the model you would like the method to operate on, for example:

/**
 * Selects the array of ids. You can also pass in a single id instead of an array.
 * @param {number[]|number} dbids - element or array of elements to select.
 * @param {Autodesk.Viewing.Model} [model] - the model instance containing the ids.
 * @param {number} [selectionType] - a member of `Autodesk.Viewing.SelectionMode`.
 */
Viewer3D.prototype.select = function(dbids, model, selectionType) {...}
 
/**
 * Ensures the passed in dbid / ids are hidden.
 * @param {number[]|number} node - An array of dbids or just an id
 * @param {Autodesk.Viewing.Model} [model] - The model that contains the dbId. By default uses the initial model loaded into the scene. 
 */
Viewer3D.prototype.hide = function(node, model) {...}

It should also be noted that using these types of methods without the model argument will always refer to the "default" model which is typically the one that was loaded first. Moreover, the viewer methods do not operate across all models, so for example, calling viewer.isolate(null) will not de-isolate elements in all models. In these cases you'll want to iterate over all loaded models, for example like so:

for (const model of viewer.getVisibleModels()) {
  viewer.isolate(null, model);
}

In cases where the additional model argument is not available, you may need to access methods on the specific Model instance. For example, instead of Viewer3D.prototype.getProperties which currently does not allow you to specify which model you're interested in, you can call the same method on the Model class directly:

/**
 * Asyncronous method that gets object properties.
 * @param {number} dbId - The database identifier.
 * @param {Callbacks#onPropertiesSuccess} [onSuccessCallback] - Callback for when the properties are fetched.
 * @param {Callbacks#onGenericError} [onErrorCallback] - Callback for when the properties are not found or another error occurs.
 */
Model.prototype.getProperties = function(dbId, onSuccessCallback, onErrorCallback) {...}

 

Related Article