DirectShape Element to Represent Room Volume

Yesterday, I implemented a new add-in, RoomVolumeDirectShape, that creates DirectShape elements representing the volumes of all the rooms.

I'll also mention some challenges encountered en route, some free add-ins shared by Cherry BIM Services, an insight in the meaning of the MEP fitting Loss Method property, and AI-generated talking head models:

Request to Display Room Volumes in Forge SVF File

The RoomVolumeDirectShape add-in was inspired by the following request:

The context: We are building digital twins out of BIM data. To do so, we use Revit, Dynamo, and Forge.

The issue: We rely on the rooms in Revit to perform a bunch of tasks (reassign equipment localization, rebuild a navigation tree, and so on).

Unfortunately, these rooms are not displayed in the Revit 3D view.

Therefore, they are nowhere to be found in the Forge SVF file.

Our (so-so) solution: uses Dynamo to extract the room geometry and build Revit volumes.

It works, but it is:

The whole process amounts to several hours of manual work.

We want to fix this.

Our goal: A robust implementation that will get rid of Dynamo, automate the process in Revit, and in the end, run that in a Forge Design Automation process.

The ideal way forward is exactly what you describe: A native C# Revit API that find the rooms, creates a direct shape volume for them, and copy their properties to that.

No intermediate formats, no UI, just straight automation work.

RoomVolumeDirectShape Functionality

Fulfilling this request, I implemented a new RoomVolumeDirectShape add-in that performs the following simple steps:

Retrieving All Element Properties

The GetParamValues method retrieves and returns all the element parameter values in a dictionary mapping parameter names to the corresponding values.

For each entry, it also appends a single-character indicator of the parameter storage type to the key.

It makes use of two helper methods:

    /// <summary>
    /// Return parameter storage type abbreviation
    /// </summary>
    static char ParameterStorageTypeChar(
      Parameter p )
    {
      ifnull == p )
      {
        throw new ArgumentNullException(
          "p""expected non-null parameter" );
      }

      char abbreviation = '?';

      switch( p.StorageType )
      {
        case StorageType.Double:
          abbreviation = 'r'// real number
          break;
        case StorageType.Integer:
          abbreviation = 'n'// integer number
          break;
        case StorageType.String:
          abbreviation = 's'// string
          break;
        case StorageType.ElementId:
          abbreviation = 'e'// element id
          break;
        case StorageType.None:
          throw new ArgumentOutOfRangeException(
            "p""expected valid parameter "
            + "storage type, not 'None'" );
      }
      return abbreviation;
    }

    /// <summary>
    /// Return parameter value formatted as string
    /// </summary>
    static string ParameterToString( Parameter p )
    {
      string s = "null";

      if( p != null )
      {
        switch( p.StorageType )
        {
          case StorageType.Double:
            s = p.AsDouble().ToString( "0.##" );
            break;
          case StorageType.Integer:
            s = p.AsInteger().ToString();
            break;
          case StorageType.String:
            s = p.AsString();
            break;
          case StorageType.ElementId:
            s = p.AsElementId().IntegerValue.ToString();
            break;
          case StorageType.None:
            s = "none";
            break;
        }
      }
      return s;
    }

    /// <summary>
    /// Return all the element parameter values in a
    /// dictionary mapping parameter names to values
    /// </summary>
    static Dictionary<stringstring> GetParamValues(
      Element e )
    {
      // Two choices: 
      // Element.Parameters property -- Retrieves 
      // a set containing all the parameters.
      // GetOrderedParameters method -- Gets the 
      // visible parameters in order.

      //IList<Parameter> ps = e.GetOrderedParameters();

      ParameterSet pset = e.Parameters;

      Dictionary<stringstring> d
        = new Dictionary<stringstring>( pset.Size );

      foreachParameter p in pset )
      {
        // AsValueString displays the value as the 
        // user sees it. In some cases, the underlying
        // database value returned by AsInteger, AsDouble,
        // etc., may be more relevant, as done by 
        // ParameterToString

        string key = string.Format( "{0}({1})",
          p.Definition.Name,
          ParameterStorageTypeChar( p ) );

        string val = ParameterToString( p );

        if( d.ContainsKey( key ) )
        {
          if( d[key] != val )
          {
            d[key] += " | " + val;
          }
        }
        else
        {
          d.Add( key, val );
        }
      }
      return d;
    }

Converting a .NET Dictionary to JSON

FormatDictAsJson converts the .NET dictionary of element properties to a JSON-formatted string:

    /// <summary>
    /// Return a JSON string representing a dictionary
    /// mapping string key to string value.
    /// </summary>
    static string FormatDictAsJson( 
      Dictionary<stringstring> d )
    {
      List<string> keys = new List<string>( d.Keys );
      keys.Sort();

      List<string> key_vals = new List<string>( 
        keys.Count );

      foreachstring key in keys )
      {
        key_vals.Add( 
          string.Format( "\"{0}\" : \"{1}\"",
            key, d[key] ) );
      }
      return "{" + string.Join( ", ", key_vals ) + "}";
    }

Generating DirectShape from ClosedShell

With the element parameter property retrieval and JSON formatting helper methods in place, very little remains to be done.

We gather all the rooms in the BIM using a filtered element collector, aware of the fact that the Room class only exists in the Revit API, not internally in Revit.

The filtered element collector therefore has to retrieve SpatialElement objects instead and use .NET post-processing to extract the rooms, cf. accessing room data.

Once we have the rooms, we can process each one as follows:

  GeometryElement geo = r.ClosedShell;

  Dictionary<stringstring> param_values
    = GetParamValues( r );

  string json = FormatDictAsJson( param_values );

  DirectShape ds = DirectShape.CreateElement(
    doc, _id_category_for_direct_shape );

  ds.ApplicationId = id_addin;
  ds.ApplicationDataId = r.UniqueId;
  ds.SetShape( geo.ToList<GeometryObject>() );
  ds.get_Parameter( _bip_properties ).Set( json );
  ds.Name = "Room volume for " + r.Name;

Complete External Command Class Execute Method

For the sake of completeness, here is the entire external command class and execute method implementation:

#region Namespaces
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using Autodesk.Revit.ApplicationServices;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;
using Autodesk.Revit.UI;
#endregion

namespace RoomVolumeDirectShape
{
  [TransactionTransactionMode.Manual )]
  public class Command : IExternalCommand
  {
    // Cannot use OST_Rooms; DirectShape.CreateElement 
    // throws ArgumentExceptionL: Element id categoryId 
    // may not be used as a DirectShape category.

    /// <summary>
    /// Category assigned to the room volume direct shape
    /// </summary>
    ElementId _id_category_for_direct_shape
      = new ElementIdBuiltInCategory.OST_GenericModel );

    /// <summary>
    /// DirectShape parameter to populate with JSON
    /// dictionary containing all room properies
    /// </summary>
    BuiltInParameter _bip_properties
      = BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS;

// ... Property retrieval and JSON formatting helper methods ...

    public Result Execute(
      ExternalCommandData commandData,
      ref string message,
      ElementSet elements )
    {
      UIApplication uiapp = commandData.Application;
      UIDocument uidoc = uiapp.ActiveUIDocument;
      Application app = uiapp.Application;
      Document doc = uidoc.Document;

      string id_addin = uiapp.ActiveAddInId.ToString();

      IEnumerable<Room> rooms
        = new FilteredElementCollector( doc )
        .WhereElementIsNotElementType()
        .OfClass( typeofSpatialElement ) )
        .Where( e => e.GetType() == typeofRoom ) )
        .Cast<Room>();

      usingTransaction tx = new Transaction( doc ) )
      {
        tx.Start( "Generate Direct Shape Elements "
          + "Representing Room Volumes" );

        foreachRoom r in rooms )
        {
          Debug.Print( r.Name );

          GeometryElement geo = r.ClosedShell;

          Dictionary<stringstring> param_values
            = GetParamValues( r );

          string json = FormatDictAsJson( param_values );

          DirectShape ds = DirectShape.CreateElement(
            doc, _id_category_for_direct_shape );

          ds.ApplicationId = id_addin;
          ds.ApplicationDataId = r.UniqueId;
          ds.SetShape( geo.ToList<GeometryObject>() );
          ds.get_Parameter( _bip_properties ).Set( json );
          ds.Name = "Room volume for " + r.Name;
        }
        tx.Commit();
      }
      return Result.Succeeded;
    }
  }
}

For the full Visual Studio solution and updates to the code, please refer to The RoomVolumeDirectShape GitHub repository.

Sample Model and Results

I tested this in the standard Revit rac_basic_sample_project.rvt sample model:

Revit Architecture rac_basic_sample_project.rvt

Isolated, the resulting direct shapes look like this:

DirectShape elements representing room volumes

Challenges Encountered Underway

I ran into a couple of issues en route that cost me time to resolve, ever though absolutely trivial, so I'll make a note of them here for my own future reference:

Licensing System Error 22

Something happened on my virtual Windows machine, and I saw an error saying:

  ---------------------------
  Autodesk Revit 2020
  ---------------------------
  Licensing System Error 22 
  Failed to locate Adls
  ---------------------------
  OK   
  ---------------------------

Luckily, a similar issue has already been discussed in the forum thread on licensing system error 22 – failed to locate Adls.

The solution described there worked fine in my case as well:

Valid Direct Shape Categories

I had to fiddle a bit choosing which category to use for the DirectShape element creation.

The rooms category is not acceptable, generic model and structural framing is.

Attempting to use an invalid category throws an ArgumentException saying, Element id categoryId may not be used as a DirectShape category.

Direct Shape Phase and Visibility

Right away after the first trial run, I could see the resulting DirectShape elements in RevitLookup, and all their properties looked fine.

However, try as I might, I was unable to see them in the Revit 3D view...

...until I finally flipped through the phases and found the right one.

The model is apparently in a state in which newly created geometry lands in a phase that is not displayed in the default 3D view.

Cherry BIM Services

Enough on my activities.

Someone else has also been pretty active recently:

Ninh Truong Huu Ha of Cherry BIM Services recently shared several free Revit add-ins, and also published code for one of them.

Oops, the code has disappeared again from Ninh's GitHub repository; in fact, the whole repository disappeared...

Inspired by Jeremy Tammik and Harry Mattison who always share their incredible knowledge to the world, I decided from now on, all of my Revit add-ins will be free to use for all Revit users. One year ago, I had absolutely zero knowledge of the coding world, e.g., C#, Revit API, Visual Studio, etc. I would never have thought that someday I could have my own Revit add-in published in the Autodesk Store.

Many thanks to Ninh for sharing these tools!

On the Value of the 'Loss Method' Property

Next, let's point out an MEP analysis related question raised and solved by Hanley Deng in the Revit API discussion forum thread on how to get the value of the property 'Loss Method':

Question: Pipe fittings have a property named "Loss Method".

In the UI, its value is "Use Definition on Type".

In the API, however, the value is a GUID, e.g., "3bf616f9-6b98-4a21-80ff-da1120c8f6d6":

Loss method parameter property

How can I convert the API GUID value, "3bf616f9-6b98-4a21-80ff-da1120c8f6d6", into the UI value, "Use Definition on Type"?

Answer: The loss method can be programmed, so the GUID you see might be something like the add-in identifier, c.f. this discussion on the pipe fitting K factor.

Response: Problem solved. This problem is solved in 2 cases:

  1. For Pipe fittings, when Loss Method is "Use definition on Type": In this case, the parameter.AsString() value equals the GUID stored in Autodesk.Revit.DB.MEPCalculatationServerInfo.PipeUseDefinitionOnTypeGUID. In this case, I cannot find the UI display string for it, so I hardcode the UI display string.
  2. I all other cases, including other values in Pipe Fittings, and all the values in Duct Fittings, the ServerName is the string in the UI display, accessible through the following API call:
  Autodesk.Revit.DB.MEPCalculatationServerInfo
    .GetMEPCalculationServerInfo(objFamilyInstance), 

Many thanks to Hanley for clarifying this.

AI-Generated Talking Head Models

Finally, let's close with this impressive demonstration of AI simulated talking head models, presented in the five-minute video on few-shot adversarial learning of realistic neural talking head models: