Calculating Gross and Net Wall Areas

Determination of gross and net areas and volumes is fundamental to BIM.

Here is an interesting solution to determine the gross and net area of a wall, i.e. with and without its openings, making use of the SpatialElementGeometryCalculator and the
temporary transaction trick.

The question was raised and solved by Phillip Miller of Kiwi Codes Solutions Ltd, starting with the
Revit API discussion forum thread on
door/window areas:

Question: I need to determine the cut areas of windows and doors in a wall. I thought this would be a simple matter of grabbing the BIP “HOST_Area_Computed” as with my testing that was returning the correct area. It turns out that this is not the case from further testing.

Please refer to the attached RVT file with two doors inserted into a wall. They look similar but one is reporting through RevitLookup 4m2 and the other 2m2.

Why is this? What actually is “HOST_Area_Computed” reporting? What is the most reliable way of getting the cut area of windows and doors?

Answer: I cannot really say anything about the values reported by the HOST_Area_Computed parameter. That belongs to the user interface, as far as I am concerned, and you would have to ask a product usage expert or application engineer to explain the meaning of that.

For a pure API perspective, I assume you are aware of the
Revit SDK MaterialQuantities sample?

Does that not give you pretty much exactly what you need?

Response: Thank you for your reply and also the link to the SDK example. I will look into that in the morning.

To clarify what I’m doing, is, I want to find the surface areas of walls that are associated with Rooms. To do this I’m making use of the SpatialElementGeometryCalculator class that is perfect as it does give areas of walls, floor and ceilings even if they are angled. The only problem is it doesn’t subtract things like doors and windows.

Your link to your blog has jogged my memory though with a workaround that I could implement. I know what doors and windows are associated with the room and also the wall associated with the rooms face, so I could get the area of the wall (which does calculate the gross – openings), then remove the associated doors and windows grab the new area and minus it from the gross and then turn back the transaction.
I’m pretty sure that will then give me the true area of the room’s walls.

Answer: Your use of the SpatialElementGeometryCalculator sounds very interesting.

The Building Coder currently does not provide any samples of using that except the rather complex
space adjacency for heat load calculation.

Response: Thank you so much for your suggestions. The good news is that the temporary deletion of cutting objects in the wall works very well. I’m not to sure why the BIP Area parameter is reporting odd values but I suppose that does not matter any more.

I took a look at your suggested SDK examples and they didn’t really apply to my situation as I was requiring the surface areas of walls associated to rooms.
As a wall can span multiple rooms I had two choices.
First up I used the room bounding object to extract the wall information.
The downside to this approach is that you had to take the boundary segment length and then multiply that by the wall height, hoping that the wall had a consistent wall height (not always the case).
I also was requiring the floor and ceiling areas and information and this was achieved by shooting rays from the room location point directly up and down to gather that information. We found the results in the approach to be a bit unreliable and not very accurate at times depending on how the walls etc. were modelled.

That led us to investigate the “SpatialElementGeometryCalculator” class, which after some trial and error does exactly what we were after. I’m happy to provide you with some small sample code of this working for your users.

Answer: It seems to me the one of the door areas seems to be counting something double.

Yes, of course the spatial element geometry calculator is much more suited to the need you describe.

Response: Here is a very simple stripped down version of the above calculating the wall surface area, minus cutting family instances.

The advantages of this method over any other method that I have seen so far is that if your ceilings are room bounding and they are vaulted the wall areas are calculated correctly.

The solution is a VB.NET solution that could easily be converted to C# if needed (copy to an editor to see the truncated lines in full, or look at the module
extCmd.vb on GitHub:

Imports Autodesk.Revit.ApplicationServices
Imports System.IO
Imports Autodesk.Revit.DB.Architecture
Imports System.Collections.Generic
Imports System.Diagnostics
Imports Autodesk.Revit.Attributes
Imports Autodesk.Revit.DB
Imports BoundarySegment = Autodesk.Revit.DB.BoundarySegment
Imports Autodesk.Revit.DB.ExternalService
Imports Autodesk.Revit.UI

<Autodesk.Revit.Attributes.Transaction(Autodesk.Revit.Attributes.TransactionMode.Manual)> _
<Autodesk.Revit.Attributes.Regeneration(Autodesk.Revit.Attributes.RegenerationOption.Manual)> _
<Autodesk.Revit.Attributes.Journaling(Autodesk.Revit.Attributes.JournalingMode.NoCommandData)> _
Public Class extCmd
    Implements IExternalCommand

    Public Function Execute(commandData As ExternalCommandData, ByRef message As String, elements As ElementSet) As Result Implements IExternalCommand.Execute
        Try

            Dim app As Autodesk.Revit.UI.UIApplication = commandData.Application
            Dim doc As Document = app.ActiveUIDocument.Document

            Dim roomCol As FilteredElementCollector = New FilteredElementCollector(app.ActiveUIDocument.Document).OfClass(GetType(SpatialElement))
            Dim s As String = "Finished populating Rooms with Boundary Data" + vbNewLine + vbNewLine
            For Each e As SpatialElement In roomCol
                Dim room As Room = TryCast(e, Room)
                If room IsNot Nothing Then
                    Try
                        Dim spatialElementBoundaryOptions As New SpatialElementBoundaryOptions()
                        spatialElementBoundaryOptions.SpatialElementBoundaryLocation = SpatialElementBoundaryLocation.Finish
                        Dim calculator1 As New Autodesk.Revit.DB.SpatialElementGeometryCalculator(doc, spatialElementBoundaryOptions)
                        Dim results As SpatialElementGeometryResults = calculator1.CalculateSpatialElementGeometry(room)

                        ' get the solid representing the room's geometry
                        Dim roomSolid As Solid = results.GetGeometry()
                        Dim iSegment As Integer = 1
                        For Each face As Face In roomSolid.Faces
                            Dim subfaceList As IList(Of SpatialElementBoundarySubface) = results.GetBoundaryFaceInfo(face)
                            For Each subface As SpatialElementBoundarySubface In subfaceList
                                If subface.SubfaceType = SubfaceType.Side Then ' only interested in walls in this example
                                    Dim ele As Element = doc.GetElement(subface.SpatialBoundaryElement.HostElementId)
                                    Dim subfaceArea As Double = subface.GetSubface().Area
                                    Dim netArea As Double = sqFootToSquareM(calwallAreaMinusOpenings(subfaceArea, ele, doc, room))
                                    s += "Room " + room.Parameter(BuiltInParameter.ROOM_NUMBER).AsString + " : Wall " + ele.Parameter(BuiltInParameter.DOOR_NUMBER).AsString + " : Area " + netArea.ToString + "m2" + vbNewLine
                                End If

                            Next
                        Next
                        s += vbNewLine
                    Catch ex As Exception
                    End Try

                End If

            Next
            MsgBox(s, MsgBoxStyle.Information, "Room Boundaries")
            Return Result.Succeeded
        Catch ex As Exception
            MsgBox(ex.Message.ToString + vbNewLine + _
                   ex.StackTrace.ToString)
            Return Result.Failed
        End Try
    End Function


    Private Function calwallAreaMinusOpenings(ByVal subfaceArea As Double, ByVal ele As Element, ByVal doc As Document, ByVal room As Room) As Double
        Dim fiCol As FilteredElementCollector = New FilteredElementCollector(doc).OfClass(GetType(FamilyInstance))
        Dim lstTotempDel As New List(Of ElementId)
        'Now find the familyInstances that are associated to the current room
        For Each fi As FamilyInstance In fiCol
            If fi.Parameter(BuiltInParameter.HOST_ID_PARAM).AsValueString = ele.Id.ToString Then
                If fi.Room IsNot Nothing Then
                    If fi.Room.Id = room.Id Then
                        lstTotempDel.Add(fi.Id)
                        Continue For
                    End If
                End If
                If fi.FromRoom IsNot Nothing Then
                    If fi.FromRoom.Id = room.Id Then
                        lstTotempDel.Add(fi.Id)
                        Continue For
                    End If
                End If
                If fi.ToRoom IsNot Nothing Then
                    If fi.ToRoom.Id = room.Id Then
                        lstTotempDel.Add(fi.Id)
                        Continue For
                    End If
                End If
            End If
        Next

        If lstTotempDel.Count > 0 Then
            Dim t As New Transaction(doc, "tmp Delete")
            Dim wallnetArea As Double = ele.Parameter(BuiltInParameter.HOST_AREA_COMPUTED).AsDouble
            t.Start()
            doc.Delete(lstTotempDel)
            doc.Regenerate()
            Dim wallGrossArea As Double = ele.Parameter(BuiltInParameter.HOST_AREA_COMPUTED).AsDouble
            t.RollBack()
            Dim fiArea As Double = wallGrossArea - wallnetArea
            Return subfaceArea - fiArea
        Else
            Return subfaceArea
        End If
    End Function


    Private Function sqFootToSquareM(ByVal sqFoot As Double) As Double
        Return Math.Round(sqFoot * 0.092903, 2)
    End Function

End Class

Answer:
Thank you very much for your sample code.

I created a
SpatialElementGeometryCalculator GitHub repository for
it and placed both the original VB.NET implementation and my migration efforts to C# in it.

You can look at the
list of commits to
see the steps that I took:

  • Commits on Mar 17, 2015
    • added comments – an hour ago
    • added comments – an hour ago
    • reformatted long lines and compare element id integer instead of string – an hour ago
    • added comments – 2 hours ago
    • cleaned up foreach statement and compiled successfully – 2 hours ago
  • Commits on Mar 16, 2015
    • C# code generated by disassembling VB.NET assembly using Reflector – 22 hours ago
    • original VB.NET implementation – 23 hours ago
    • added link to discussion forum thread – 23 hours ago
    • added authors and tbc link – 23 hours ago
    • added authors and tbc link – 23 hours ago
    • Initial commit – 23 hours ago

I started off by compiling, installing and testing the VB.NET solution, then disassembling the VB.NET add-in assembly DLL using
Reflector to
generate some raw C# code as a starting point.

The result was not ready to compile, so some additional steps were required, both just to compile it and for further clean-up.

One little enhancement that I added was to use an integer comparison instead of string comparison to identify the family instances hosted by the given wall.
Using AsElementId().IntegerValue.Equals( Id.IntegerValue ) is more efficient than comparing AsValueString and ele.Id.ToString, since the string conversion is eliminated.
Integer comparison is faster, because you are just comparing one single binary register instead of a whole sequence of bytes.

Another significant potential enhancement in calwallAreaMinusOpenings would be to use a parameter filter for the HOST_ID_PARAM parameter value instead of the .NET element id comparison to narrow down the family instances to those hosted by the wall before returning the filtered element collector result from Revit back to .NET.

That would vastly speed up the process for large models, because in the current implementation, all family instances are retrieved and marshalled across from native Revit to the .NET add-in memory space before the parameter value is checked in .NET. Using a parameter filter speeds things up significantly, at least by 50 percent. In a large model, where the great majority of the family instances will not be hosted by the wall of interest, this could cause a huge performance improvement.

The end result looks like this in C#:

[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="room"></param>
    /// <returns></returns>
    double calwallAreaMinusOpenings(
        double subfaceArea,
        Element wall,
        Room room )
    {
        Document doc = wall.Document;

        Debug.Assert(
            room.Document.ProjectInformation.UniqueId.Equals(
                doc.ProjectInformation.UniqueId ),
            "expected wall and room from same document" );

        // Determine all openings in the given wall.

        FilteredElementCollector fiCol
            = new FilteredElementCollector( doc )
                .OfClass( typeof( FamilyInstance ) );

        List<ElementId> lstTotempDel
            = new List<ElementId>();

        foreach( FamilyInstance fi in fiCol )
        {
            // The family instances hosted by this wall
            // could be filtered out more efficiently using 
            // a filtered element collector parameter filter.
            // This would be important in a large model.

            if( fi.get_Parameter(
                BuiltInParameter.HOST_ID_PARAM )
                    .AsElementId().IntegerValue.Equals(
                        wall.Id.IntegerValue ) )
            {
                if( ( fi.Room != null )
                    && ( fi.Room.Id == room.Id ) )
                {
                    lstTotempDel.Add( fi.Id );
                }
                else if( ( fi.FromRoom != null )
                    && ( fi.FromRoom.Id == room.Id ) )
                {
                    lstTotempDel.Add( fi.Id );
                }
                else if( ( fi.ToRoom != null )
                    && ( fi.ToRoom.Id == room.Id ) )
                {
                    lstTotempDel.Add( fi.Id );
                }
            }
        }

        // Determine total area of all openings.

        double openingArea = 0;

        if( 0 < lstTotempDel.Count )
        {
            Transaction t = new Transaction( doc );

            double wallAreaNet = wall.get_Parameter(
                BuiltInParameter.HOST_AREA_COMPUTED )
                    .AsDouble();

            t.Start( "tmp Delete" );
            doc.Delete( lstTotempDel );
            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 Data\r\n\r\n";

            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()
                                        + " m2\r\n";
                                }
                            }
                        }
                        s = s + "\r\n";
                    }
                    catch( Exception )
                    {
                    }
                }
            }
            TaskDialog.Show( "Room Boundaries", s);

            rc = Result.Succeeded;
        }
        catch( Exception ex )
        {
            TaskDialog.Show( "Room Boundaries",
                ex.Message.ToString() + "\r\n"
                + ex.StackTrace.ToString() );

            rc = Result.Failed;
        }
        return rc;
    }
}

Little house 3D view

Little house plan view

Little house wall area results

As you can see, the door and the two small windows consume a bit less than 2.5 square meters of wall area.

The complete source code, Visual Solution files and add-in manifests for both the VB.NET and C# implementations are hosted by the
SpatialElementGeometryCalculator GitHub repository,
and the version presented here is
release 2015.0.0.0.

Many thanks to Phillip for exploring and developing this powerful solution!

Wall Area and Orientation

Another recent query that might also make use of this discussion was raised by Arif Hanif in a
comment on
room and wall adjacency:

Question: Love the website, I have recently started to learn C# and have started exploring the API to help with MEP Design. It has been frustrating dealing with gbXML. I am trying to get Wall Area and Orientation for Room to Excel. Can you point me to some of your past postings that could help me.

Answer: There are a lot of discussions on getting the wall area, e.g. for mass quantities, the
Revit SDK MaterialQuantities sample mentioned
above and the discussion of
2D polygon areas and outer loop.

For the orientation, here are some starting points:

Those initial ponderings led to these further discussions:

We put in some additional work on converting Revit project coordinates to real-world ones, including the orientation transformation, when working on the setout point application:

I hope this helps.

Enjoy.


Comments

8 responses to “Calculating Gross and Net Wall Areas”

  1. Arif Hanif Avatar
    Arif Hanif

    Thanks Jeremy This is really helpful, I was able to piece a lot of the information together to get started.
    I am having an issue with obtaining opening X,Y,Z coordinates.
    foreach (MyOpening opening in surface.openings)
    {
    s += ” Opening:\n Area: ” + opening.area + “\n Uvalue: ” + opening.type.Uvalue + “\n”;
    s += ” Opening Points:\n”;
    foreach (XYZ element in opening.edges)
    {
    s += ” Normal:\n”;
    s += ” ” + string.Format(“({0},{1},{2})”, opening.normal.X, opening.normal.Y, opening.normal.Z) + “\n”;
    }
    }

  2. Dear Arif,
    Thank you for your appreciation.
    I am glad you are making progress.
    Your code does not make much sense to me, though.
    Are you sure it even compiles?
    The setout point application basically shows you all there is to know about obtaining XYZ coordinates from Revit element geometry.
    You should possibly do some more reading and experimenting before asking any further questions :-)
    Cheers, Jeremy.

  3. Hi Jeremy,
    Why not using “(wall as HostObject).FindInserts(…)” to determine the instances nested into the given wall ?
    Maybe the method above give better performances ?

  4. Dear Vilo,
    That is a very valid question indeed.
    The simple answer is: I was unaware of it.
    In olden times, methods similar to that shown above were the only way to retrieve this information. Hence, for instance, this relationship inverter:
    http://thebuildingcoder.typepad.com/blog/2008/10/relationship-in.html
    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.
    Cheers, Jeremy.

  5. Dear Vilo,
    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 inserts = ( wall as HostObject )
    .FindInserts( true, true, true, true );
    Debug.Assert(
    lstTotempDel.Count.Equals( inserts.Count ),
    “expected FindInserts to return the same openings” );
    The GitHub repository code is updated now.
    Thank you!
    Cheers, Jeremy.

  6. Dear Vilo,
    Thank you again for this important suggestion.
    I passed it on to Phillip as well, and he confirms that it is even more useful in other way as well, e.g. for compound walls.
    I published the new findings and updated C# solution as a new post:
    http://thebuildingcoder.typepad.com/blog/2015/03/findinserts-retrieves-all-openings-in-all-wall-types.html
    Thank you!
    Cheers, Jeremy.

  7. Hi Jeremy,
    This is pretty much just a gripe, but bear with me if you don’t mind. From the diff you posted above:
    – foreach( View v in sheet.Views )
    + foreach( ElementId id in viewIds )
    {
    + View v = doc.GetElement( id ) as View;
    +
    What? That’s hardly an improvement! Now client code is even more verbose.
    Also the old paradigm, if it had been updated to return IEnumerable<View>, would be more amenable to use with Linq queries for post filtering.
    I’m not a fan of this one either:
    – pipe = doc.Create.NewPipe( q0, q1,
    – pipe_type_standard );
    + //pipe = doc.Create.NewPipe( q0, q1, pipe_type_standard ); // 2014
    +
    + pipe = Pipe.Create( doc, systemTypeId,
    + pipe_type_standard.Id, levelId, q0, q1 ); // 2015
    }
    For the pipe, I now need to go find a systemTypeId from somewhere, but what if I don’t want to assign this at creation time?
    Not sure it’s doing any good to post this, but I like to vent occasionally.

  8. Dear Dave,
    Thank you for your comments, which I will reinterpret as constructive questions that raise interesting issues which help illustrate some API design principles and future trends important and worthwhile understanding.
    I asked Arnošt Löbel for an in-depth explanation, and he replies:
    Boy, is that a loaded question of what? Where do I start? I am afraid I cannot give a satisfactory answer and be brief at the same time. I’ll try to answer in stages for that very reason.
    A) Very brief explanation
    We tend to use functions which take or return element Ids rather then element objects because:

    It’s faster
    It’s safer
    It’s consistent and ties directly to what Revit does internally

    B) Detailed and boring explanation
    When we originally exposed the API (2005?), there was no direct link between the API and internal Revit code. That had turned out to be a maintenance nightmare. As the API has developed from tiny to large and huge we have realised we simply cannot maintain two parallel hierarchies. That’s why we have developed a framework which allows us to generate the public API directly from the internal native code. Now, basically, what we Revit programmers see, the API programmer gets. Sometime we add certain public method for the API programmer’s convenience, but the majority of the API is pretty much a mirror of Revit internal classes and interfaces.
    This applies both to the case of methods using Ids as well as the originally used element creating factories. For the latter, it’s very straightforward: We simply do not have such factories internally and we would not like having them. We believe that element creation is easier when it lives with the element class itself. It is also discoverable better that way.
    As for the element Ids – it has been part of Revit coding standard pretty much since ever. I mentioned that the main reasons are safety and speed. We often can do with just element Ids because that allows us to use elements without loading them to memory. For example, if I am looking for certain kind of a view, I can quickly get a list of their Ids, test what they are without expanding them (this is a fast test), and then only get the view I need. That mean I would be expanding one potential element only instead of, say, some hundred of view elements.
    The safety aspect has to do with the fact that element objects can become inaccessible when deleted (naturally) or undone or redone. Such operations are not always obvious or visible to the internal programmer who holds a pointer to an element, which leads to very undesirable problem of dangling, invalid pointers. If the programmer uses the element’s Id instead, however, then operations on the element are safe. All the programmer needs to do is to test if the element obtained for the given Id is NULL or not. Yes, it is somehow less convenient, but it is incomparably safer with almost no performance penalty. We have made internal element lookup so fast, that it is practically instant.
    C) The case of Pipe creation
    On top of the above explanation, there is more about this API that perhaps deserves further explanation. I would need to talk to the MEP team to be certain, but I have seen the pattern in other APIs, thus I assume my guess will be very close to what the MEP folks would tell me. The changes the user see in 2015 are due to our continuing effort to separate Revit UI layer from the underlying model, to which we typically refer to as Revit DB. Without going into great details, the separation between those two should be such that the DB does not know about the UI, while the UI can know about the DB. Makes sense? So, even though it is not apparent when looking at the original pre-2015 pipe creation, the method (which is obviously a DB method) used to reach out to the UI layer to fetch the current type a pipe would be created with if the creation started via the UI. That, for us, is no longer acceptable and it is not how Revit works internally. The original pipe-creation method did what it did because it was written manually and did nor reflect Revit internals. With our effort of tying the API tightly with Revit internals, such approach is no longer possible. The API programmer now needs to do what Revit programmers do too, which is:

    If there is UI, find out via the UI interface what the current type is and use that.
    If UI is not accessible (for whatever reason), the programmer simply must know what type is to be used.

    I hope my answers are satisfactory. I realise they are not the briefest, but I tried my best.
    Many thanks to Arnošt for his detailed explanation!
    I hope this helps.
    Cheers, Jeremy.

Leave a Reply to Arif HanifCancel reply

Discover more from Autodesk Developer Blog

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

Continue reading