9 Nov 2017
Switching Viewables in the Forge Viewer

Here is a quick post to document a feature I just added to my main Forge web site: https://forge-rcdb.autodesk.io. It's about providing a UI to let the user switch between multiple views (or viewable items) contained in a single document. The A360 viewer offers this possibility but it is not part of the viewer API. Look no further I'm bringing that to you at no cost ;)
Listing and switching between viewable items in a Forge document is actually pretty straightforward, you will mainly have to deal with wrapping a nice UI around it to let the user select which item has to be displayed. To see how to include or exclude views from a document before uploading to Forge, take a look at this article: How to Export Multiple 3D Views.
Let's dive into the code (all my examples below are using ES6 and async/await + React for the UI part):
I - Load a document from a translated URN
/////////////////////////////////////////////////////////
// Load a document from URN
//
/////////////////////////////////////////////////////////
static loadDocument (urn) {
return new Promise((resolve, reject) => {
const paramUrn = !urn.startsWith('urn:')
? 'urn:' + urn
: urn
Autodesk.Viewing.Document.load(paramUrn, (doc) => {
resolve (doc)
}, (error) => {
reject (error)
})
})
}
II - Get the list of all viewable items in the loaded document
/////////////////////////////////////////////////////////
// Return viewables
//
/////////////////////////////////////////////////////////
static getViewableItems (doc, roles = ['3d', '2d']) {
const rootItem = doc.getRootItem()
let items = []
const roleArray = roles
? (Array.isArray(roles) ? roles : [roles])
: []
roleArray.forEach((role) => {
items = [ ...items,
...Autodesk.Viewing.Document.getSubItemsWithProperties(
rootItem, { type: 'geometry', role }, true) ]
})
return items
}
III - Load the selected viewable
First we unload the current model, this code assumes we have a model loaded at the time user selects a different item, and simply loads a new one by obtaining its path. I am using viewer.tearDown which ensure memory of the current model is being released. This also supports switching from a 3D model to a 2D one and reciprocally. You can abstract away the React code which is specific to my app here.
/////////////////////////////////////////////////////////
// Load the selected viewable
//
/////////////////////////////////////////////////////////
onItemSelected (item) {
const {activeItem} = this.react.getState()
if (item.guid !== activeItem.guid) {
this.viewer.tearDown()
this.viewer.start()
const path =
this.viewerDocument.getViewablePath(item)
this.viewer.loadModel(path)
this.react.setState({
activeItem: item
})
}
}
That's pretty much all you need to know, the rest of the implementation was purely React and css work to create a nice UI around it. That feature is now available by default for any model with more than one view that you upload to the gallery section of my app: https://forge-rcdb.autodesk.io/gallery
See the code in action below, or refer to the implementation: Viewing.Extension.ViewableSelector.js
///////////////////////////////////////////////////////// | |
// Viewing.Extension.ViewableSelector | |
// by Philippe Leefsma, November 2017 | |
// | |
///////////////////////////////////////////////////////// | |
import MultiModelExtensionBase from 'Viewer.MultiModelExtensionBase' | |
import './Viewing.Extension.ViewableSelector.scss' | |
import WidgetContainer from 'WidgetContainer' | |
import ReactTooltip from 'react-tooltip' | |
import ServiceManager from 'SvcManager' | |
import Toolkit from 'Viewer.Toolkit' | |
import ReactDOM from 'react-dom' | |
import Image from 'Image' | |
import Label from 'Label' | |
import React from 'react' | |
class ViewableSelectorExtension extends MultiModelExtensionBase { | |
///////////////////////////////////////////////////////// | |
// Class constructor | |
// | |
///////////////////////////////////////////////////////// | |
constructor (viewer, options) { | |
super (viewer, options) | |
this.react = options.react | |
} | |
///////////////////////////////////////////////////////// | |
// | |
// | |
///////////////////////////////////////////////////////// | |
get className() { | |
return 'viewable-selector' | |
} | |
///////////////////////////////////////////////////////// | |
// Extension Id | |
// | |
///////////////////////////////////////////////////////// | |
static get ExtensionId() { | |
return 'Viewing.Extension.ViewableSelector' | |
} | |
///////////////////////////////////////////////////////// | |
// Load callback | |
// | |
///////////////////////////////////////////////////////// | |
load () { | |
this.react.setState({ | |
activeItem: null, | |
items: [] | |
}).then (async() => { | |
const urn = this.options.model.urn | |
this.viewerDocument = | |
await this.options.loadDocument(urn) | |
const items = | |
await Toolkit.getViewableItems( | |
this.viewerDocument) | |
if (items.length > 1) { | |
this.createButton() | |
await this.react.setState({ | |
activeItem: items[0], | |
items: [items[0], items[1], items[2]] | |
}) | |
if (this.options.showPanel) { | |
this.showPanel (true) | |
} | |
} | |
}) | |
console.log('Viewing.Extension.ViewableSelector loaded') | |
return true | |
} | |
///////////////////////////////////////////////////////// | |
// Unload callback | |
// | |
///////////////////////////////////////////////////////// | |
unload () { | |
this.react.popViewerPanel(this) | |
console.log('Viewing.Extension.ViewableSelector unloaded') | |
return true | |
} | |
///////////////////////////////////////////////////////// | |
// Load the selected viewable | |
// | |
///////////////////////////////////////////////////////// | |
onItemSelected (item) { | |
const {activeItem} = this.react.getState() | |
if (item.guid !== activeItem.guid) { | |
this.viewer.tearDown() | |
this.viewer.start() | |
const path = | |
this.viewerDocument.getViewablePath(item) | |
this.viewer.loadModel(path) | |
this.react.setState({ | |
activeItem: item | |
}) | |
} | |
} | |
///////////////////////////////////////////////////////// | |
// Create a button to display the panel | |
// | |
///////////////////////////////////////////////////////// | |
createButton () { | |
this.button = document.createElement('button') | |
this.button.title = 'This model has multiple views ...' | |
this.button.className = 'viewable-selector btn' | |
this.button.innerHTML = 'Views' | |
this.button.onclick = () => { | |
this.showPanel(true) | |
} | |
const span = document.createElement('span') | |
span.className = 'fa fa-list-ul' | |
this.button.appendChild(span) | |
this.viewer.container.appendChild(this.button) | |
} | |
///////////////////////////////////////////////////////// | |
// Show/Hide panel | |
// | |
///////////////////////////////////////////////////////// | |
showPanel (show) { | |
if (show) { | |
const {items} = this.react.getState() | |
this.button.classList.add('active') | |
const container = this.viewer.container | |
const height = Math.min( | |
container.offsetHeight - 110, | |
(items.length + 1) * 78 + 55) | |
this.react.pushViewerPanel(this, { | |
maxHeight: height, | |
draggable: false, | |
maxWidth: 500, | |
minWidth: 310, | |
width: 310, | |
top: 30, | |
height | |
}) | |
} else { | |
this.react.popViewerPanel(this.id).then(() => { | |
this.button.classList.remove('active') | |
}) | |
} | |
} | |
///////////////////////////////////////////////////////// | |
// Render React panel content | |
// | |
///////////////////////////////////////////////////////// | |
renderContent () { | |
const {activeItem, items} = this.react.getState() | |
const urn = this.options.model.urn | |
const apiUrl = this.options.apiUrl | |
const domItems = items.map((item) => { | |
const active = (item.guid === activeItem.guid) | |
? ' active' :'' | |
const query = `size=400&guid=${item.guid}` | |
const src = `${apiUrl}/thumbnails/${urn}?${query}` | |
return ( | |
<div key={item.guid} className={"item" + active} | |
onClick={() => this.onItemSelected(item)}> | |
<div className="image-container" | |
data-for={`thumbnail-${item.guid}`} | |
data-tip> | |
<Image src={src}/> | |
</div> | |
<ReactTooltip id={`thumbnail-${item.guid}`} | |
className="tooltip-thumbnail" | |
delayShow={700} | |
effect="solid" | |
place="right"> | |
<div> | |
<img src={src} height="200"/> | |
</div> | |
</ReactTooltip> | |
<Label text={item.name}/> | |
</div> | |
) | |
}) | |
return ( | |
<div className="items"> | |
{domItems} | |
<div style={{height: '80px'}}/> | |
</div> | |
) | |
} | |
///////////////////////////////////////////////////////// | |
// Render title | |
// | |
///////////////////////////////////////////////////////// | |
renderTitle () { | |
return ( | |
<div className="title"> | |
<label> | |
Select Viewable | |
</label> | |
<div className="viewable-selector-controls"> | |
<button onClick={() => this.showPanel(false)} | |
title="Toggle panel"> | |
<span className="fa fa-times"/> | |
</button> | |
</div> | |
</div> | |
) | |
} | |
///////////////////////////////////////////////////////// | |
// Render main | |
// | |
///////////////////////////////////////////////////////// | |
render (opts) { | |
return ( | |
<WidgetContainer | |
renderTitle={() => this.renderTitle(opts.docked)} | |
showTitle={opts.showTitle} | |
className={this.className}> | |
{this.renderContent()} | |
</WidgetContainer> | |
) | |
} | |
} | |
Autodesk.Viewing.theExtensionManager.registerExtension( | |
ViewableSelectorExtension.ExtensionId, | |
ViewableSelectorExtension) |