4 Feb 2021

React + TypeScript showing shared model

We have some articles and sample codes on using React with the Viewer:
How to use React in a Forge Viewer DockingPanel
https://github.com/Autodesk-Forge/viewer-react-express-headless 
https://github.com/Autodesk-Forge/forge-react-boiler.nodejs

Also some resources on using TypeScript:
TypeScript Definitions for Forge Viewer and Node.js Client SDK Now Available!
All definitely defined! TypeScript definitions updates and React TypeScript sample (React + TypeScript)
https://github.com/Autodesk-Forge/viewer-nodejs-typeview.sample
https://github.com/dukedhx/viewer-react-typescript-workbox (React + TypeScript)

So, not many about using React and TypeScript together and none about also implementing and using a custom Viewer Extension.

One way to create a project with the client-side part only (without adding the server-side for access token generation) is to work with a shared model - see Publicly share models in customized Viewer. That's what we'll do in this blog post. 
Another way would be to place the SVF content somewhere public and open that from the Viewer, like in this example.
 

In order to get started, we can simply use the create-react-app npm package, which now also supports creating a TypeScript project: Adding TypeScript  

Then you can follow the steps in this article.

In my case I'm using jQuery as well, so had to add "@types/jquery" to the npm install statement.

For some reason, I did not need to include "types": ["forge-viewer"] in the tsconfig.json file. Its need may depend on some other settings in that file.

I created a file named Viewer.tsx in the project to host my Viewer component and migrate the necessary code to it from the previously mentioned article.   
For testing purposes, I shared a Revit model from Fusion Team and modified the code to load the first two sheets from that model and also use the "Autodesk.DiffTool" extension to compare them.

import { Component } from 'react';
import './Viewer.css';

class Viewer extends Component {
  embedURLfromA360: string; 
  viewer?: Autodesk.Viewing.GuiViewer3D;

  constructor(props:any) { 
    // Note: in strict mode this will be called twice
    // https://stackoverflow.com/questions/55119377/react-js-constructor-called-twice
    super(props);  
    this.embedURLfromA360 = "https://myhub.autodesk360.com/ue29c89b7/shares/public/SH7f1edQT22b515c761e81af7c91890bcea5?mode=embed"; // Revit file (A360/Forge/Napa.rvt)    
  }

  render() {
    return (
      <div className="Viewer" id="MyViewerDiv" />
    );
  }

  public componentDidMount() { 
    if(!window.Autodesk) { 
      this.loadCss('https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.min.css');         

      this.loadScript('https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.min.js') 
        .onload = () => {      
          this.onScriptLoaded();  
        }; 
    }
  } 

  public loadCss(src: string): HTMLLinkElement {
    const link = document.createElement('link'); 
    link.rel="stylesheet";
    link.href=src;
    link.type="text/css";
    document.head.appendChild(link);         
    return link; 
  }

  private loadScript(src: string): HTMLScriptElement { 
    const script = document.createElement('script'); 
    script.type = 'text/javascript'; 
    script.src = src; 
    script.async = true; 
    script.defer = true; 
    document.body.appendChild(script);         
    return script; 
  } 

  private onScriptLoaded() {
    let that: any = this;
    this.getURN(function (urn: string) {
      var options = {
        env: "AutodeskProduction",
        getAccessToken: that.getForgeToken.bind(that),
      };
      var documentId: string = "urn:" + urn;
      Autodesk.Viewing.Initializer(options, function onInitialized() {
        Autodesk.Viewing.Document.load(documentId, that.onDocumentLoadSuccess.bind(that), that.onDocumentLoadError);
      });
    });
  }

  getURN(onURNCallback: any) {
    $.get({
      url: this.embedURLfromA360
        .replace("public", "metadata")
        .replace("mode=embed", ""),
      dataType: "json",
      success: function (metadata) {
        if (onURNCallback) {
          let urn = btoa(metadata.success.body.urn)
            .replace("/", "_")
            .replace("=", "");
          onURNCallback(urn);
        }
      },
    });
  }

  getForgeToken(onTokenCallback: any) {
    $.post({
      url: this.embedURLfromA360
        .replace("public", "sign")
        .replace("mode=embed", "oauth2=true"),
      data: "{}",
      success: function (oauth) {
        if (onTokenCallback)
          onTokenCallback(oauth.accessToken, oauth.validitySeconds);
      },
    });
  }

  async onDocumentLoadSuccess(doc: Autodesk.Viewing.Document) {
    // A document contains references to 3D and 2D viewables.
    var items = doc.getRoot().search({
      'type': 'geometry',
      'role': '2d'
    });
    if (items.length === 0) {
      console.error('Document contains no viewables.');
      return;
    }

    var viewerDiv: any = document.getElementById('MyViewerDiv');
    this.viewer = new Autodesk.Viewing.GuiViewer3D(viewerDiv);
    this.viewer.start();

    // loading it dynamically
    const { MyExtension } = await import('./MyExtension');
    MyExtension.register();
    this.viewer.loadExtension('MyExtension');

    var options2 = {};
    let that: any = this;
    this.viewer.loadDocumentNode(doc, items[1], options2).then(function (model1: Autodesk.Viewing.Model) {
      var options1: any = {};
      options1.keepCurrentModels = true;
      
      that.viewer.loadDocumentNode(doc, items[0], options1).then(function (model2: Autodesk.Viewing.Model) {
      
        let extensionConfig: any = {}
        extensionConfig['mimeType'] ='application/vnd.autodesk.revit'
        extensionConfig['primaryModels'] = [model1]
        extensionConfig['diffModels'] = [model2]
        extensionConfig['diffMode'] =  'overlay' 
        extensionConfig['versionA'] =  '2' 
        extensionConfig['versionB'] =  '1' 
                                    
        that.viewer.loadExtension('Autodesk.DiffTool', extensionConfig)
          .then((res: any)=> {
              console.log(res);                              
          })
          .catch(function(err: any) {
              console.log(err);
          })
      });
    });
  }

  onDocumentLoadError(errorCode: Autodesk.Viewing.ErrorCodes) {

  }
}

export default Viewer;

I also added a Viewer.css file to keep a few style settings for the Viewer div element. 

#MyViewerDiv {
  width: 100%;
  height: 100%;
  margin: 0;
  background-color: #f0f8ff;
  position: relative;
}

Then I created a very simple extension in a file I named MyExtension.ts just to test that scenario as well.

export class MyExtension extends Autodesk.Viewing.Extension {
  load() {
    console.log('MyExtension has been loaded');
    return true;
  }

  unload() {
    console.log('MyExtension has been unloaded');
    return true;
  }

  static register() {
    Autodesk.Viewing.theExtensionManager.registerExtension(
      "MyExtension",
      MyExtension
    );
  }
};

Since the Viewer library is loaded dynamically, see Viewer.tsx -> loadScript(), I also had to import my extension dynamically, otherwise it would be loaded before the Viewer library is loaded, and cause an error:

// loading extension dynamically
const { MyExtension } = await import('./MyExtension');
MyExtension.register();
this.viewer.loadExtension('MyExtension');

Now I just had to reference the Viewer component from the App component, and we were ready.

import { Component } from 'react';
import Viewer from './Viewer'
import './App.css';

class App extends Component {
  render () { 
    return (
      <div className="App">
        <Viewer />
      </div>
    );
  }
}

export default App;

Here is the source code: https://github.com/adamenagy/viewer-react-typescript

Related Article