///////////////////////////////////////////////////////// // 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)