Preparing a Revit Add-in for Design Automation

Today I discuss the next step in my IfcSpaceZoneBoundaries project.

This step deals with a completely different aspect than the previous discussions: how to prepare a Revit add-in for use in the DA4R or Design Automation for Revit environment, which entered public beta just last week.

I spent a considerable part of last weekend getting the DA4R version ready for testing, and figuring out how to best handle it in the VS solution:

Context, Add-In Functionality and History

First, let's reiterate and update description of the architecture and data flow.

The add-in functionality that we wish to make use of in the DA4R environment consists in opening a Revit file, extracting the room and zone data stored in it and saving that to CSV.

The Revit document is actually an .ifc.RVT file created by the IFC linking-in process, currently executed on the desktop.

As it stands now, the .ifc.RVT file has to be generated by linking in the IFC file using a standard Revit add-in on the desktop, because the required functionality provided by the revit-ifc open source is not yet available in DA4R:

Unfortunately, in this scenario, the IFC linking-in step takes place on the desktop, not in DA4R. Since the add-in implements it, it can be fully automated. A more direct solution would be to implement an IFC parser and extract the required information directly from there, bypassing the Revit conversion for that step.

Please also refer to these previous discussions leading up to where we are with this today:

Preparing the Add-In for DA4R

I spent some thought on how to best prepare the existing add-in project for creating the DA4R version.

Create a separate branch in GitHub? Define compile-time switches? Implement a multi-target Visual Studio project?

In the end, I decided that the simplest and most obvious approach would probably be best:

Accordingly, the Visual Studio solution now contains three projects:

Both the desktop add-in and the Forge DA4R AppBundle use the same Exporter class library and only contain minimal architectural code.

Local Testing versus Live Deployment

In addition to the separation between desktop add-in and DA4R appbundle, the latter can also be switched back and forth for local testing versus real live Forge DA4R deployment, by defining or undefining the constant FORGE_DA4R_TEST_LOCALLY in AppBundle/App.cs.

Here is a diff between the two states.

Depending on which state we are in, we use either the ApplicationInitialized or the DesignAutomationReadyEvent to run the application functionality.

Here is the entire external DB application implementation:

//#define FORGE_DA4R_TEST_LOCALLY

#region Namespaces
using System;
using System.IO;
using System.Reflection;
using Autodesk.Revit.ApplicationServices;
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Events;
using DesignAutomationFramework;
using IfcSpaceZoneBoundaries.Exporter;
#endregion

namespace IfcSpaceZoneBoundaries.AppBundle
{
  class App : IExternalDBApplication
  {
    /// <summary>
    /// Export all linked-in IFC document rooms and zones
    /// </summary>
    int ExportLinkedInIfcDocs( Application app )
    {
      if( 0 == app.Documents.Size )
      {
        string path = JtSettings.Instance
          .IfcRvtInputFilePath;

        Document doc = app.OpenDocumentFile( path );

        if( doc == null )
        {
          string s = string.Format(
            "Could not open document {0}.", path );

          JtLogger.Log( s );

          throw new InvalidOperationException( s );
        }
      }
      return RoomZoneExporter.ExportAll( app );
    }

#if FORGE_DA4R_TEST_LOCALLY
    void OnApplicationInitialized(
      object sender,
      ApplicationInitializedEventArgs e )
    {
      // `sender` is an Application instance:

      Application app = sender as Application;

      ExportLinkedInIfcDocs( app );
    }
#else // if not FORGE_DA4R_TEST_LOCALLY
    private void OnDesignAutomationReadyEvent( 
      object sender, 
      DesignAutomationReadyEventArgs e )
    {
      // `sender` is an Application instance:

      Application app = sender as Application;

      ExportLinkedInIfcDocs( app );
    }
#endif // FORGE_DA4R_TEST_LOCALLY

    public ExternalDBApplicationResult OnStartup(
      ControlledApplication a )
    {
      string path = Assembly.GetExecutingAssembly().Location;

      JtLogger.Init( Path.ChangeExtension( path, "log" ) );

      JtSettings.Init( Path.ChangeExtension( path, "json" ) );

#if FORGE_DA4R_TEST_LOCALLY
      a.ApplicationInitialized += OnApplicationInitialized;
#else // if not FORGE_DA4R_TEST_LOCALLY
      DesignAutomationBridge.DesignAutomationReadyEvent 
        += OnDesignAutomationReadyEvent;
#endif // FORGE_DA4R_TEST_LOCALLY

      return ExternalDBApplicationResult.Succeeded;
    }

    public ExternalDBApplicationResult OnShutdown(
      ControlledApplication a )
    {
      JtSettings.Save();
      JtLogger.Done();
      return ExternalDBApplicationResult.Succeeded;
    }
  }
}

User Defined Input Arguments for DA4R

I implemented the JtSettings class to demonstrate defining and passing in input parameters to a DA4R app via an input parameter file.

I simplified and minimised its interface to reduce the amount of code to be duplicated in the add-in and appbundle, e.g., by converting it to a self-contained singleton.

The interface to read the user-defined settings in DA4R currently consists of just two lines of code, as you can see above:

  string path = JtSettings.Instance
    .IfcRvtInputFilePath;

Logging of Results

Similarly to the enhanced settings class, I also streamlined the implementation of the JtLogger warning, error and result message logging system to make it easy and minimal to use from the two environments.

I hope you enjoyed my analysis and description and wish you lots of fun and success getting started with your own DA4R projects!

Log raft