Skip to main content

Model Summary

In this step we will create another viewer extension, this time introducing custom UI elements as well. We will add a custom button to the viewer toolbar, and when the button is clicked, we display a docking panel with the summary of property values in different design elements.

Extension skeleton

Just like with the previous extension, create a new file under the extensions subfolder, call it SummaryExtension.js, and populate it with the following code:

wwwroot/extensions/SummaryExtension.js
import { BaseExtension } from './BaseExtension.js';

class SummaryExtension extends BaseExtension {
constructor(viewer, options) {
super(viewer, options);
}

load() {
super.load();
console.log('SummaryExtension loaded.');
return true;
}

unload() {
super.unload();
console.log('SummaryExtension unloaded.');
return true;
}

onModelLoaded(model) {
super.onModelLoaded(model);
this.update();
}

onSelectionChanged(model, dbids) {
super.onSelectionChanged(model, dbids);
this.update();
}

onIsolationChanged(model, dbids) {
super.onIsolationChanged(model, dbids);
this.update();
}

async update() {
// TODO
}
}

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

Next, in wwwroot/viewer.js, import the extension code to our application, and pass the extension ID to the viewer constructor:

import './extensions/LoggerExtension.js';
import './extensions/SummaryExtension.js';
const config = {
extensions: [
'LoggerExtension',
'SummaryExtension',
]
};
const viewer = new Autodesk.Viewing.GuiViewer3D(container, config);

Toolbar button

Now we will create a new button in the viewer toolbar. Since other extensions will need this functionality as well, let's update our BaseExtension class (in the BaseExtension.js file) with the following helper methods:

wwwroot/extensions/BaseExtension.js
export class BaseExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this._onObjectTreeCreated = (ev) => this.onModelLoaded(ev.model);
this._onSelectionChanged = (ev) => this.onSelectionChanged(ev.model, ev.dbIdArray);
this._onIsolationChanged = (ev) => this.onIsolationChanged(ev.model, ev.nodeIdArray);
}

load() {
this.viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, this._onObjectTreeCreated);
this.viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this._onSelectionChanged);
this.viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, this._onIsolationChanged);
return true;
}

unload() {
this.viewer.removeEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, this._onObjectTreeCreated);
this.viewer.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this._onSelectionChanged);
this.viewer.removeEventListener(Autodesk.Viewing.ISOLATE_EVENT, this._onIsolationChanged);
return true;
}

onToolbarCreated() {}

onModelLoaded(model) {}

onSelectionChanged(model, dbids) {}

onIsolationChanged(model, dbids) {}

findLeafNodes(model) {
return new Promise(function (resolve, reject) {
model.getObjectTree(function (tree) {
let leaves = [];
tree.enumNodeChildren(tree.getRootId(), function (dbid) {
if (tree.getChildCount(dbid) === 0) {
leaves.push(dbid);
}
}, true);
resolve(leaves);
}, reject);
});
}

async findPropertyNames(model) {
const dbids = await this.findLeafNodes(model);
return new Promise(function (resolve, reject) {
model.getBulkProperties(dbids, {}, function (results) {
let propNames = new Set();
for (const result of results) {
for (const prop of result.properties) {
propNames.add(prop.displayName);
}
}
resolve(Array.from(propNames.values()));
}, reject);
});
}

createToolbarButton(buttonId, buttonIconUrl, buttonTooltip) {
let group = this.viewer.toolbar.getControl('dashboard-toolbar-group');
if (!group) {
group = new Autodesk.Viewing.UI.ControlGroup('dashboard-toolbar-group');
this.viewer.toolbar.addControl(group);
}
const button = new Autodesk.Viewing.UI.Button(buttonId);
button.setToolTip(buttonTooltip);
group.addControl(button);
const icon = button.container.querySelector('.adsk-button-icon');
if (icon) {
icon.style.backgroundImage = `url(${buttonIconUrl})`;
icon.style.backgroundSize = `24px`;
icon.style.backgroundRepeat = `no-repeat`;
icon.style.backgroundPosition = `center`;
}
return button;
}

removeToolbarButton(button) {
const group = this.viewer.toolbar.getControl('dashboard-toolbar-group');
group.removeControl(button);
}
}

The new code is using classes from the Autodesk.Viewing.UI namespace to create a new toolbar button group, and a new toolbar button with specific ID, icon URL, and tooltip message.

Now let's use these helper methods in the SummaryExtension class:

wwwroot/extensions/SummaryExtension.js
import { BaseExtension } from './BaseExtension.js';

class SummaryExtension extends BaseExtension {
constructor(viewer, options) {
super(viewer, options);
this._button = null;
}

load() {
super.load();
console.log('SummaryExtension loaded.');
return true;
}

unload() {
super.unload();
if (this._button) {
this.removeToolbarButton(this._button);
this._button = null;
}
console.log('SummaryExtension unloaded.');
return true;
}

onToolbarCreated() {
this._button = this.createToolbarButton('summary-button', 'https://img.icons8.com/small/32/brief.png', 'Show Model Summary');
this._button.onClick = () => {
// TODO
};
}

onModelLoaded(model) {
super.onModelLoaded(model);
this.update();
}

onSelectionChanged(model, dbids) {
super.onSelectionChanged(model, dbids);
this.update();
}

onIsolationChanged(model, dbids) {
super.onIsolationChanged(model, dbids);
this.update();
}

async update() {
// TODO
}
}

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

The onToolbarCreated method will be automatically called by the viewer when (and if) the toolbar is ready. Note that the viewer may be activated in a "headless" mode, in which case no UI will be created, and the onToolbarCreated method will not be called.

Custom panel

And finally, we update the SummaryExtension extension so that when the user clicks on the toolbar button, we display a custom viewer panel with data aggregated from properites of the currently loaded design.

Start by creating a SummaryPanel.js file in the same folder where SummaryExtension.js is located, and add the following code to it:

wwwroot/extensions/SummaryPanel.js
export class SummaryPanel extends Autodesk.Viewing.UI.PropertyPanel {
constructor(extension, id, title) {
super(extension.viewer.container, id, title);
this.extension = extension;
}

async update(model, dbids, propNames) {
this.removeAllProperties();
for (const propName of propNames) {
const initialValue = { sum: 0, count: 0, min: Infinity, max: -Infinity };
const aggregateFunc = (aggregate, value, property) => {
return {
count: aggregate.count + 1,
sum: aggregate.sum + value,
min: Math.min(aggregate.min, value),
max: Math.max(aggregate.max, value),
units: property.units,
precision: property.precision
};
};
const { sum, count, min, max, units, precision } = await this.aggregatePropertyValues(model, dbids, propName, aggregateFunc, initialValue);
if (count > 0) {
const category = propName;
this.addProperty('Count', count, category);
this.addProperty('Sum', this.toDisplayUnits(sum, units, precision), category);
this.addProperty('Avg', this.toDisplayUnits((sum / count), units, precision), category);
this.addProperty('Min', this.toDisplayUnits(min, units, precision), category);
this.addProperty('Max', this.toDisplayUnits(max, units, precision), category);
}
}
}

async aggregatePropertyValues(model, dbids, propertyName, aggregateFunc, initialValue = 0) {
return new Promise(function (resolve, reject) {
let aggregatedValue = initialValue;
model.getBulkProperties(dbids, { propFilter: [propertyName] }, function (results) {
for (const result of results) {
if (result.properties.length > 0) {
const prop = result.properties[0];
aggregatedValue = aggregateFunc(aggregatedValue, prop.displayValue, prop);
}
}
resolve(aggregatedValue);
}, reject);
});
}

toDisplayUnits(value, units, precision) {
return Autodesk.Viewing.Private.formatValueWithUnits(value, units, 3, precision);
}
}

The new class is derived from Autodesk.Viewing.UI.PropertyPanel which is the standard panel UI used for displaying properties of selected design elements. In this case we use the removeAllProperties and addProperty methods to display different kind of data. Given a specific set of object IDs and property names, the SummaryPanel will display aggregated values (count, sum, average, minimum, and maximum) of these properties across all objects.

Finally, let's add the panel to our extension:

wwwroot/extensions/SummaryExtension.js
import { BaseExtension } from './BaseExtension.js';
import { SummaryPanel } from './SummaryPanel.js';

const SUMMARY_PROPS = ['Length', 'Area', 'Volume', 'Density', 'Mass', 'Price'];

class SummaryExtension extends BaseExtension {
constructor(viewer, options) {
super(viewer, options);
this._button = null;
this._panel = null;
}

load() {
super.load();
console.log('SummaryExtension loaded.');
return true;
}

unload() {
super.unload();
if (this._button) {
this.removeToolbarButton(this._button);
this._button = null;
}
if (this._panel) {
this._panel.setVisible(false);
this._panel.uninitialize();
this._panel = null;
}
console.log('SummaryExtension unloaded.');
return true;
}

onToolbarCreated() {
this._panel = new SummaryPanel(this, 'model-summary-panel', 'Model Summary');
this._button = this.createToolbarButton('summary-button', 'https://img.icons8.com/small/32/brief.png', 'Show Model Summary');
this._button.onClick = () => {
this._panel.setVisible(!this._panel.isVisible());
this._button.setState(this._panel.isVisible() ? Autodesk.Viewing.UI.Button.State.ACTIVE : Autodesk.Viewing.UI.Button.State.INACTIVE);
if (this._panel.isVisible()) {
this.update();
}
};
}

onModelLoaded(model) {
super.onModelLoaded(model);
this.update();
}

onSelectionChanged(model, dbids) {
super.onSelectionChanged(model, dbids);
this.update();
}

onIsolationChanged(model, dbids) {
super.onIsolationChanged(model, dbids);
this.update();
}

async update() {
if (this._panel) {
const selectedIds = this.viewer.getSelection();
const isolatedIds = this.viewer.getIsolatedNodes();
if (selectedIds.length > 0) { // If any nodes are selected, compute the aggregates for them
this._panel.update(this.viewer.model, selectedIds, SUMMARY_PROPS);
} else if (isolatedIds.length > 0) { // Or, if any nodes are isolated, compute the aggregates for those
this._panel.update(this.viewer.model, isolatedIds, SUMMARY_PROPS);
} else { // Otherwise compute the aggregates for all nodes
const dbids = await this.findLeafNodes(this.viewer.model);
this._panel.update(this.viewer.model, dbids, SUMMARY_PROPS);
}
}
}
}

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

Try it out

This time we can test our new extension directly in the viewer. Try clicking the new toolbar button to open our summary panel. The panel shows various aggregates of properties we hard-coded in the SUMMARY_PROPS constant (Length, Area, Volume, Density, Mass, and Price) for all objects. And when we isolate or select some of the design elements by hand, the aggregated values should reflect that as well.

Aggregates Result