Autodesk Forge is now Autodesk Platform Services

9 Nov 2017

Switching Viewables in the Forge Viewer

Default blog image

    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)

Related Article