Created
May 17, 2017 15:48
-
-
Save leefsmp/0e77baf619843d3a8273b8997e78fe39 to your computer and use it in GitHub Desktop.
ModelLoaderExtension on https://forge-rcdb.autodesk.io./configurator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
///////////////////////////////////////////////////////// | |
// 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment