Auto-dimensioning rooms with Dynamo and Python

In this post (series I hope) I want to share some thoughts on possibilities of adding dimensions to rooms in Revit automatically. It is not a trivial task in my opinion, especially that there is no single good solution! Today I want to present a script that, in most cases, is going to dimension rooms along their cardinal directions.

So, in the example below it would mean measuring each room twice – between blue segments and between red segments:

I realize that above dimensions would need some tidying, but it is out of scope of this post:)

As the script is rather lengthy I tried to make it modular and split it into separate code blocks.

So let’s start with the first and the simplest one. It’s role is to gather all the room’s boundary segments. I think it is pretty self explanatory:

import clr

clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *

#The inputs to this node will be stored as a list in the IN variables.
rooms = UnwrapElement(IN[0])

output = []

options = SpatialElementBoundaryOptions()
options.SpatialElementBoundaryLocation = SpatialElementBoundaryLocation.Finish

for r in rooms:
	segments = []
	loops = r.GetBoundarySegments(options)
	for loop in loops:
		for segment in loop:
			segments.append(segment)
	output.append(segments)

#Assign your output to the OUT variable.
OUT = output

The second node is bit more complex. It has two main loops. The first one is grouping all the segments by their direction. So in most cases it splits them into horizontal and vertical lists:

#group parallel segments together
for rs in room_segments:
	directions = []
	segment_groups = []
	for segment in rs:
		l = segment.GetCurve()
		d = l.GetEndPoint(1)-l.GetEndPoint(0)
		idx = -1
		for i in range(len(directions)):
			if isParallel(d,directions[i]):
				idx = i
				break
		
		if idx!=-1:
			segment_groups[idx].append(segment)
		else:
			directions.append(d)
			new_group = []
			new_group.append(segment)
			segment_groups.append(new_group)
			
	sets.append(segment_groups)

Second loop is taking those direction-sorted lists and splits them further. It creates sublists of collinear segments for each direction. In Revit it can often happen that one room boundary line consist of a number of smaller segments – for instance when a wall is split or when it has a column inside. In our example above you can notice that the right room’s top boundary actually consists of two separate walls – that’s exactly the case. So, as a result of the first node, we will get two boundary segments for this edge of the room – one per each wall – instead of a longer common segment. We have to group them.

#split groups into collinear sets
for rs in sets:
	room_output = []
	for set in rs:
		csets = []
		for s in set:
			for cs in csets:
				if len(cs)>0:
					l0 = s.GetCurve()
					l1=cs[0].GetCurve()
					if isCollinear(l0,l1):
						cs.append(s)
						break
			else:
				new_set = [s]
				csets.append(new_set)
		room_output.append(csets)
	
	output.append(room_output)		

This node contains also some helper functions – mostly vector math. To keep the post shorter I am not going to explain them, but here is the entire second node content:

import clr

clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *

def isParallel(v1,v2):
	return v1.CrossProduct(v2).IsAlmostEqualTo(XYZ(0,0,0))  
  
def isCollinear(l0,l1):
	a = l0.GetEndPoint(0)
	b = l0.GetEndPoint(1)
	c = l1.GetEndPoint(0)
	d = l1.GetEndPoint(1)
	return (b-a).CrossProduct(c-a).IsAlmostEqualTo((b-a).CrossProduct(d-a)) and (b-a).CrossProduct(c-a).IsAlmostEqualTo(XYZ(0,0,0))

#The inputs to this node will be stored as a list in the IN variables.
room_segments = UnwrapElement(IN[0])

sets = []

#group parallel segments together
for rs in room_segments:
	directions = []
	segment_groups = []
	for segment in rs:
		l = segment.GetCurve()
		d = l.GetEndPoint(1)-l.GetEndPoint(0)
		idx = -1
		for i in range(len(directions)):
			if isParallel(d,directions[i]):
				idx = i
				break
		
		if idx!=-1:
			segment_groups[idx].append(segment)
		else:
			directions.append(d)
			new_group = []
			new_group.append(segment)
			segment_groups.append(new_group)
			
	sets.append(segment_groups)
	
	
output = []

#split groups into collinear sets
for rs in sets:
	room_output = []
	for set in rs:
		csets = []
		for s in set:
			for cs in csets:
				if len(cs)>0:
					l0 = s.GetCurve()
					l1=cs[0].GetCurve()
					if isCollinear(l0,l1):
						cs.append(s)
						break
			else:
				new_set = [s]
				csets.append(new_set)
		room_output.append(csets)
	
	output.append(room_output)				

#Assign your output to the OUT variable.
OUT = output

Now for the last node – it contains one main loop that creates dimensions for each direction found in each room. What we have to do here is to decide which exact segments we want to dimension. The simplest answer, as shown on the image above, is to span a dimension line between furthest boundaries in each direction. This way we are going to measure maximum length and width of the room.

So the code below first sorts segments in each set by their length. This is not necessary, but I assume that the longest segment will be the main one to work with. Next, we are creating a list of segement-set pairs. Each pair has also a distance between it’s sets measured. This way we can determine two segment-sets that are furthest apart.

Once we sort the list, based on distance between boundaries, we pick the first pair and create references to it’s sets longest segments (first in the list). We also calculate a perpendicular line running through the midpoint of one of the segments. This will be our dimension line’s position.

for rs in room_sets:
	for dir in rs:
		for set in dir:
			set=sorted(set, key=lambda x: x.GetCurve().Length, reverse = True)

		set_pairs = []
		for s0 in dir:
			for s1 in dir:
				if s0!=s1:
					c = s0[0].GetCurve()
					c.MakeUnbound()
					d = c.Distance(s1[0].GetCurve().GetEndPoint(0))
					set_pairs.append([d,s0[0],s1[0]])
		sorted_by_distance = sorted(set_pairs, key = lambda x:x[0], reverse=True)
		first = sorted_by_distance[0][1]
		second = sorted_by_distance[0][2]
		nl = normal_line(first)
		refArray = ReferenceArray()
		refArray.Append(segment_reference(first))
		refArray.Append(segment_reference(second))
		d = doc.Create.NewDimension(view, nl, refArray)
		dims.append(d)

This node also contains some additional helper functions. One of them is quite essential as it converts picked segments into references that can be used for creating of a dimension. At the moment it supports only two kinds of bounding elements – walls and room separators. It is quite simple to process room separators as they are single curves with one reference. With walls it is a bit more complicated. First we have to find both side faces of the wall and determine which one of them overlaps with the segment. Only then we know which reference has to be returned by the function.

def segment_reference(s):

	se = doc.GetElement(s.ElementId)
	#if model line (room separator)
	if isinstance(se, Autodesk.Revit.DB.ModelLine):
		return se.GeometryCurve.Reference
	#if wall
	if isinstance(se,Autodesk.Revit.DB.Wall):
		rExt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Exterior)[0]
		rInt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Interior)[0]
		fExt = doc.GetElement(rExt).GetGeometryObjectFromReference(rExt)
		fInt = doc.GetElement(rInt).GetGeometryObjectFromReference(rInt)
		
		if fExt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fExt.Intersect(s.GetCurve())==SetComparisonResult.Subset:
			return rExt
		if fInt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fInt.Intersect(s.GetCurve())==SetComparisonResult.Subset:
			return rInt
					
	return None

The entire code of the last python node looks like so:

import clr

clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *

import Autodesk

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Transactions import TransactionManager
from RevitServices.Persistence import DocumentManager

def normal_line(s):
	l = s.GetCurve()
	d = l.Direction
	n = XYZ(-d.Y,d.X,0)
	m = l.GetEndPoint(0) + (l.GetEndPoint(1)-l.GetEndPoint(0))/2
	nl = Line.CreateBound(m, m+n)
	return nl

	
def segment_reference(s):

	se = doc.GetElement(s.ElementId)
	#if model line (room separator)
	if isinstance(se, Autodesk.Revit.DB.ModelLine):
		return se.GeometryCurve.Reference
	#if wall
	if isinstance(se,Autodesk.Revit.DB.Wall):
		rExt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Exterior)[0]
		rInt = HostObjectUtils.GetSideFaces(se,ShellLayerType.Interior)[0]
		fExt = doc.GetElement(rExt).GetGeometryObjectFromReference(rExt)
		fInt = doc.GetElement(rInt).GetGeometryObjectFromReference(rInt)
		
		if fExt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fExt.Intersect(s.GetCurve())==SetComparisonResult.Subset:
			return rExt
		if fInt.Intersect(s.GetCurve())==SetComparisonResult.Overlap or fInt.Intersect(s.GetCurve())==SetComparisonResult.Subset:
			return rInt
		
	return None
	
doc = DocumentManager.Instance.CurrentDBDocument
#The inputs to this node will be stored as a list in the IN variables.
room_sets = UnwrapElement(IN[0])
view = UnwrapElement(IN[1])

dims = []

TransactionManager.Instance.EnsureInTransaction(doc)

for rs in room_sets:
	for dir in rs:
		for set in dir:
			set=sorted(set, key=lambda x: x.GetCurve().Length, reverse = True)

		set_pairs = []
		for s0 in dir:
			for s1 in dir:
				if s0!=s1:
					c = s0[0].GetCurve()
					c.MakeUnbound()
					d = c.Distance(s1[0].GetCurve().GetEndPoint(0))
					set_pairs.append([d,s0[0],s1[0]])
		sorted_by_distance = sorted(set_pairs, key = lambda x:x[0], reverse=True)
		first = sorted_by_distance[0][1]
		second = sorted_by_distance[0][2]
		nl = normal_line(first)
		refArray = ReferenceArray()
		refArray.Append(segment_reference(first))
		refArray.Append(segment_reference(second))
		d = doc.Create.NewDimension(view, nl, refArray)
		dims.append(d)
		
TransactionManager.Instance.TransactionTaskDone()

#Assign your output to the OUT variable.
OUT = dims

The above script is already quite complex codewise, but still limited in performance. I plan to write further posts expanding it’s functionality. For instance the right room might use some more dimensions measuring the niche. Also some code should be provided to process family instances – such as columns – that can also be parts of room boundaries.

Below you can download a complete Dynamo 2.0.3 definition with the above script.

Posts created 32

Leave a Reply

Your email address will not be published.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top