Autodesk Forge is now Autodesk Platform Services

17 May 2017

Preparing your viewing application for multi-model workflows - Part 2: Model Loader

In my previous post I exposed the code for the base extension class I'm using in my app in order to handle efficiently multiple models in the viewer. The first extension that relies on it is the ModelLoader: its purpose is to expose controls that let the user insert models into the scene, manage inserted models and remove them if needed.  

It can load models from the Autodesk Cloud as well as local ones, using the approach I described in an earlier post: Working seamlessly online/offline when developing your Web applications with the Forge Viewer

Upon insertion of a new model,  the extension is tagging the model object with a randomly generated guid, so each model can be uniquely identified at a later stage if needed by another extension. The way to achieve it is pretty straighforward, using the callback method provided by viewer.loadModel

const onModelLoaded = (model) => {

      model.guid = guid()
}

viewer.loadModel(path, loadOption, onModelLoaded)

Here is my implementation for guid() function:

guid (format = 'xxxxxxxxxxxx') {

  var d = new Date().getTime()

  const guid = format.replace(
    /[xy]/g,
    function (c) {
      var r = (d + Math.random() * 16) % 16 | 0
      d = Math.floor(d / 16)
      return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16)
    })

  return guid
}

Another useful thing the extension is doing is to fire "model.activated" event when a model is being activated: this occurs either when a model is being selected by user directly through the viewer (and the model was not previously the active one) or the model is being selected in the dropdown control of ModelLoader extnesion. The purpose of that event is to trigger modelActivated callback that allows other multi-model extensions to reload their current data is they are interested in the active model. I will present such extensions in the future.

The UI also provides controls to transform models (translate, rotate, scale), however this logic is not implemented by the extension itself: controls are created and handled by the ModelTransformer extension and injected into the parent UI using React.   

You can take a look at a live demo of the ModelLoader extension there and refer to the full source code below 

/////////////////////////////////////////////////////////
// Viewing.Extension.ModelLoader
// by Philippe Leefsma, April 2017
//
/////////////////////////////////////////////////////////
import MultiModelExtensionBase from 'Viewer.MultiModelExtensionBase'
import ContentEditable from 'react-contenteditable'
import './Viewing.Extension.ModelLoader.scss'
import WidgetContainer from 'WidgetContainer'
import ServiceManager from 'SvcManager'
import { ReactLoader } from 'Loader'
import Toolkit from 'Viewer.Toolkit'
import DOMPurify from 'dompurify'
import ReactDOM from 'react-dom'
import Label from 'Label'
import React from 'react'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap'
class ModelLoaderExtension extends MultiModelExtensionBase {
/////////////////////////////////////////////////////////
// Class constructor
//
/////////////////////////////////////////////////////////
constructor (viewer, options) {
super (viewer, options)
this.renderTitle = this.renderTitle.bind(this)
this.dialogSvc =
ServiceManager.getService('DialogSvc')
this.modelSvc =
ServiceManager.getService('ModelSvc')
this.react = options.react
}
/////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
get className() {
return 'model-loader'
}
/////////////////////////////////////////////////////////
// Extension Id
//
/////////////////////////////////////////////////////////
static get ExtensionId() {
return 'Viewing.Extension.ModelLoader'
}
/////////////////////////////////////////////////////////
// Load callback
//
/////////////////////////////////////////////////////////
load () {
if (!this.viewer.model) {
this.viewer.container.classList.add('empty')
}
const models = this.models
const activeModel = models.length
? models[0]
: null
this.firstFileType = activeModel
? this.getFileType(activeModel.urn)
: null
this.react.setState({
activeModel,
models
}).then (() => {
this.react.pushRenderExtension(this)
})
const transformerReactOptions = {
pushRenderExtension: () => {
return Promise.resolve()
},
popRenderExtension: () => {
return Promise.resolve()
}
}
const transformerOptions = Object.assign({}, {
react: transformerReactOptions,
fullTransform : true,
hideControls : true
}, this.options.transformer)
this.viewer.loadDynamicExtension(
'Viewing.Extension.ModelTransformer',
transformerOptions).then((modelTransformer) => {
this.react.setState({
modelTransformer
})
if (activeModel ) {
modelTransformer.setModel(
activeModel)
}
})
console.log('Viewing.Extension.ModelLoader loaded')
return true
}
/////////////////////////////////////////////////////////
// Unload callback
//
/////////////////////////////////////////////////////////
unload () {
console.log('Viewing.Extension.ModelLoader unloaded')
this.react.popViewerPanel(this)
super.unload ()
return true
}
/////////////////////////////////////////////////////////
// Displays model selection popup dialog
//
/////////////////////////////////////////////////////////
showModelDlg () {
this.dialogSvc.setState({
className: 'model-loader-dlg',
title: 'Select Model ...',
showOK: false,
search: '',
content:
<div>
<ReactLoader show={true}/>
</div>,
open: true
})
this.modelSvc.getModels(this.options.database).then(
(models) => {
const dbModelsByName =
_.sortBy(models, (model) => {
return model.name
})
this.dialogSvc.setState({
dbModels: dbModelsByName,
open: true
}, true)
this.setDlgItems (dbModelsByName)
this.batchRequestThumbnails(5)
})
}
/////////////////////////////////////////////////////////
// Get file type by base64 decoding the model URN
//
/////////////////////////////////////////////////////////
getFileType (urn) {
return window.atob(urn).split(".").pop(-1)
}
/////////////////////////////////////////////////////////
// Loads a model based on database info
// For testing purpose also supports
// loading models offline
// See for more details: http://autode.sk/2qsKxx8
//
/////////////////////////////////////////////////////////
loadModel (dbModel) {
return new Promise(async(resolve) => {
const fileType = this.getFileType(dbModel.urn)
const loadOptions = {
placementTransform:
this.buildPlacementTransform(fileType)
}
switch (dbModel.env) {
case 'AutodeskProduction':
const doc = await Toolkit.loadDocument(
dbModel.urn)
const items = Toolkit.getViewableItems(doc)
if (items.length) {
const path = doc.getViewablePath(items[0])
this.viewer.loadModel(path, loadOptions,
(model) => {
model.database = this.options.database
model.dbModelId = dbModel._id
model.name = dbModel.name
model.guid = this.guid()
model.urn = dbModel.urn
resolve (model)
})
}
break
case 'Local':
this.viewer.loadModel(dbModel.path, loadOptions,
(model) => {
model.database = this.options.database
model.dbModelId = dbModel._id
model.name = dbModel.name
model.guid = this.guid()
model.urn = dbModel.urn
resolve (model)
})
break
}
})
}
/////////////////////////////////////////////////////////
// Unload model upon user request
//
/////////////////////////////////////////////////////////
async unloadModel () {
const {activeModel, models} = this.react.getState()
const onClose = async(result) => {
if (result === 'OK') {
const filteredModels = models.filter((model) => {
return model.guid !== activeModel.guid
})
if (!filteredModels.length) {
this.viewer.container.classList.add('empty')
this.firstFileType = null
}
await this.react.setState({
models: filteredModels
})
const nextActiveModel = filteredModels.length
? filteredModels[0]
: null
this.eventSink.emit('model.unloaded', {
model: activeModel
})
this.viewer.impl.unloadModel(activeModel)
await this.setActiveModel(nextActiveModel, {
source: 'model.unloaded'
})
}
this.dialogSvc.off('dialog.close', onClose)
}
const msg = DOMPurify.sanitize(
`Are you sure you want to unload`
+ `<b><br/>${activeModel.name}</b> ?`)
this.dialogSvc.on('dialog.close', onClose)
this.dialogSvc.setState({
className: 'model-loader-unload-dlg',
title: 'Unload Model ...',
content:
<div dangerouslySetInnerHTML={{__html: msg}}>
</div>,
open: true
})
}
/////////////////////////////////////////////////////////
// .rvt and .nwc files are z-oriented, whereas other
// file formats are y-oriented.
// Depending what file type was the initial model,
// we need to adjust the subsequent loaded models
//
/////////////////////////////////////////////////////////
buildPlacementTransform (fileType) {
this.firstFileType = this.firstFileType || fileType
const placementTransform = new THREE.Matrix4()
// those file type have different orientation
// than other, so need to correct it
// upon insertion
const zOriented = ['rvt', 'nwc']
if (zOriented.indexOf(this.firstFileType) > -1) {
if (zOriented.indexOf(fileType) < 0) {
placementTransform.makeRotationX(
90 * Math.PI/180)
}
} else {
if(zOriented.indexOf(fileType) > -1) {
placementTransform.makeRotationX(
-90 * Math.PI/180)
}
}
return placementTransform
}
/////////////////////////////////////////////////////////
// Fit whole model to view
//
/////////////////////////////////////////////////////////
fitModelToView (model) {
const instanceTree = model.getData().instanceTree
if (instanceTree) {
const rootId = instanceTree.getRootId()
this.viewer.fitToView([rootId], model)
}
}
/////////////////////////////////////////////////////////
// ModelBeginLoad event
//
/////////////////////////////////////////////////////////
onModelBeginLoad (event) {
const {models} = this.react.getState()
const model = event.model
this.react.setState({
models: [...models, model]
})
this.setActiveModel (model, {
source: 'model.loaded',
fitToView: true
})
this.firstFileType = this.firstFileType ||
this.getFileType(model.urn)
}
/////////////////////////////////////////////////////////
// ModelRootLoaded event
//
/////////////////////////////////////////////////////////
onModelRootLoaded (event) {
this.viewer.container.classList.remove('empty')
}
/////////////////////////////////////////////////////////
// Model Selected event
//
/////////////////////////////////////////////////////////
onSelection (event) {
if (event.selections && event.selections.length) {
const selection = event.selections[0]
const model = selection.model
this.setActiveModel (model, {
source: 'model.selected'
})
this.eventSink.emit('model.selected', {
model
})
}
}
/////////////////////////////////////////////////////////
// Set model as active
//
/////////////////////////////////////////////////////////
async setActiveModel (model, params = {}) {
const activeGuid = this.viewer.activeModel
? this.viewer.activeModel.guid
: null
this.viewer.activeModel = model
if (params.fitToView) {
this.fitModelToView (model)
}
await this.react.setState({
activeModel: model
})
if (model) {
this.setStructure(model)
if (model.guid !== activeGuid) {
this.eventSink.emit('model.activated', {
source: params.source,
model
})
}
}
}
/////////////////////////////////////////////////////////
// Fixing the model structure browser to show active
// model structure
//
/////////////////////////////////////////////////////////
setStructure (model) {
const instanceTree = model.getData().instanceTree
if (instanceTree && this.viewer.modelstructure) {
this.viewer.modelstructure.setModel(
instanceTree)
}
}
/////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
onKeyDown (e) {
if (e.keyCode === 13) {
e.stopPropagation()
e.preventDefault()
}
}
/////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////
onSearchChanged (e) {
const search = e.target.value.toLowerCase()
this.dialogSvc.setState({
search
}, true)
const state = this.dialogSvc.getState()
const filteredDbModels =
state.dbModels.filter((dbModel) => {
return search.length
? dbModel.name.toLowerCase().indexOf(search) > -1
: true
})
this.setDlgItems (filteredDbModels)
}
/////////////////////////////////////////////////////////
// Load model items in popup selection dialog
//
/////////////////////////////////////////////////////////
setDlgItems (dbModels) {
const modelDlgItems = dbModels.map((dbModel) => {
return (
<div key={dbModel._id} className="model-item"
onClick={() => {
this.loadModel(dbModel).then((model) => {
this.eventSink.emit('model.loaded', {
model
})
})
this.dialogSvc.setState({
open: false
})
}}>
<img className={dbModel.thumbnail ? "":"default-thumbnail"}
src={dbModel.thumbnail ? dbModel.thumbnail : ""}/>
<Label text= {dbModel.name}/>
</div>
)
})
const state = this.dialogSvc.getState()
this.dialogSvc.setState({
content:
<div>
<ReactLoader show={false}/>
<ContentEditable
onChange={(e) => this.onSearchChanged(e)}
onKeyDown={(e) => this.onKeyDown(e)}
data-placeholder="Search ..."
html={state.search}
className="search"
/>
<div className="scroller">
{ modelDlgItems }
</div>
</div>
}, true)
}
/////////////////////////////////////////////////////////
// batch requests thumbnails for models shown in
// popup selection dialog
//
/////////////////////////////////////////////////////////
batchRequestThumbnails (size) {
const state = this.dialogSvc.getState()
const chunks = _.chunk(state.dbModels, size)
chunks.forEach((modelChunk) => {
const modelIds = modelChunk.map((model) => {
return model._id
})
this.modelSvc.getThumbnails(
this.options.database, modelIds).then(
(thumbnails) => {
const dbModels = state.dbModels.map((model) => {
const idx = modelIds.indexOf(model._id)
return (idx < 0
? model
: Object.assign({}, model, {
thumbnail: thumbnails[idx]
}))
})
this.dialogSvc.setState({
dbModels
}, true)
this.setDlgItems (dbModels)
})
})
}
/////////////////////////////////////////////////////////
// Panel docking mode
//
/////////////////////////////////////////////////////////
async setDocking (docked) {
const id = ModelLoaderExtension.ExtensionId
if (docked) {
await this.react.popRenderExtension(id)
await this.react.pushViewerPanel(this, {
height: 250,
width: 350
})
} else {
await this.react.popViewerPanel(id)
this.react.pushRenderExtension(this)
}
}
/////////////////////////////////////////////////////////
// React method - render panel title
//
/////////////////////////////////////////////////////////
renderTitle (docked) {
const spanClass = docked
? 'fa fa-chain-broken'
: 'fa fa-chain'
return (
<div className="title">
<label>
Model Loader
</label>
<div className="model-loader-controls">
<button onClick={() => this.setDocking(docked)}
title="Toggle docking mode">
<span className={spanClass}/>
</button>
</div>
</div>
)
}
/////////////////////////////////////////////////////////
// React method - render panel controls
//
/////////////////////////////////////////////////////////
renderControls () {
const {activeModel, models} = this.react.getState()
const modelItems = models.map((model, idx) => {
return (
<MenuItem eventKey={idx} key={model.guid}
onClick={() => {
this.setActiveModel(model, {
source: 'dropdown',
fitToView: true
})
}}>
{ model.name }
</MenuItem>
)
})
const modelName = activeModel
? activeModel.name
: ''
return (
<div className="controls">
<div className="row">
<DropdownButton
title={"Model: " + modelName}
className="sequence-dropdown"
disabled={!activeModel}
key="sequence-dropdown"
id="sequence-dropdown">
{ modelItems }
</DropdownButton>
<button onClick={() => this.showModelDlg()}
title="Load model">
<span className="fa fa-plus"/>
</button>
<button onClick={() => this.unloadModel()}
disabled={!activeModel}
title="Unload model">
<span className="fa fa-times"/>
</button>
</div>
</div>
)
}
/////////////////////////////////////////////////////////
// React method - render transformer extension UI
//
/////////////////////////////////////////////////////////
renderTransformer () {
const {modelTransformer} = this.react.getState()
return modelTransformer
? modelTransformer.render({showTitle: false})
: <div/>
}
/////////////////////////////////////////////////////////
// React method - render extension UI
//
/////////////////////////////////////////////////////////
render (opts) {
return (
<WidgetContainer
renderTitle={() => this.renderTitle(opts.docked)}
showTitle={opts.showTitle}
className={this.className}>
{ this.renderControls() }
{ this.renderTransformer() }
</WidgetContainer>
)
}
}
Autodesk.Viewing.theExtensionManager.registerExtension(
ModelLoaderExtension.ExtensionId,
ModelLoaderExtension)

Related Article