FindInserts Retrieves All Openings in All Wall Types

On Tuesday, I presented the new

SpatialElementGeometryCalculator sample
for

calculating gross and net wall areas
.

It discusses a whole bunch of interesting aspects, e.g.:

  • Use of the SpatialElementGeometryCalculator class.
  • Porting a VB.NET Revit add-in to C#.
  • Use of the
    temporary transaction trick
    .
  • Use of filtered element collectors to determine all openings in a wall.
  • Filtered element collector optimisation possibilities, e.g. integer instead of string comparison, use of a parameter filter instead of .NET post-processing.

Vilo submitted a very relevant

comment
on that sample, asking:

Question:
Why not use (wall as HostObject).FindInserts(...) to determine the instances nested into the given wall?

Maybe the method above gives better performance?

Response:
That is a very valid question indeed.

The simple answer is: I was unaware of it, in spite of it being used in the

space adjacency for heat load calculation
sample.

In olden times, methods similar to that shown above were the only way to retrieve this information.

Hence, for instance, this relationship inverter implementation.

Thank you very much for this valuable hint!

By the way, there is no need to say ‘wall as HostObject’, because the wall is a HostObject, being derived from it. You can simply use wall. FindInserts directly.

Later:
I see what you mean now, of course. In the code above, the ‘wall’ variable is of type Element. I added the following debug code to verify that at least the number of openings retrieved is equal:


  // This approach is much more efficient and
  // entirely avoids the use of all filtered
  // element collectors.
 
  IList<ElementId> inserts = ( wall as HostObject )
    .FindInserts( true, true, true, true );
 
  Debug.Assert(
    lstTotempDel.Count.Equals( inserts.Count ),
    "expected FindInserts to return the same openings" );

The

SpatialElementGeometryCalculator GitHub repository
code
is updated now.

Thank you!

I passed on this information to Phillip Miller of Kiwi Codes Solutions Ltd, who provided the initial VB.NET implementation based on the
Revit API discussion forum thread on
door/window areas, and he replies:

Response:
You will not believe the timing on yours and Vilo’s post.

I had already made the filtered element collector more optimised as you suggested in your blog post, but like you I didn’t know about the FindInserts method. It rocks.

Just this morning I was contacted by my client saying the custom API works great on walls with hosted families but not with walls that had structure separated with lining walls, e.g., three walls making up one wall with the linings walls joined the structural wall.

As you can see in my code I was comparing FamilyInstance.HostID with the wall.id. In a compound wall situation like above the cut opening in the linings wall has a different ID that the FamilyInstance HostID, so it didn’t find any openings.

The FindInserts method solved all this in a much nicer way.

Answer:
Thank you very much for your confirmation and providing samples in which the FindInserts method really makes a significant difference, besides the performance improvement.

Implementing a solution for the compound wall situation would probably be possible using filtered element collectors as well, but much more complex.

The code using FindInserts is a lot simpler and more succinct than the filtered element collector code, even for the non-compound situation.

Here is the updated C# implementation using it:


[Transaction( TransactionMode.Manual )]
public class Command : IExternalCommand
{
  /// <summary>
  /// Convert square feet to square meters 
  /// with two decimal places precision.
  /// </summary>
  double sqFootToSquareM( double sqFoot )
  {
    return Math.Round( sqFoot * 0.092903, 2 );
  }
 
  /// <summary>
  /// Calculate wall area minus openings. Temporarily
  /// delete all openings in a transaction that is
  /// rolled back.
  /// </summary>
  /// <param name="subfaceArea">Initial gross subface area</param>
  /// <param name="wall"></param>
  /// <param name="doc"></param>
  /// <param name="room"></param>
  /// <returns></returns>
  double calwallAreaMinusOpenings(
    double subfaceArea,
    Element wall,
    Room room )
  {
    Document doc = wall.Document;
 
    // Is this a reliable way to compare documents?
 
    Debug.Assert(
      room.Document.ProjectInformation.UniqueId.Equals(
        doc.ProjectInformation.UniqueId ),
      "expected wall and room from same document" );
 
    // Determine all openings in the given wall.
 
    IList<ElementId> inserts = ( wall as HostObject )
      .FindInserts( true, true, true, true );
 
    // Determine total area of all openings.
 
    double openingArea = 0;
 
    if( 0 < inserts.Count )
    {
      Transaction t = new Transaction( doc );
 
      double wallAreaNet = wall.get_Parameter(
        BuiltInParameter.HOST_AREA_COMPUTED )
          .AsDouble();
 
      t.Start( "tmp Delete" );
      doc.Delete( inserts );
      doc.Regenerate();
      double wallAreaGross = wall.get_Parameter(
        BuiltInParameter.HOST_AREA_COMPUTED )
          .AsDouble();
      t.RollBack();
 
      openingArea = wallAreaGross - wallAreaNet;
    }
 
    return subfaceArea - openingArea;
  }
 
  public Result Execute(
    ExternalCommandData commandData,
    ref string message,
    ElementSet elements )
  {
    UIApplication app = commandData.Application;
    Document doc = app.ActiveUIDocument.Document;
 
    SpatialElementBoundaryOptions sebOptions
      = new SpatialElementBoundaryOptions();
 
    sebOptions.SpatialElementBoundaryLocation
      = SpatialElementBoundaryLocation.Finish;
 
    Result rc;
 
    try
    {
      FilteredElementCollector roomCol
        = new FilteredElementCollector( doc )
          .OfClass( typeof( SpatialElement ) );
 
      string s = "Finished populating Rooms with "
        + "Boundary Datarnrn";
 
      foreach( SpatialElement e in roomCol )
      {
        Room room = e as Room;
 
        if( room != null )
        {
          try
          {
            Autodesk.Revit.DB
              .SpatialElementGeometryCalculator
                calc = new Autodesk.Revit.DB
                  .SpatialElementGeometryCalculator(
                    doc, sebOptions );
 
            SpatialElementGeometryResults results
              = calc.CalculateSpatialElementGeometry(
                room );
 
            Solid roomSolid = results.GetGeometry();
 
            foreach( Face face in roomSolid.Faces )
            {
              IList<SpatialElementBoundarySubface>
                subfaceList = results.GetBoundaryFaceInfo(
                  face );
 
              foreach( SpatialElementBoundarySubface
                subface in subfaceList )
              {
                if( subface.SubfaceType
                  == SubfaceType.Side )
                {
                  Element wall = doc.GetElement(
                    subface.SpatialBoundaryElement
                      .HostElementId );
 
                  double subfaceArea = subface
                    .GetSubface().Area;
 
                  double netArea = sqFootToSquareM(
                    calwallAreaMinusOpenings(
                      subfaceArea, wall, room ) );
 
                  s = s + "Room "
                    + room.get_Parameter(
                      BuiltInParameter.ROOM_NUMBER )
                        .AsString()
                    + " : Wall " + wall.get_Parameter(
                      BuiltInParameter.ALL_MODEL_MARK )
                        .AsString()
                    + " : Area " + netArea.ToString()
                    + " m2rn";
                }
              }
            }
            s = s + "rn";
          }
          catch( Exception )
          {
          }
        }
      }
      TaskDialog.Show( "Room Boundaries", s );
 
      rc = Result.Succeeded;
    }
    catch( Exception ex )
    {
      TaskDialog.Show( "Room Boundaries",
        ex.Message.ToString() + "rn"
        + ex.StackTrace.ToString() );
 
      rc = Result.Failed;
    }
    return rc;
  }
}

I updated the C# implementation, but not the VB.NET one, in the
SpatialElementGeometryCalculator GitHub repository containing
the complete source code, Visual Solution files and add-in manifests for both and tagged the version presented here as

release 2015.0.0.2
.

Many thanks to Phillip and Vilo for the significant improvement of this important solution!


Comments

3 responses to “FindInserts Retrieves All Openings in All Wall Types”

  1. Håkon Clausen Avatar
    Håkon Clausen

    Hi,
    If have found two issues with this solution after playing around with it for some hours.
    1) In contrast to the “Calculating Gross and Net Wall Areas” solution, after getting the wall inserts with FindInserts it does not check that the inserts actually belongs to the room and thus removes inserts to other rooms if the wall is part of multiple rooms. This will of course lead to wrong result. Checking the instance room/toroom/fromroom parameter like the previous solution rectifies this.
    2) If a wall has multiple faces or subfaces, the opening area is subtracted multiple times. Take e.g. the hall from the 2013 basic sample (or any room with a wall with multiple doors to other rooms. This will make calwallAreaMinusOpenings report a negative wall area. Creating a gross sum for each wall, then looping through the walls found and calculate the opening area once seems to be an approach.
    I have tested something like this:
    foreach (Face face in results.GetGeometry().Faces)
    {
    foreach (var subface in results.GetBoundaryFaceInfo(face))
    {
    if (subface.SubfaceType != SubfaceType.Side) { continue; }
    var wall = this.doc.GetElement(subface.SpatialBoundaryElement.HostElementId) as HostObject;
    if (wall == null) { continue; }
    var grossArea = subface.GetSubface().Area;
    if (!walls.ContainsKey(wall.UniqueId))
    {
    walls.Add(wall.UniqueId, grossArea);
    }
    else
    {
    walls[wall.UniqueId] += grossArea;
    }
    }
    }
    var result = 0.0;
    foreach (var id in walls.Keys)
    {
    var wall = (HostObject)this.doc.GetElement(id);
    var openings = CalculateWallOpeningArea(wall, room);
    result += (walls[id] – openings);
    Debug.WriteLine(“Wall: {0} Gross {1} Net {2}”.FormatWith(wall.get_Parameter(BuiltInParameter.ALL_MODEL_MARK).AsString(), walls[id], (walls[id] – openings)));
    }

  2. Dear Håkon,
    Those sound like two absolutely valid points, and your solution to fix them looks very promising to me.
    I see that you added some other functionality that you do not explicitly mention, such as a dictionary of the affected walls.
    Could you possibly fork the SpatialElementGeometryCalculator GitHub repository, add your changes to that, and create a pull request for me to merge them back in to take a closer look at?
    https://github.com/jeremytammik/SpatialElementGeometryCalculator
    I think your improvements are very important and will be of great interest to others as well.
    Thank you!
    Cheers, Jeremy.

  3. Dear Håkon,
    I raised an issue for your suggestions:
    https://github.com/jeremytammik/SpatialElementGeometryCalculator/issues/1
    Cheers, Jeremy.

Leave a Reply to Jeremy TammikCancel reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading