9 Jan 2020
Placing custom markup by dbId
There are several approaches to implement markups in the Viewer. We have a few articles on this topic.
- 3D Markup with icons and Info-Card by Michale Beale
- Create Pushpin Markup by SVG by Xiaodong Liang
Both articles use a similar way: placing a DIV on top of the model and repositioning it when the camera changes. Clever, right? That's works, so let's use it. But it requires the exact location (XYZ) to position the markup, which can be a bit tricky to determine. Next, Petr Broz implemented it using the id and fragment id and showing a markup with images, here it is:
Petr's approach is almost what I wanted for my sample, but wanted to simplify it a bit more, passing the dbId, text and icon to show, something like the following:
viewer.loadExtension('IconMarkupExtension', {
icons: [
{ dbId: 987,label: '300C',css:'fas fa-thermometer' }
]
});
This new extension uses the dbId to find the center of the 3D geometry, e.g. a Revit door may be composed of several pieces, so it calls THREE.Box3.union for all the bounding boxes, then gets the center of it. The label is just a text. The CSS contains the icon used on the markup and additional stylings, such as background and border. One may choose to use font awesome icons (that's what I've used here, nice and sharp).
A few extra things: we need a toolbar icon and a click event, right? And enable a "pushpin" like markup, without labels (e.g. an exclamation mark). Wrapping everything together, the GIF on the top of this article uses something like the following:
viewer.loadExtension('IconMarkupExtension', {
button: {
icon: 'fa-thermometer-half',
tooltip: 'Show Temperature'
},
icons: [
{ dbId: 3944, label: '300°C', css: 'fas fa-thermometer-full' },
{ dbId: 721, label: '356°C', css: 'fas fa-thermometer-full' },
{ dbId: 10312, label: '450°C', css: 'fas fa-thermometer-empty' },
{ dbId: 563, css: 'fas fa-exclamation-triangle' },
],
onClick: (id) => {
viewers.select(id);
viewers.utilities.fitToView();
switch (id){
case 563:
alert('Sensor offline');
}
}
})
To use it, just create an extension JS file and use the code below. If you want to see it in action, try this live sample.
class IconMarkupExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this._group = null;
this._button = null;
this._icons = options.icons || [];
}
load() {
const updateIconsCallback = () => {
if (this._enabled) {
this.updateIcons();
}
};
this.viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, updateIconsCallback);
this.viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, updateIconsCallback);
this.viewer.addEventListener(Autodesk.Viewing.HIDE_EVENT, updateIconsCallback);
this.viewer.addEventListener(Autodesk.Viewing.SHOW_EVENT, updateIconsCallback);
return true;
}
unload() {
// Clean our UI elements if we added any
if (this._group) {
this._group.removeControl(this._button);
if (this._group.getNumberOfControls() === 0) {
this.viewer.toolbar.removeControl(this._group);
}
}
return true;
}
onToolbarCreated() {
// Create a new toolbar group if it doesn't exist
this._group = this.viewer.toolbar.getControl('customExtensions');
if (!this._group) {
this._group = new Autodesk.Viewing.UI.ControlGroup('customExtensions');
this.viewer.toolbar.addControl(this._group);
}
// Add a new button to the toolbar group
this._button = new Autodesk.Viewing.UI.Button('IconExtension');
this._button.onClick = (ev) => {
this._enabled = !this._enabled;
this.showIcons(this._enabled);
this._button.setState(this._enabled ? 0 : 1);
};
this._button.setToolTip(this.options.button.tooltip);
this._button.container.children[0].classList.add('fas', this.options.button.icon);
this._group.addControl(this._button);
}
showIcons(show) {
const $viewer = $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer');
// remove previous...
$('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer label.markup').remove();
if (!show) return;
// do we have anything to show?
if (this._icons === undefined || this.icons === null) return;
// do we have access to the instance tree?
const tree = this.viewer.model.getInstanceTree();
if (tree === undefined) { console.log('Loading tree...'); return; }
const onClick = (e) => {
if (this.options.onClick)
this.options.onClick($(e.currentTarget).data('id'));
};
this._frags = {}
for (var i = 0; i < this._icons.length; i++) {
// we need to collect all the fragIds for a given dbId
const icon = this._icons[i];
this._frags['dbId' + icon.dbId] = []
// create the label for the dbId
const $label = $(`
<label class="markup update" data-id="${icon.dbId}">
<span class="${icon.css}"> ${icon.label || ''}</span>
</label>
`);
$label.css('display', this.viewer.isNodeVisible(icon.dbId) ? 'block' : 'none');
$label.on('click', onClick);
$viewer.append($label);
// now collect the fragIds
const _this = this;
tree.enumNodeFragments(icon.dbId, function (fragId) {
_this._frags['dbId' + icon.dbId].push(fragId);
_this.updateIcons(); // re-position of each fragId found
});
}
}
getModifiedWorldBoundingBox(dbId) {
var fragList = this.viewer.model.getFragmentList();
const nodebBox = new THREE.Box3()
// for each fragId on the list, get the bounding box
for (const fragId of this._frags['dbId' + dbId]) {
const fragbBox = new THREE.Box3();
fragList.getWorldBounds(fragId, fragbBox);
nodebBox.union(fragbBox); // create a unifed bounding box
}
return nodebBox
}
updateIcons() {
for (const label of $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer .update')) {
const $label = $(label);
const id = $label.data('id');
// get the center of the dbId (based on its fragIds bounding boxes)
const pos = this.viewer.worldToClient(this.getModifiedWorldBoundingBox(id).center());
// position the label center to it
$label.css('left', Math.floor(pos.x - $label[0].offsetWidth / 2) + 'px');
$label.css('top', Math.floor(pos.y - $label[0].offsetHeight / 2) + 'px');
$label.css('display', this.viewer.isNodeVisible(id) ? 'block' : 'none');
}
}
}
Autodesk.Viewing.theExtensionManager.registerExtension('IconMarkupExtension', IconMarkupExtension);