Today, we revisit the interesting and generic question on retrieving all exterior walls.
That may be easy in a perfect and complete model.
However, it raises some challenges in an incomplete BIM:
This time around, this question was raised by Feng @718066900 Wang in the Revit API discussion forum thread on how to get all the outermost walls in the model.
We already explored some aspects last week, on retrieving all exterior walls.
Today, we can present a working solution for an incomplete BIM.
Question: How do I get all the outermost walls in the model?
Here is a picture showing what I mean:
Here is the sample model exterior_walls.rvt
.
Several approaches to solve this were already brought up last week:
DirectionCalculation
Revit SDK sample and The Building Coder discussion of it
on south facing walls solves
using the built-in wall function parameter FUNCTION_PARAM
to filter for exterior walls.
However, the function parameter is not always correctly set on the wall type, and the wall type is not always correctly assigned.BuildingEnvelopeAnalyzer
class that will retrieve all exterior walls for you.
However, it relies on the building model being properly enclosed, i.e., roof and floor elements must be added to form properly enclosed spaces in order for the analyser to work.As we discovered, the first two approaches above cannot be applied to the incomplete BIM at hand.
For instance, here is the erroneous result of applying the BuildingEnvelopeAnalyzer
to it:
There is no closure, no ceiling or floor, because I want to determine the outermost walls first to automatically create the ceiling and floor.
Getting the outermost walls first will enable more automation.
If the building is 'open' upward and downward, in theory, all walls are exposed to the outside, and therefore all of them are 'exterior'.
Happily, the third approach above can still be used in this 2D situation.
It would also be possible to solve this task through a geometric algorithm, of course.
Many different approaches can be taken here as well.
I would love to discover a really reliable one that works under all circumstances.
Here is an idea that comes to mind right now on the fly:
You can easily determine whether a given point lies within a given polygon.
I also implemented a point containment algorithm for the Revit API, and a room in area predicate using it.
Now, if you have all your walls, their location curves (if they are non-linear, things get trickier), and endpoints, and are sure that they all form closed polygons, you could determine the maximal polygon enclosing all others by choosing the one that contains the maximum number of wall endpoints.
You might also be able to use some library providing 2D polygon or Boolean operations for this.
Some such libraries, other options and helpful ideas are discussed in The Building Coder topic group on 2D Booleans and adjacent areas.
We also recently discussed determining the outermost loop of a face.
However, in this case, making use of the built-in Revit room generation functionality is probably the easiest way to go.
I tried it out manually in the sample model, and it seems to work perfectly!
Add room separation lines around the outside of the building:
Create a room around the building using them:
This can easily be achieved programmatically as well.
Now all you need to do is retrieve the room boundary, eliminate the exterior separation line boundary segments, delete the separation lines and room, and you are done.
Feng Wang implemented a solution based on these suggestions in CmdGetOutermosWallByCreateRoom.zip.
Here are my initial comments on his code that I keep repeating again and again, and therefore here now yet again:
using
statementTo add a room around the entire building, we need to determine the building extents, or, at least, the maximal extents of all exterior walls.
One approach to achieve that might be to query each wall for its geometry or location curve, extract all their vertices, and construct a bounding box from them.
However, querying a Revit element for its bounding box is much faster and more efficient than accessing and analysing its geometry or location curve.
Moreover, The Building Coder samples
Util
class already
implements a bounding box extension method ExpandToContain
that we can use here, which expands a given bounding box to encompass another one:
public static class JtBoundingBoxXyzExtensionMethods { /// <summary> /// Expand the given bounding box to include /// and contain the given point. /// </summary> public static void ExpandToContain( this BoundingBoxXYZ bb, XYZ p ) { bb.Min = new XYZ( Math.Min( bb.Min.X, p.X ), Math.Min( bb.Min.Y, p.Y ), Math.Min( bb.Min.Z, p.Z ) ); bb.Max = new XYZ( Math.Max( bb.Max.X, p.X ), Math.Max( bb.Max.Y, p.Y ), Math.Max( bb.Max.Z, p.Z ) ); } /// <summary> /// Expand the given bounding box to include /// and contain the given other one. /// </summary> public static void ExpandToContain( this BoundingBoxXYZ bb, BoundingBoxXYZ other ) { bb.ExpandToContain( other.Min ); bb.ExpandToContain( other.Max ); } }
With that functionality, we can easily retrieve the maximum extents of all the walls:
/// <summary> /// Return a bounding box around all the /// walls in the entire model; for just a /// building, or several buildings, this is /// obviously equal to the model extents. /// </summary> static BoundingBoxXYZ GetBoundingBoxAroundAllWalls( Document doc, View view = null ) { // Default constructor creates cube from -100 to 100; // maybe too big, but who cares? BoundingBoxXYZ bb = new BoundingBoxXYZ(); FilteredElementCollector walls = new FilteredElementCollector( doc ) .OfClass( typeof( Wall ) ); foreach( Wall wall in walls ) { bb.ExpandToContain( wall.get_BoundingBox( view ) ); } return bb; }
Now we are ready to apply the temporary transaction trick, create the room, query it for its boundary and retrieve the exterior walls.
This is implemented by the following methods:
RetrieveWallsGeneratingRoomBoundaries
retrieves all walls that generate boundary segments for the given room using the BoundarySegment
ElementId
property.GetOutermostWalls
determines the maximum model extents, temporarily generates room boundary lines and a new room outside them, and retrieves the room boundary walls.Execute
method determines the exterior walls and highlights them for the user by adding them to the current selection set./// <summary> /// 过滤出需要的墙体 -- /// Return all walls that are generating boundary /// segments for the given room. Includes debug /// code to compare wall lengths and wall areas. /// </summary> static List<ElementId> RetrieveWallsGeneratingRoomBoundaries( Document doc, Room room ) { List<ElementId> ids = new List<ElementId>(); IList<IList<BoundarySegment>> boundaries = room.GetBoundarySegments( new SpatialElementBoundaryOptions() ); int n = boundaries.Count; int iBoundary = 0, iSegment; foreach( IList<BoundarySegment> b in boundaries ) { ++iBoundary; iSegment = 0; foreach( BoundarySegment s in b ) { ++iSegment; // Retrieve the id of the element that // produces this boundary segment Element neighbour = doc.GetElement( s.ElementId ); Curve curve = s.GetCurve(); double length = curve.Length; if( neighbour is Wall ) { Wall wall = neighbour as Wall; Parameter p = wall.get_Parameter( BuiltInParameter.HOST_AREA_COMPUTED ); double area = p.AsDouble(); LocationCurve lc = wall.Location as LocationCurve; double wallLength = lc.Curve.Length; ids.Add( wall.Id ); } } } return ids; } /// <summary> /// 获取当前模型指定视图内的所有最外层的墙体 /// Get all the outermost walls in the /// specified view of the current model /// </summary> /// <param name="doc"></param> /// <param name="view">视图,默认是当前激活的视图 /// View, default is currently active view</param> public static List<ElementId> GetOutermostWalls( Document doc, View view = null ) { double offset = Util.MmToFoot( 1000 ); if( view == null ) { view = doc.ActiveView; } BoundingBoxXYZ bb = GetBoundingBoxAroundAllWalls( doc, view ); XYZ voffset = offset * ( XYZ.BasisX + XYZ.BasisY ); bb.Min -= voffset; bb.Max += voffset; XYZ[] bottom_corners = Util.GetBottomCorners( bb, 0 ); CurveArray curves = new CurveArray(); for( int i = 0; i < 4; ++i ) { int j = i < 3 ? i + 1 : 0; curves.Append( Line.CreateBound( bottom_corners[i], bottom_corners[j] ) ); } using( TransactionGroup group = new TransactionGroup( doc ) ) { Room newRoom = null; group.Start( "Find Outermost Walls" ); using( Transaction transaction = new Transaction( doc ) ) { transaction.Start( "Create New Room Boundary Lines" ); SketchPlane sketchPlane = SketchPlane.Create( doc, view.GenLevel.Id ); ModelCurveArray modelCaRoomBoundaryLines = doc.Create.NewRoomBoundaryLines( sketchPlane, curves, view ); // 创建房间的坐标点 -- Create room coordinates double d = Util.MmToFoot( 600 ); UV point = new UV( bb.Min.X + d, bb.Min.Y + d ); // 根据选中点,创建房间 当前视图的楼层 doc.ActiveView.GenLevel // Create room at selected point on the current view level newRoom = doc.Create.NewRoom( view.GenLevel, point ); if( newRoom == null ) { string msg = "创建房间失败。"; TaskDialog.Show( "xx", msg ); transaction.RollBack(); return null; } RoomTag tag = doc.Create.NewRoomTag( new LinkElementId( newRoom.Id ), point, view.Id ); transaction.Commit(); } //获取房间的墙体 -- Get the room walls List<ElementId> ids = RetrieveWallsGeneratingRoomBoundaries( doc, newRoom ); group.RollBack(); // 撤销 return ids; } } public Result Execute( ExternalCommandData commandData, ref string message, ElementSet elements ) { UIApplication uiapp = commandData.Application; UIDocument uidoc = uiapp.ActiveUIDocument; Document doc = uidoc.Document; List<ElementId> ids = GetOutermostWalls( doc ); uidoc.Selection.SetElementIds( ids ); return Result.Succeeded; }
Many thanks to Feng Wang and the development team for helping to sort this out!
Once again, in a completely unrelated area, Frank @Fair59 Aarssen comes to the rescue, providing a succinct answer to the Revit API discussion forum thread on how to filter for elements which satisfy a filter rule:
Question: I'm trying to get the family instances which satisfy a filter rule as shown in this image:
So far, I'm able to get the list of a category that has a specific filter name.
However, I'd like to get the family instances of those categories which satisfy the filter rule.
I'm not sure how to do that via API.
Explanation: Let's say, in Revit, someone needs to find all the walls on the Level 1 or a wall that has some thickness value xyz; they apply a filter rule, and all the walls that satisfy a filter rule get highlighted.
We need this functionality in an add-in, so we could develop a BIM-Explorer for our modellers to explore and navigate any element easily.
The same idea was implemented by Ideate Software in their explorer add-in, cf. the 12-minute demo on Auditing Your Revit Project with Ideate Explorer:
For further understanding, you can check out the Boost Your BIM explanation of Filter Rule data – where is it hiding?
Answer: You can filter the document, first for the categories of the filter, then for each filter rule:
FilteredElementCollector pfes = new FilteredElementCollector( doc ) .OfClass( typeof( ParameterFilterElement ) ); foreach( ParameterFilterElement pfe in pfes ) { #region Get Filter Name, Category and Elements underlying the categories ElementMulticategoryFilter catfilter = new ElementMulticategoryFilter( pfe.GetCategories() ); FilteredElementCollector elemsByFilter = new FilteredElementCollector( doc ) .WhereElementIsNotElementType() .WherePasses( catfilter ); foreach( FilterRule rule in pfe.GetRules() ) { IEnumerable<Element> elemsByFilter2 = elemsByFilter.Where( e => rule.ElementPasses( e ) ); } #endregion
By the way, ParameterFilterElement.GetRules
is obsolete in Revit 2019 and can be replaced by GetElementFilter
in future.
Many thanks to Frank for the solution and to Ali @imaliasad Asad for raising the question and explaining it further to me.