Link in and Analyse IFC File Zones and Spaces

I've been fiddling around a bit lately extracting room and zone information from IFC files, as you may have noticed from my previous discussions of exporting room boundaries to CSV and retrieving linked IfcZone elements using Python.

I now implemented more functionality in this direction, to extract both geometry and relationships, namely the room and zone boundaries as well as the room to zone containment or allocation.

It is included in my new IfcSpaceZoneBoundaries add-in project.

It retrieves this information and exports it to CSV.

Besides that, it demonstrates a host of other important aspects:

In the process of implementing all this, I encountered and resolved a number of interesting issues.

Here are some of them:

What Happens on Linking in an IFC File?

Linking in an IFC file behaves differently and generates different elements than importing it.

For instance, let's assume we are given an IFC file X.ifc and a new empty blank host RVT to link it into, e.g.:

These additional files are generated by the linking-in process launched by calling the manual built-in Link IFC command:

The IFC data that I am interested in for rooms and zones originates in the geometry and properties.

The geometry is converted to DirectShape elements, the properties to shared parameters, and the IfcSpaceZoneBoundaries add-in can retrieve all of these and export them to CSV for further processing, e.g., integrating with Forge data in a viewer extension.

I assume that the .ifc.RVT file created by this process contains all the information I need, i.e., the room and zone geometry and relationships as DirectShape elements.

In that case, I can read it directly from the .ifc.RVT file and have no need for the hosting RVT at all, except as a place from which to launch the linking command.

Linking in an IFC File Programmatically

Can I launch the linking-in process programmatically as well?

I succeeded at implementing a method that automatically links in an IFC file into a blank RVT project.

I first thought the process of programmatically linking in the IFC file would be pretty straightforward. However, looking more closely at the CreateFromIFC method documentation, it is probably not completely trivial. It says:

This function is one of a series of steps necessary for linking an IFC file. To understand how it is used in context, please download the IFC open source code, and look in the Revit.IFC.Import project at Importer.ImportIFC(ImporterIFC importer), under the IFCImportAction.Link branch.

Based on this information and private communication with Angel Velez, I implemented the following method CreateIfcLink to successfully link in an IFC file into a blank RVT project:

  /// <summary>
  /// Create a link to a given IFC file.
  /// Return true on success.
  /// </summary>
  bool CreateIfcLink(
    Document doc,
    string ifcpath )
  {
    bool rc = false;

    IDictionary<stringstring> options
      = new Dictionary<stringstring>( 2 );

    options["Action"] = "Link"// default is "Open"
    options["Intent"] = "Reference"// this is the default

    Importer importer = Importer.CreateImporter(
      doc, ifcpath, options );

    try
    {
      importer.ReferenceIFC( doc, ifcpath, options );
      rc = true;
    }
    catchException ex )
    {
      ifnull != Importer.TheLog )
        Importer.TheLog.LogError(
          -1, ex.Message, false );
    }
    finally
    {
      ifnull != Importer.TheLog )
        Importer.TheLog.Close();
      ifnull != IFCImportFile.TheFile )
        IFCImportFile.TheFile.Close();
    }
    return rc;
  }

Please note that this method requires references to the RevitAPIIFC and Revit.IFC.Import .NET assembly libraries.

Retrieving All Linked-In IFC Files

As said, the information I am after is only present in Revit if the IFC file is linked in to a host project, not imported.

Furthermore, the linked-in IFC files are converted to .ifc.RVT documents and can thus be recognised by this extension.

Based on that information and the assumption that no other files are equipped with this extension, I implemented a method GetLinkedInIfcDocs to retrieve all the linked-in IFC files from the Revit application:

  /// <summary>
  /// Retrieve and return all linked-in IFC documents.
  /// </summary>
  List<Document> GetLinkedInIfcDocs( Application app )
  {
    List<Document> ifcdocs = null;
    DocumentSet docs = app.Documents;
    int n = docs.Size;

    App.Log( string.Format( "{0} open document{1}",
      n, Util.PluralSuffix( n ) ) );

    foreachDocument d in docs )
    {
      string s = d.PathName;
      if( s.EndsWith( ".ifc.RVT" ) )
      {
        ifnull == ifcdocs)
        {
          ifcdocs = new List<Document>();
        }
        ifcdocs.Add( d );
      }
    }
    return ifcdocs;
  }

Processing Linked-In IFC Files

The add-in mainline Execute method uses the two methods above to retrieve all currently linked-in IFC files.

If none are found, we assume that a specific IFC file needs to be processed.

It is specified in the user configuration settings input file, formatted in JSON.

The IFC file path is read from the settings file, and a link to it is generated in the current document, which thus becomes the host file.

After the linking-in process, we can retrieve all linked-in IFC documents again; this time, we expect to find at least the one we created the link for:

  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;

    // Retrieve all linked-in IFC documents

    List<Document> ifcdocs = GetLinkedInIfcDocs( app );

    ifnull == ifcdocs || 0 == ifcdocs.Count )
    {
      // If no IFC links are present, create one

      string path = App.Settings.IfcInputFilePath;

      if( CreateIfcLink( doc, path ) )
      {
        ifcdocs = GetLinkedInIfcDocs( app );
      }
    }

    int n = ifcdocs.Count;

    App.Log( string.Format(
      "{0} linked-in IFC document{1} found.",
      n, Util.PluralSuffix( n ) ) );

    foreachDocument ifcdoc in ifcdocs )
    {
      App.Log( "Linked-in IFC document: "
        + ifcdoc.PathName );

      RoomZoneExporter a = new RoomZoneExporter(
        ifcdoc );
    }
    return ( 0 < n ) 
      ? Result.Succeeded 
      : Result.Failed;
  }
}

Room and Zone Data

The RoomZoneData class retrieves and stores the information exported to CSV output for each room and zone:

class RoomZoneData
{
  /// <summary>
  /// These are the data fields exported for each 
  /// room and zone. The first is simply 'S' or 'Z'.
  /// The zone and layer properties are only set 
  /// on room object records.
  /// </summary>
  public string Space_or_Zone;
  public string GUID;
  public string Name;
  public string Zone;
  public string Layer;
  public string Pset;
  public string Z;
  public string Boundary;

  /// <summary>
  /// Predicate indicating a valid room or zone
  /// </summary>
  public bool IsRoomOrZone
  {
    get { return null != Space_or_Zone; }
  }

  /// <summary>
  /// Private constant strings for retrieving IFC
  /// properties
  /// </summary>
  const string _pname_export_as = "IfcExportAs";
  const string _pname_guid = "IfcGUID";
  const string _pname_name = "IfcName";
  const string _pname_layer = "IfcPresentationLayer";
  const string _pname_pset = "IfcPropertySetList";
  const string _pname_zone = "IfcZone";
  const string _export_as_room = "IfcSpace.INTERNAL";
  const string _export_as_zone = "IfcZone";

  /// <summary>
  /// Export CSV format using comma separated fields 
  /// with no other delimiters
  /// </summary>
  const string _format_string = "{0},{1},{2},{3},{4},{5},{6},{7}";

  /// <summary>
  /// Instantiate a room or zone data object from
  /// a given Revit element `e`
  /// </summary>
  public RoomZoneData( Element e )
  {
    string export_as = Util.GetStringParamValue(
      e, _pname_export_as );

    if( export_as.Equals( _export_as_room ) )
    {
      Space_or_Zone = "S";
    }
    else if( export_as.Equals( _export_as_zone ) )
    {
      Space_or_Zone = "Z";
    }

    if( IsRoomOrZone )
    {
      GUID = Util.GetStringParamValue( e, _pname_guid );
      Name = Util.GetStringParamValue( e, _pname_name );
      Zone = Util.GetStringParamValue( e, _pname_zone );
      Layer = Util.GetStringParamValue( e, _pname_layer );
      Pset = Util.GetStringParamValue( e, _pname_pset );
      Boundary = Util.GetBottomFaceBoundaryStringAndZ( e, out Z );
    }
  }

  /// <summary>
  /// Return a string to export room or zone data 
  /// to CSV
  /// </summary>
  public string AsString()
  {
    return string.Format( _format_string,
      Space_or_Zone,
      GUID,
      Name,
      Zone,
      Layer,
      Pset,
      Z,
      Boundary );
  }
}

Geometric Solid, Face and Vertex Processing

I have implemented and documented all kinds of algorithms to retrieve and process geometric solid, face and vertex information in the Revit API many times over in the past, so it has become pretty straightforward to adapt to new needs now.

I won't dive into any explicit details here. You can look at the source code yourself. It is pretty self-explanatory.

The Util class handles the nitty-gritty details of retrieving the bottom face of a solid and its vertices.

The IntPoint2d and IntPoint3d classes convert the units from imperial feet to millimetres and store the latter as integers, eliminating some precision problems and simplifying the file formatting, since no more decimal places are needed.

Zone