Roomedit3d Live Real-Time BIM Update Recording

I completed the first running version of my roomedit3d project connecting BIM and the cloud demonstrating two cool possibilities to enhance interaction with the View and Data API:

Let's take a closer look at the implementation and a test run recording:

Overview

All the background information and full discussions of the implementation details are provided in my mention of the initial idea, the working proof of concept with a C# .NET console test application and the overview of the Revit-independent implementation aspects.

Here is a quick recapitulation:

Roomedit3d Implementation

All the interesting aspects are implemented by just two modules, the BIM updater and the external application managing the socket.io subscription and external event.

In fact, the BIM updater is kind of trivial, too, so really App.cs is the only really interesting part :-)

Let's look at both, though, and the external command as well, for the sake of completeness:

External Command to Toggle Broadcast Subscription

[TransactionTransactionMode.ReadOnly )]
public class Command : IExternalCommand
{
  public Result Execute(
    ExternalCommandData commandData,
    ref string message,
    ElementSet elements )
  {
    bool subscribed = App.ToggleSubscription();

    TaskDialog.Show( "Toggled Subscription",
      ( subscribed ? "S" : "Uns" ) + "ubscribed." );

    return Result.Succeeded;
  }
}

BimUpdater External Event Handler

/// BIM updater, driven both via external 
/// command and external event handler.
/// </summary>
class BimUpdater : IExternalEventHandler
{
  /// <summary>
  /// The queue of pending tasks consisting 
  /// of UniqueID and translation offset vector.
  /// </summary>
  Queue<Tuple<stringXYZ>> _queue = null;

  public BimUpdater()
  {
    _queue = new Queue<Tuple<stringXYZ>>();
  }

  /// <summary>
  /// Execute method invoked by Revit via the 
  /// external event as a reaction to a call 
  /// to its Raise method.
  /// </summary>
  public void Execute( UIApplication a )
  {
    Document doc = a.ActiveUIDocument.Document;

    using ( Transaction t = new Transaction( doc ) )
    {
      t.Start( GetName() );

      while ( 0 < _queue.Count )
      {
        Tuple<stringXYZ> task = _queue.Dequeue();

        Debug.Print( "Translating {0} by {1}",
          task.Item1, Util.PointString( task.Item2 ) );

        Element e = doc.GetElement( task.Item1 );
        ElementTransformUtils.MoveElement(
          doc, e.Id, task.Item2 );
      }
      t.Commit();
    }
  }

  /// <summary>
  /// Required IExternalEventHandler interface 
  /// method returning a descriptive name.
  /// </summary>
  public string GetName()
  {
    return App.Caption + " " + GetType().Name;
  }

  /// <summary>
  /// Enqueue a BIM update action to be performed,
  /// consisting of UniqueID and translation 
  /// offset vector.
  /// </summary>
  public void Enqueue( string uid, XYZ offset )
  {
    _queue.Enqueue(
      new Tuple<stringXYZ>(
        uid, offset ) );
  }
}

External Application Managing Socket.io and External Event

class App : IExternalApplication
{
  /// <summary>
  /// Caption
  /// </summary>
  public const string Caption = "Roomedit3d";

  /// <summary>
  /// Socket broadcast URL.
  /// </summary>
  const string _url = "https://roomedit3d.herokuapp.com:443";

  #region External event subscription and handling
  /// <summary>
  /// Store the external event.
  /// </summary>
  static ExternalEvent _event = null;

  /// <summary>
  /// Store the external event.
  /// </summary>
  static BimUpdater _bimUpdater = null;

  /// <summary>
  /// Store the socket we are listening to.
  /// </summary>
  static Socket _socket = null;

  /// <summary>
  /// Provide public read-only access to external event.
  /// </summary>
  public static ExternalEvent Event
  {
    get { return _event; }
  }

  /// <summary>
  /// Enqueue a new BIM updater task.
  /// </summary>
  static void Enqueue( object data )
  {
    JObject data2 = JObject.FromObject( data );

    string s = string.Format(
      "transform: uid={0} ({1:0.00},{2:0.00},{3:0.00})",
      data2["externalId"], data2["offset"]["x"],
      data2["offset"]["y"], data2["offset"]["z"] );

    Util.Log( "Enqueue task " + s );

    string uid1 = data2["externalId"].ToString();
    XYZ offset1 = new XYZ(
      double.Parse( data2["offset"]["x"].ToString() ),
      double.Parse( data2["offset"]["y"].ToString() ),
      double.Parse( data2["offset"]["z"].ToString() ) );

    string uid = (string) data2["externalId"];
    XYZ offset = new XYZ(
      (double) data2["offset"]["x"],
      (double) data2["offset"]["y"],
      (double) data2["offset"]["z"] );

    _bimUpdater.Enqueue( uid, offset );
    _event.Raise();
  }

  /// <summary>
  /// Toggle on and off subscription to automatic 
  /// BIM update from cloud. Return true when subscribed.
  /// </summary>
  public static bool ToggleSubscription()
  {
    if ( null != _event )
    {
      Util.Log( "Unsubscribing..." );

      _socket.Disconnect();
      _socket = null;

      _bimUpdater = null;

      _event.Dispose();
      _event = null;

      Util.Log( "Unsubscribed." );
    }
    else
    {
      Util.Log( "Subscribing..." );

      _bimUpdater = new BimUpdater();

      var options = new IO.Options()
      {
        IgnoreServerCertificateValidation = true,
        AutoConnect = true,
        ForceNew = true
      };

      _socket = IO.Socket( _url, options );

      _socket.On( Socket.EVENT_CONNECT, ()
        => Util.Log( "Connected" ) );

      _socket.On( "transform", data
        => Enqueue( data ) );

      _event = ExternalEvent.Create( _bimUpdater );

      Util.Log( "Subscribed." );
    }
    return null != _event;
  }
  #endregion // External event subscription and handling

  public Result OnStartup( UIControlledApplication a )
  {
    return Result.Succeeded;
  }

  public Result OnShutdown( UIControlledApplication a )
  {
    return Result.Succeeded;
  }
}

That's all there is too it!

Are you surprised how short and easy it is?

La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il n'y a plus rien à retirer.

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.

Antoine de Saint-Exupéry

Test Run Video Recording

Now the time has come to show the full solution running live, connecting the View and Data API viewer and the Revit add-in updating the BIM live in real-time.

Here is a five-minute video recording showing the system up and running:

To my great surprise, I do not have to do anything at all to convert or transform the viewer coordinates back into the Revit model.

In other words, the viewer seems to be using the same units as Revit does, i.e., imperial feet, and swapping the X, Y and Z axes appropriately too.

I had expected to have implement and apply some kind of transformation myself.

Download

The version up and running in the recording above is the Revit add-in Roomedit3dApp release 2017.0.0.4 with the web server and viewer hosted by roomedit3d release 0.0.4 running on Heroku.

The most up-to-date versions are always provided by the Roomedit3dApp and roomedit3d master branches, respectively, and the main documentation is in the latter.

I hope you find this useful and wish you much fun and success connecting the desktop and the cloud, and BIM with many powerful Forge and own custom web services.

By the way, I very much hope you can make your way to the Forge DevCon coming up real soon now, in just two weeks time!

I look forward to seeing you there!

Many thanks to Philippe Leefsma for all his help in Barcelona getting the basics up and running! Philippe implemented most of the code in the web server, including the viewer management, viewer extension and socket.io broadcast.

To Do

This completes the old first item in my previous to do list.

Here is an update with a new first entry:

iTerm2

I just installed and started using iTerm2, again based on Philippe's recommendation last week.

So far, the switch from the standard Mac terminal to iTerm2 is completely seamless, and now I am looking forward to discovering and exploring the numerous advantages one by one as they in handy.

Thanks again, Philippe!