Autodesk Forge Viewer and Angular2

I’ve been using the Autodesk Forge viewer quite a bit lately to integrate 3D building models within various prototype applications. Until now I had only used the Forge Viewer with plain JavaScript (or a bit of JQuery). I recently tried to integrate the viewer within an Angular 2 application and thought I’d share my solution – as I was unable to find any examples when I did a quick google.

Angular2 (just called Angular) is a rewrite of AngularJS framework. A key difference is that Angular2 moves away from the MVC pattern in favour of Components and the shadow DOM. Although not a requirements, Angular2 recommends the use of TypeScript to help more strongly type JavaScript with a view to help maintainability of large applications. Angular is just JavaScript, so it’s not difficult to integrate external JavaScript libraries with it – you just have to follow particular conventions to get these libraries to work. The solution to integrating the Forge Viewer is very similar to some of the React samples on GitHub.

Step 1

After creating a new Angular app via angular-cli, add the required JS includes to index.html:

<script src="https://developer.api.autodesk.com/viewingservice/v1/viewers/three.min.js?v=v2.13"></script>
<script src="https://developer.api.autodesk.com/viewingservice/v1/viewers/viewer3D.min.js?v=v2.13"></script>

Note that I’m going to use the headless Forge Viewer in this example – so I don’t need to include the Forge Viewer’s CSS.

Step 2

Create a new component using angular-cli:

ng generate component forge-viewer

Add the following to forge-viewer.component.html:

<div #viewerContainer class="viewer">
</div>

This provides a Div for the Forge Viewer to render in to. We need to add a #viewerContainer reference within theDiv so that we can obtain an ElementRef to give the Forge Viewer the DOM element to bind to. Add the following CSS to forge-viewer.component.css:

.viewer {
  position: relative;
  width: 100%;
  height: 450px;
}

Step 3

We’ve done the basic setup, we now need to add the main functionality to forge-viewer.component.ts.

import { Component, ViewChild, OnInit, OnDestroy, ElementRef } from '@angular/core';

// We need to tell TypeScript that Autodesk exists as a variables/object somewhere globally
declare const Autodesk: any;

@Component({
  selector: 'forge-viewer',
  templateUrl: './forge-viewer.component.html',
  styleUrls: ['./forge-viewer.component.scss'],
})
export class ForgeViewerComponent implements OnInit, OnDestroy{
  @ViewChild('viewerContainer') viewerContainer: any;
  private viewer: any;

  constructor(private elementRef: ElementRef) { }

...

There are a couple of lines above that are crucially important. We’ve imported the Autodesk Viewer from Autodesk’s servers – this creates a global Autodesk object. We don’t have any TypeScript typings for this object (ts.d files). At time of writing, there were no definitions on the DefinatelyTyped repository. TypeScript is just a superset of JavaScript, so it’s not a problem that we don’t have a typings file. All we need to do is declare an Autodesk variable:

declare const Autodesk: any;

This tells the TypeScript compiler that somewhere globally there is an object called Autodesk.

Also important is a reference to the Div we want to render the viewer in:

@ViewChild('viewerContainer') viewerContainer: any;

Step 4

We’ll now create an instance of the Forge Viewer – we’ll need to do this once the component has been initialised AND our hosting Div has been rendered in the DOM. We’ll use the ngAfterViewInit lifecycle hook:

ngAfterViewInit() {
  this.launchViewer();
}

private getAccessToken(onSuccess: any) {
  const { access_token, expires_in } = // Your code to get a token
  onSuccess(access_token, expires_in);
}

private launchViewer() {
  if (this.viewer) {
    // Viewer has already been initialised
    return;
  }

  const options = {
    env: 'AutodeskProduction',
    getAccessToken: (onSuccess) => { this.getAccessToken(onSuccess) },
  };

  // For a headless viewer
  this.viewer = new Autodesk.Viewing.Viewer3D(this.viewerContainer.nativeElement, {});
  // For a viewer with UI
  // this.viewer = new Autodesk.Viewing.Private.GuiViewer3D(this.viewerContainer.nativeElement, {});

  Autodesk.Viewing.Initializer(options, () => {
    // Initialise the viewer and load a document
    this.viewer.initialize();
    this.loadDocument();
  });
}

private loadDocument() {
  const urn = `urn:${//document urn}`;

  Autodesk.Viewing.Document.load(urn, (doc) => {
    // Get views that can be displayed in the viewer
    const geometryItems = Autodesk.Viewing.Document.getSubItemsWithProperties(doc.getRootItem(), {type: 'geometry'}, true);

    if (geometryItems.length === 0) {
      return;
    }

    // Example of adding event listeners
    this.viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, this.geometryLoaded);
    this.viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (event) => this.selectionChanged(event));

    // Load view in to the viewer
    this.viewer.load(doc.getViewablePath(geometryItems[0]));
  }, errorMsg => console.error);
}

private geometryLoaded(event: any) {
  const viewer = event.target;

  viewer.removeEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, this.geometryLoaded);

  // Example - set light preset and fit model to view
  viewer.setLightPreset(8);
  viewer.fitToView();
}

private selectionChanged(event: any) {
  const model = event.model;
  const dbIds = event.dbIdArray;

  // Get properties of object
  this.viewer.getProperties(dbIds[0], (props) => {
    // Do something with properties.
  });
}

ngOnDestroy() {
  // Clean up the viewer when the component is destroyed
  if (this.viewer && this.viewer.running) {
    this.viewer.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.selectionChanged);
    this.viewer.tearDown();
    this.viewer.finish();
    this.viewer = null;
  }
}

A lot of the code is very similar to how you’d instantiate the viewer via plain JavaScript. The following line creates a new instance of the viewer in the Div of our component template:

this.viewer = new Autodesk.Viewing.Viewer3D(this.viewerContainer.nativeElement, {});

The reset of the code just loads a document and demonstrates how events can be bound.

Gotchas

Whilst working on this prototype, I encountered one gotcha. I could successfully create an instance of the Viewer and load a model in to it. My application had simple routing – when I navigated away from the route where the viewer was hosts, to another route and then back, the viewer wouldn’t display. It seemed that viewer thought it has already been instantiated so didn’t bother and skipped to loading the model…which didn’t work because there was no instance of the viewer.

My solution to the problem isn’t as elegant as I wanted, but does work:

this.viewer = new Autodesk.Viewing.Viewer3D(this.viewerContainer.nativeElement, {}); // Headless viewer

// Check if the viewer has already been initialised - this isn't the nicest, but we've set the env in our
// options above so we at least know that it was us who did this!
if (!Autodesk.Viewing.Private.env) {
  Autodesk.Viewing.Initializer(options, () => {
    this.viewer.initialize();
      this.loadDocument();
  });
} else {
  // We need to give an initialised viewing application a tick to allow the DOM element to be established before we re-draw
  setTimeout(() => {
    this.viewer.initialize();
    this.loadDocument();
  });
}

The 2nd time out component loads, Autodesk.Viewing.Private.env will already be set (we set it!). So we simply call initialise on the viewer and load the model. This didn’t work first time – but adding a setTimeout gave Angular a tick to sort out DOM binding/it’s update cycle before attempting to load the viewer.

Screenshots

The full forge-viewer.component.ts file

import { Component, ViewChild, OnInit, OnDestroy, ElementRef, Input } from '@angular/core';

// We need to tell TypeScript that Autodesk exists as a variables/object somewhere globally
declare const Autodesk: any;

@Component({
  selector: 'forge-viewer',
  templateUrl: './forge-viewer.component.html',
  styleUrls: ['./forge-viewer.component.scss'],
})
export class ForgeViewerComponent implements OnInit, OnDestroy {
  private selectedSection: any = null;
  @ViewChild('viewerContainer') viewerContainer: any;
  private viewer: any;

  constructor(private elementRef: ElementRef) { }

  ngOnInit() {
  }

  ngAfterViewInit() { 
    this.launchViewer();
  }

  ngOnDestroy() {
    if (this.viewer && this.viewer.running) {
      this.viewer.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.selectionChanged);
      this.viewer.tearDown();
      this.viewer.finish();
      this.viewer = null;
    }
  }

  private launchViewer() {
    if (this.viewer) {
      return;
    }

    const options = {
      env: 'AutodeskProduction',
      getAccessToken: (onSuccess) => { this.getAccessToken(onSuccess) },
    };

    this.viewer = new Autodesk.Viewing.Viewer3D(this.viewerContainer.nativeElement, {}); // Headless viewer
 
    // Check if the viewer has already been initialised - this isn't the nicest, but we've set the env in our
    // options above so we at least know that it was us who did this!
    if (!Autodesk.Viewing.Private.env) {
      Autodesk.Viewing.Initializer(options, () => {
        this.viewer.initialize();
        this.loadDocument();
      });
    } else {
      // We need to give an initialised viewing application a tick to allow the DOM element to be established before we re-draw
      setTimeout(() => {
        this.viewer.initialize();
        this.loadDocument();
      });
    }
  }

  private loadDocument() {
    const urn = `urn:${// model urn}`;

    Autodesk.Viewing.Document.load(urn, (doc) => {
      const geometryItems = Autodesk.Viewing.Document.getSubItemsWithProperties(doc.getRootItem(), {type: 'geometry'}, true);

      if (geometryItems.length === 0) {
        return;
      }

      this.viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, this.geometryLoaded);
      this.viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (event) => this.selectionChanged(event));

      this.viewer.load(doc.getViewablePath(geometryItems[0]));
    }, errorMsg => console.error);
  }

  private geometryLoaded(event: any) {
    const viewer = event.target;

    viewer.removeEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, this.geometryLoaded);
    viewer.setLightPreset(8);
    viewer.fitToView();
    // viewer.setQualityLevel(false, true); // Getting rid of Ambientshadows to false to avoid blackscreen problem in Viewer.
  }

  private selectionChanged(event: any) {
    const model = event.model;
    const dbIds = event.dbIdArray;

    // Get properties of object
    this.viewer.getProperties(dbIds[0], (props) => {
       // Do something with properties
    });
  }

  private getAccessToken(onSuccess: any) {
    const { access_token, expires_in } = // get token
    onSuccess(access_token, expires_in);
  }
}
Advertisements

More Autodesk Forge

Back in August, I blogged about attending the Autodesk Forge DevCon is San Francisco. This month I’m again extremely fortunate and am attending Autodesk University in Las Vegas with work.

Since my previous blog, I’ve been busy on a proof of concept that marries our NBS Create specification product and the Autodesk Forge Viewer. There will be more to follow in the coming months, but for now I just wanted to capture a few features I implemented incase they are useful to anyone else.

1. Creating an extension that captures object selection

The application I’m prototyping needs to extract data from the model when an object is clicked. The Forge Viewer api documentation covers how to create and register an extension to get selection events etc. Adding functionality as an extension, is the recommended approach for adding custom functionality to the viewer.

The data my application needs from the viewer can only be obtained when the viewer has fully loaded the model’s geometry and object tree. So we have to be sure we subscribe to the appropriate events.

Create and register the extension

function NBSExtension(viewer, options) {
  Autodesk.Viewing.Extension.call(this, viewer, options);
}

NBSExtension.prototype = Object.create(Autodesk.Viewing.Extension.prototype);
NBSExtension.prototype.constructor = NBSExtension;

Autodesk.Viewing.theExtensionManager.registerExtension('NBSExtension', NBSExtension);

Subscribe and handle the events

My extension needs to handle the SELECTION_CHANGED_EVENT, GEOMETRY_LOADED_EVENT and OBJECT_TREE_CREATED_EVENT. The events are bound on the extensions “load” method.

NBSExtension.prototype.load = function () {
  console.log('NBSExtension is loaded!');

  this.onSelectionBinded = this.onSelectionEvent.bind(this);
  this.viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.onSelectionBinded);

  this.onGeometryLoadedBinded = this.onGeometryLoadedEvent.bind(this);
  this.viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, this.onGeometryLoadedBinded);

  this.onObjectTreeCreatedBinded = this.onObjectTreeCreatedEvent.bind(this);
  this.viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, this.onObjectTreeCreatedBinded);

  return true;
};

A well behaved extension should also clean up after it’s unloaded.

NBSExtension.prototype.unload = function () {
  console.log('NBSExtension is now unloaded!');

  this.viewer.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.onSelectionBinded);
  this.onSelectionBinded = null;

  this.viewer.removeEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, this.onGeometryLoadedBinded);
  this.onGeometryLoadedBinded = null;

  this.viewer.removeEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, this.onObjectTreeCreatedBinded);
  this.onObjectTreeCreatedBinded = null;

  return true;
};

When the events fire, the following functions are called to allow us to handle the event however we want:

// Event handler for Autodesk.Viewing.SELECTION_CHANGED_EVENT
NBSExtension.prototype.onSelectionEvent = function (event) {
  var currSelection = this.viewer.getSelection();

  // Do more work with current selection
}

// Event handler for Autodesk.Viewing.GEOMETRY_LOADED_EVENT
NBSExtension.prototype.onGeometryLoadedEvent = function (event) {
 
};

// Event handler for Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT
NBSExtension.prototype.onObjectTreeCreatedEvent = function (event) {

};

2. Get object properties

Once we have the selected item, we can call getProperties on the viewer to get an array of all of the property key/value pairs for that object.

var currSelection = this.viewer.getSelection();

// Do more work with current selection
var dbId = currSelection[0];

this.viewer.getProperties(dbId, function (data) {
  // Find the property NBSReference 
  var nbsRef = _.find(data.properties, function (item) {
    return (item.displayName === 'NBSReference');
  });

  // If we have found NBSReference, get the display value
  if (nbsRef && nbsRef.displayValue) {
    console.log('NBS Reference found: ' + nbsRef.displayValue);
  }
}, function () {
  console.log('Error getting properties');
});

The call to this.viewer.getSelection() returns an array of dbId’s (Database ID’s). Each Id can be passed to the getProperties function to get the properties for that dbId. My extension then looks through the array of properties for an “NBSReference” property which can be used to display the associated specification for that object.

Notice that I use Underscore.js’s _.find() function to search the array of properties. I opted for this as I found IE11 didn’t support Javascript’s native Array.prototype.find(). I like the readability of the function and Underscore.js provides the necessary polyfill for IE11.

3. Getting area and volume information

Once the geometry is loaded from the model and the internal object tree create, it’s possible to query the properties in the model that relate to area and volume. For my prototype, I wanted to sum the area and volume of types of a objects the user has selected in the model.

In order to do this, I needed to:

  1. Get the dbId of the selection item
  2. Find that dbID in the object tree
  3. Move to the object’s parent and get all of it’s children (in other words, get the siblings of the selected item)
  4. Sum the area and volume properties of the children

The first step is to build our own representation of the model tree in memory (this must effectively be how the Forge viewer displays the model tree). My code is based on this blog post by Philippe Leefsma.

var viewer = viewerApp.getCurrentViewer();
var model = viewer.model;

if (!modelTree && model.getData().instanceTree) {
  modelTree = buildModelTree(viewer.model);
}

var buildModelTree = function (model) {
  // builds model tree recursively
  function _buildModelTreeRec(node) {
    instanceTree.enumNodeChildren(node.dbId, function (childId) {
      node.children = node.children || [];

      var childNode = {
        dbId: childId,
        name: instanceTree.getNodeName(childId)
      }

      node.children.push(childNode);
      _buildModelTreeRec(childNode);
    });
  }

  // get model instance tree and root component
  var instanceTree = model.getData().instanceTree;
  var rootId = instanceTree.getRootId();
  var rootNode = {
    dbId: rootId,
    name: instanceTree.getNodeName(rootId)
  }
 
  _buildModelTreeRec(rootNode);

  return rootNode;
};

This gives us a representation of the model tree. Once we’ve located all of the siblings, we can use the dbId of each sibling to get it’s area and volume properties.

The code I wrote was based on this sample, originally written be Jim Awe I have to admit, my code is a little bit messy. There are a lot of asynchronous operations going on, which use quite a few callbacks and you do end up close to a pyramid of doom. The code was good for my needs, but I think if I was doing anything more complicated I’d look in to using Promises to tidy the code up a bit.

function _getReportData(items, callback) {
  var results = { "areaSum": 0.0, "areaSumLabel": "", "areaProps": [], "volumeSum": 0.0, "volumeSumLabel": "", volumeProps: [], "instanceCount": 0, "friendlyNotationWithSuffix": friendlyNotationWithSuffix.trim() };

  var viewer = viewerApp.getCurrentViewer();
  var nodes = items;

  nodes.forEach(function (dbId, nodeIndex, nodeArray) {
    // Find node 
    var leafNodes = getLeafNodes(dbId, modelTree);
    if (!leafNodes) return;
    results.instanceCount += leafNodes.length;

    leafNodes.forEach(function (node, leafNodeIndex, leafNodeArray) {
      viewer.getProperties(node.dbId, function (propObj) {
        for (var i = 0; i < propObj.properties.length; ++i) {
          var prop = propObj.properties[i];
          var propValue;
          var propFormat;

          if (prop.displayName === "Area") {
            propValue = parseFloat(prop.displayValue);

            results.areaSum += propValue;
            results.areaSumLabel = Autodesk.Viewing.Private.formatValueWithUnits(results.areaSum.toFixed(2), prop.units, prop.type);

            propFormat = Autodesk.Viewing.Private.formatValueWithUnits(prop.displayValue, prop.units, prop.type);
            results.areaProps.push({ "dbId": dbId, "val": propValue, "label": propFormat, "units": prop.units });
          } else if (prop.displayName === "Volume") {
            propValue = parseFloat(prop.displayValue);

            results.volumeSum += propValue;
            results.volumeSumLabel = Autodesk.Viewing.Private.formatValueWithUnits(results.volumeSum.toFixed(2), prop.units, prop.type);

            propFormat = Autodesk.Viewing.Private.formatValueWithUnits(prop.displayValue, prop.units, prop.type);
            results.volumeProps.push({ "dbId": dbId, "val": propValue, "label": propFormat, "units": prop.units });
          }
        };

        // Callback when we've processed everything
        if (callback && nodeIndex === nodeArray.length - 1 && leafNodeIndex === leafNodeArray.length - 1) {
          callback(results);
        }
      });
    });
  });
}

var getLeafNodes = function (parentNodeDbId, parentNode) {
  var result = null;

  function _getLeafNodesRec(parentNodeDbId, node) {
    // Have we found the node we're looking for?
    if (node.dbId === parentNodeDbId) {
      // We return the children (or the node itself if there are no children)
      result = node.children || [node];
    } else {
      if (node.children) {
        node.children.forEach(function (childNode, index, array) {
          if (result) return;
          _getLeafNodesRec(parentNodeDbId, childNode);
        });
      }
    }
 }

 _getLeafNodesRec(parentNodeDbId, parentNode);
 return result;
};

A couple of things to call out from the above code – The function getLeafNodes is used to get the siblings of the selected item. And the Autodesk Forge viewer has a method to nicely format volumes and areas with the appropriate units:

Autodesk.Viewing.Private.formatValueWithUnits(prop.displayValue, prop.units, prop.type);

I couldn’t actually find this documented in the API though – it was only in the samples on GitHub. But it’s a nice way of getting a nicely formatted string of values with the appropriate units.

This has been another fairly lengthy blog post – so it deserves a few screenshots of the functionality that has been implemented:

And a big shout out to Kirsty Hudson for her awesome UX work!

Autodesk Forge

Back in June, I was extremely fortunate to attend Forge DevCon 2016 at the Fort Mason Centre for Art & Culture in San Francisco.

The conference was a packed 2 days of keynotes and tech talks on the capabilities of the Forge Platform and Autodesk’s strategy for it. For those new to the platform, it is essentially a set of cloud services, APIs, and SDKs, to allow developers to quickly create the data, apps, experiences, and services that power the future of making things.

forge1

Amar Hanspal, Senior VP, Products at Autodesk introduces the Forge platform

In this blog post, I’ll show usage of the Model Derivative API, which is used to translate design files from one format to another, to prepare them for the online viewer. It can also be used to extract data from the model.

We’ll also look at the Forge Viewer, a WebGL-based, JavaScript library for 3D and 2D model rendering. 3D and 2D model data may come from a wide array of applications, such as AutoCAD, Fusion 360, Revit, IFC etc.

forge2

Fort Mason Center – San Francisco

Preparing your file for viewing

Firstly, we’ll use the Model Derivative API to upload and translate a Revit file. Files are uploaded to a “bucket”, which we’ll need to create as a one time task.

Step 1 – Create your app

Before you can get going, you need to sign in to Autodesk developer account (https://developer.autodesk.com/) and create a new application. Select the APIs you want to use and give your app a name.

forge4

Create a new app

You will then be given a Client ID and Client Secret to allow your app to obtain authentication tokens to use against the Forge APIs.

Step 2 – Obtain an authentication token

Pretty much all requests to the Forge APIs require a bearer token to authenticate them. The application I’m building up for the blog post will use ASP.NET Core and will be written in C#. I will obtain tokens with the following code:

public async Task<string> GetToken(bool allowUpload = false)
{
    HttpClient client = new HttpClient();

    client.BaseAddress = new Uri("https://developer.api.autodesk.com");
    var content = new FormUrlEncodedContent(new[] 
    {
        new KeyValuePair<string, string>("client_id", "<id>"),
        new KeyValuePair<string, string>("client_secret", "<secret>"),
        new KeyValuePair<string, string>("grant_type", "client_credentials"),
        new KeyValuePair<string, string>("scope", (allowUpload) ? "data:write data:read" : "data:read")
 });

    var result = await client.PostAsync("/authentication/v1/authenticate", content);

    JObject resultContent = JObject.Parse(await result.Content.ReadAsStringAsync());
    var token = resultContent["access_token"].ToString();

    return token;
}

This is a pretty straightforward web request, it’s worth pointing out the “scope”header. The value passed in here determines the permissions the token has e.g. read only, write, bucket creation etc.

Step 3 – Create a bucket

Models that you want to use with the Forge API’s must be uploaded to a storage area called an Open Storage Service (OSS) bucket. For the example in this blog post, this is a one time action – i.e. we will only create a bucket once and then use it for all of our models. For more information about buckets see this article.

As this is a one time action for us, we’ll use cURL to send the request to create the bucket rather than writing any C# code. We will need a bearer token though, and the token will need permission to create a bucket.

We get a token with the following request:

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'client_id=<id>&client_secret=<secret>&grant_type=client_credentials&scope=bucket:create bucket:read data:write data:read' "https://developer.api.autodesk.com/authentication/v1/authenticate"

Then create a bucket using the bearer token:

curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer <token>" -d '{
 "bucketKey":"<bucket name>",
 "policyKey":"transient"
 }' "https://developer.api.autodesk.com/oss/v2/buckets"

 Step 4 – Upload a model

Now that we have a bucket to work with, we can upload files to it for processing. The process is really simple, we send the file to the API as a byte array – passing a valid bucket name. If successful, we get the object id of the file in the bucket. Subsequent calls to the API’s will now use the objectId (or source URN), which must be passed as a BASE64 encoded string – Autodesk recommend the use of a URL safe BASE64 string (RFC 6920).

public async Task<string> PutFile(string bucketName, string fileName, byte[] array)
{
    HttpClient client = new HttpClient();

    client.BaseAddress = new Uri($"https://developer.api.autodesk.com");
    client.DefaultRequestHeaders.Add("authorization", $"Bearer {await GetToken(true)}");
    client.DefaultRequestHeaders.Add("cache-control", "no-cache");

    var content = new ByteArrayContent(array);
    content.Headers.Add("Content-Type", "application/octet-stream");

    var result = await client.PutAsync($"/oss/v2/buckets/{bucketName}/objects/{fileName}", content);

    var resultContent = await result.Content.ReadAsStringAsync();
    Console.WriteLine(resultContent.ToString());

    JObject json = JObject.Parse(resultContent);

    // Have we got an objectId?
    JToken objectId;
    if (json.TryGetValue("objectId", out objectId))
    {
        // Base64 encode the object id
        var encodedObjectId = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(objectId.ToString()));
        return encodedObjectId;
    }

    return "";
}

Step 5 – Translate the model

Next we need to request that the model we uploaded is translated to a more optimised SVF format for rendering. We do this with the following request:

public static async Task<string> Translate(string urn)
{
    HttpClient client = new HttpClient();

    client.BaseAddress = new Uri($"https://developer.api.autodesk.com");
    client.DefaultRequestHeaders.Add("authorization", $"Bearer {await GetToken(true)}");

    string json = @"{ ""input"": { ""urn"": """ + urn + @""" }, ""output"": { ""formats"": [{ ""type"": ""svf"", ""views"": [""2d"", ""3d""] }] } }";
    var content = new StringContent(json);
    content.Headers.Remove("Content-Type");
    content.Headers.Add("Content-Type", "application/json");
    
    // Force file to be re-translated if it already exists in the bucket
    //content.Headers.Add("x-ads-force", "true");

    var response = await client.PostAsync("modelderivative/v2/designdata/job", content);

    var resultContent = await response.Content.ReadAsStringAsync();
    Console.WriteLine(resultContent.ToString());

    JObject jsonPayload = JObject.Parse(resultContent);

    // Have we got a result?
    JToken result;
    if (jsonPayload.TryGetValue("result", out result))
    {
        return result.ToString();
    }

    return "";
}

Translation can take a while, in our application we want to show some feedback of progress. This can be achieved by polling another API endpoint for progress information:

public async Task<string> GetTranslationProgress(string urn)
{
    HttpClient client = new HttpClient();

    client.BaseAddress = new Uri("https://developer.api.autodesk.com");
    client.DefaultRequestHeaders.Add("authorization", $"Bearer {await GetToken()}");

    var result = await client.GetAsync($"/modelderivative/v2/designdata/{urn}/manifest");

    JObject resultContent = JObject.Parse(await result.Content.ReadAsStringAsync());
    var progress = resultContent["progress"].ToString();

    return progress;
}

Step 6 – Get model metadata

Our model has now been uploaded and is being translated, for our application we want to extract metadata from the model. We want to look for objects in the model that have a “CPI” type or instance property.

In order to obtain the metadata, we need to send a request to the Model Derivative API to obtain the metadata.

public async Task<string> GetMetadataModelGuid(string urn)
{
    HttpClient client = new HttpClient();

    client.BaseAddress = new Uri($"https://developer.api.autodesk.com");
    client.DefaultRequestHeaders.Add("authorization", $"Bearer {await GetToken(true)}");

    var response = await client.GetAsync($"/modelderivative/v2/designdata/{urn}/metadata");

    var resultContent = await response.Content.ReadAsStringAsync();
    Console.WriteLine(resultContent.ToString());

    JObject jsonPayload = JObject.Parse(resultContent);

    // Have we got a result?
    if (jsonPayload["data"]["metadata"][0]["guid"] != null)
    {
        return jsonPayload["data"]["metadata"][0]["guid"].Value<string>();
    }

    return "";
}

The response will contain a list of model views within the model – Revit files can have a number of model views. For this example application, we’ll naively return the GUID of the first model view and assume it’s the view our user is after.

We can then get all of the properties in that model view with the following request:

public async Task<JObject> GetMetadataModelProperties(string urn, string modelGuid)
{
    HttpClient client = new HttpClient();

    client.BaseAddress = new Uri($"https://developer.api.autodesk.com");
    client.DefaultRequestHeaders.Add("authorization", $"Bearer {await GetToken(true)}");

    var response = await client.GetAsync($"/modelderivative/v2/designdata/{urn}/metadata/{modelGuid}/properties");

    var resultContent = await response.Content.ReadAsStringAsync();
    Console.WriteLine(resultContent.ToString());

    JObject jsonPayload = JObject.Parse(resultContent);
    return jsonPayload;
}

Viewing the model

Everything is now setup to initialise the Forge Viewer – our model is uploaded and translated – the only thing that remains is setting up our MVC view to display the model.

Step 1 – Stylesheets

Add the following styles to your view:

<link rel="stylesheet" href="https://developer.api.autodesk.com/viewingservice/v1/viewers/style.min.css?v=2.8" type="text/css">
<link rel="stylesheet" href="https://developer.api.autodesk.com/viewingservice/v1/viewers/A360.css?v=2.8" type="text/css">

At the time of writing, v2.8 was the latest version. You can omit the version number to use the latest version, but this isn’t recommended in a Production application.

Step 2 – JavaScript reference

Next add the following JavaScript references:

https://developer.api.autodesk.com/viewingservice/v1/viewers/three.min.js?v=2.8
 https://developer.api.autodesk.com/viewingservice/v1/viewers/viewer3D.min.js?v=2.8
 https://developer.api.autodesk.com/viewingservice/v1/viewers/Autodesk360App.js?v=2.8

NOTE that the Forge viewer is built on the excellent three.js.

Step 3 – Create and initialise the viewer

The viewer is initialise with the (BASE64 encoded) source URN obtained during translation.

    var viewerApp;
    var options = {
        env: 'AutodeskProduction',
        accessToken: '@(await ForgeServices.GetToken())'
    };

    var documentId = 'urn:@ViewData["urn"]';

    Autodesk.Viewing.Initializer(options, onInitialized);

    function onInitialized() {
        viewerApp = new Autodesk.Viewing.ViewingApplication('MyViewerDiv');
        viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Private.GuiViewer3D);
        viewerApp.loadDocument(documentId, onDocumentLoaded);
    }

    function onDocumentLoaded(lmvDoc) {
        var modelNodes = viewerApp.bubble.search(av.BubbleNode.MODEL_NODE); // 3D designs
        var sheetNodes = viewerApp.bubble.search(av.BubbleNode.SHEET_NODE); // 2D designs
        var allNodes = modelNodes.concat(sheetNodes);

        if (allNodes.length) {
            viewerApp.selectItem(allNodes[0].data);
 
            if (allNodes.length === 1) {
                alert('This tutorial works best with documents with more than one viewable!');
            }
        } else {
            alert('There are no viewables for the provided URN!');
        }
    }

In the onDocumentLoaded function, we are simply selecting the first 3D view we find – agin, this is a little naive and assumes that the first 3D view is the one the user wants to see.

And that’s all there is to it – our viewer is now initialised, with all panning, zooming and orbiting goodness you’d expect. There’s even a Duke Nukem 3D style First Person mode allowing you to walk through the model with the keyboard 🙂

This slideshow requires JavaScript.

And finally…

At the start of the blog, I mentioned that I was using ASP.NET Core. All of the above screenshots were taken on a ASP.NET Core application running on Mac OS X. Microsoft are doing some amazing things with .NET, the tooling isn’t quite there yet but it is awesome seeing .NET applications running on other platforms.

forge7

ASP.NET Core MVC application running on Mac OS X El Capitan