Creating NewRoomBoundaryLines with nested list of curves

I am trying to process data from Excel into Room Boundary Lines, per level, into an existing model.
The spreadsheet has Column A: Level Name, Column B: DesignScript String. I am using regex to convert the strings to model lines. The lines are then grouped by level.

Everything seemed to be going ok until dealing with the CurveArray(). The closest I got was to use a nested for loop into curvearray, but that produces a flat list. Since I want to put many curves on multiple levels, I need to separate them. I’ve tried dict, enumerate, various indents, but no dice.

Looking at the Clockwork node, it should be easy, as it only uses one for loop to create the CurveArray. Since the NewRoomBoundaryLine method requires a singular SketchPlane and View per CurveArray, is there some kind of list wrangling I’m missing?

import sys
import re
import clr
from itertools import groupby

#Import module for Revit 
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
#import module for the Document and transactions
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

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

#get the current document in Revit.
doc = DocumentManager.Instance.CurrentDBDocument

#get levels and level names
allLevels = FilteredElementCollector(doc) \
			.OfCategory(BuiltInCategory.OST_Levels) \
			.WhereElementIsNotElementType() \
			.ToElements()

level_names = [l.Name for l in allLevels]

#Create Plane dictionary
planes = []
for level in allLevels:
	elev=level.Elevation
	point=XYZ(0,0,elev)
	planes.append(Plane.CreateByNormalAndOrigin(XYZ.BasisZ,point))
		
planeDict = {level_names[i]: planes[i] for i in range(len(level_names))}
		
#Create view dictionary
all_views = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).ToElements()

views = []
for v in all_views:
	if not v.IsTemplate:
		if (doc.GetElement(v.GetTypeId())).get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_NAME).AsString() == "Floor Plan":
			views.Add(v)
			
		viewNames = []
		for view in views:
			viewNames.append(view.Name)
			
viewDict = {viewNames[i]: views[i] for i in range(len(viewNames))}

data = IN[0]
lines = []
lvlNames = []
levelsNlines = []
sPlanes = []

#regex code to map coordinates
regx = re.compile(r'((?:-)?\d+\.\d+)').findall

#convert strings to model lines
for lvl, str in data:

	lvlNames.append(lvl)	
	
	if "Line" in str:
		line = Line.CreateBound(XYZ(float(regx(str)[0])/12,float(regx(str)[1])/12,float(regx(str)[2])/12),XYZ(float(regx(str)[3])/12,float(regx(str)[4])/12,float(regx(str)[5])/12))
		lines.append(line)

	elif "Arc" in str:
		arc = Arc.Create(XYZ(float(regx(str)[4])/12,float(regx(str)[5])/12,float(regx(str)[6])/12),float(regx(str)[7])/12,float(regx(str)[8])*0.0174533,float(regx(str)[9])*0.0174533, XYZ(1.0, 0.0, 0.0), XYZ(0.0, 1.0, 0.0))
		lines.append(arc)

	levelsNlines = zip(lvlNames,lines)

gline = []
group = []
gzip  = []
groups = []
uniquekeys = []
glines = []

#group lines per level
for k,g in groupby(levelsNlines, lambda x: x[0]):
	groups.append(list(g))
	uniquekeys.append(k)		
	for group in groups:
		gzip = zip(*group)

	glines.append(gzip[1])

TransactionManager.Instance.EnsureInTransaction(doc)

#Create SketchPlane dictionary
sPlanes = []
	
for plane in planes:
	sPlane = SketchPlane.Create(doc, plane)
	sPlanes.append(sPlane)
	
sPlaneDict = {level_names[i]: sPlanes[i] for i in range(len(level_names))}
docCreation = doc.Create

#create curvearrays per level

curvearray = CurveArray()
for gline in glines:
	for curve in gline:
		curvearray.Append(curve)


for key in uniquekeys:

	rLine = docCreation.NewRoomBoundaryLines(sPlaneDict[key], curvearray, viewDict[key])


TransactionManager.Instance.TransactionTaskDone()

elementList = []
for r in rLine:
	elementList.append(rLine)	
OUT = elementlist

Any insight is appreciated!

Would help to have a few examples of the excel strings you’re working with; otherwise we can only look at the code for an error without any way to reproduce the issue or test solutions.

Hi @jacob.small, Happy New Year!

ExcelTest.xlsx (10.4 KB)
Here you go. It’s around 20 rows per floor, with 5 levels, so you want a model with the same level names to test. It came from the 2000 row sheet I am working with. Thanks for taking a look. The one I did for Area Boundaries was easier and seems to work fine. I wonder why their methods are different? To be clear, the lines show up, but then it hangs. No error msgs,

BTW, hope we can circle back with the FME to Revit connection at some point. I’m sure it’s going to be a busy year for everybody, but it would be a very cool thing! I generated this data using FME with a spatial database as source. It’s one of several categories. Yes, it’s me, Loren from GSA / AU2022 . Good times !

1 Like

I just tried the test file and: The lines came through, no hanging, BUT it generated 3000+ of them.

Assuming you’d expect 58 lines, then this is a loop redundancy issue.

Tomorrow is my first day back in the new year so my morning is certainly going to be crazy, but I’ll try and review then. My gut says that simplifying it or moving to just creating the lines in the Python is best.

Sounds good. I’m thinking it stems from this block:

curvearray = CurveArray()
for gline in glines:
	for curve in gline:
		curvearray.Append(curve)

glines is a nested list of curves grouped by level.
Because you can only create a CurveArray() by Appending individual curves, the second for loop flattens the nested list and we end up getting the entire glines list multiplied. It is then further multiplied when creating the room boundary lines.

I could not figure out how to create a list of CurveArrays like glines. Couldn’t append them. Ideally, it would be a dictionary, same as SketchPlane and View.

So… good news and bad news for you.

The bad news is that I didn’t troubleshoot your code directly or get a fully working code base, for a few reasons.

  1. There is a better way to manage/transport this data (storing it in a JSON as one example).
  2. The levels are drawing at various heights/locations which need to be reviewed in the context of your model.
  3. The dimensions you were showing didn’t make sense to me (inches? some sort of metric units?) and when I tried to rationalize it got me rather confused rather then providing any clarity.

But the good news is that the API calls don’t appear to need direct association to the level or the view; you can call drawing a room separation from any Revit curve using any level, sketch plane, and plan view. From what I can see there is no issue with just pulling the first level, a sketch plane at the origin, and the first plan view to get the job done. However with your curves not necessarily falling on the same plane as the levels you’ve created, rooms may not become bound when placed. This is obviously pretty bad, so it may be best to pull those curves onto the level’s plane rather than assuming they are correctly located. Shouldn’t be too hard to manage if you output the Dynamo curves and adjust them there prior to making the room boundaries.

For now you can give this code a shot and see how it works. I did add some error handling, Dynamo curve creation, and a boolean to toggle execution (or not).

##############################Setup the Pythopn environment##############################
import sys, clr, re
[clr.AddReference(i) for i in ["RevitNodes", "RevitServices", 'RevitAPI']]
import Revit, RevitServices
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
from Autodesk.Revit.DB import *

##############################global variables and inputs##############################
run = IN[0] #input for the run control
if not run: sys.exit("\r\r\t\tSet Run to True\r\r") #a toggle to run the code or not
data = IN[1] #the data from excel
doc = DocumentManager.Instance.CurrentDBDocument 
lvl = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Levels).WhereElementIsNotElementType().FirstElement() #gets the first level in the document
allPlans = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).OfClass(ViewPlan).WhereElementIsNotElementType().ToElements() #gets all the plan views in the document
for p in allPlans: #iterates over the plans
    if not p.IsTemplate: #if the plan view is not a template
        view = p #set the view varible to the plan view
        break #break the loop
rmBndResults = [] #empty list to hold room boundaries
dynamoCrvs = [] #empty list to hold dynamo curves
errors = [] #empty list to hold error indexes
regx = re.compile(r'((?:-)?\d+\.\d+)').findall #regex code to pull the necesssary inputs for building curves from the strings

##############################begin to modify the Revit document##############################
TransactionManager.Instance.EnsureInTransaction(doc)
basePlane = Plane.CreateByNormalAndOrigin(XYZ(0,0,1),XYZ(0,0,0)) #generates a plane at the origin
skPl = SketchPlane.Create(doc,basePlane) #generates a sketch plane from the plane
crvArr = CurveArray() #an empty curve array to the hold the curve loop
i = 0 #index counting for error tracking
for d in data: #starts a loop over the data from excel
    str = d[1] #gets the string from the excel data 
    if "Line" in str: #if the string contains line 
        crv = Line.CreateBound(XYZ(float(regx(str)[0])/12,float(regx(str)[1])/12,float(regx(str)[2])/12),XYZ(float(regx(str)[3])/12,float(regx(str)[4])/12,float(regx(str)[5])/12)) #generates Revit line from the string
        crvArr.Append(crv) #appends the resulting line to the curve arry
        protoCrv = crv.ToProtoType() #converts the curve to a Dynamo curve
        dynamoCrvs.append(protoCrv) #appends the Dyamo curve to the dynamoCrvs list
    elif "Arc" in str: #
        crv = Arc.Create(XYZ(float(regx(str)[4])/12,float(regx(str)[5])/12,float(regx(str)[6])/12),float(regx(str)[7])/12,float(regx(str)[8])*0.0174533,float(regx(str)[9])*0.0174533, XYZ(1.0, 0.0, 0.0), XYZ(0.0, 1.0, 0.0)) #generates Revit arc from the string
        crvArr.Append(crv) #appends the resulting line to the curve arry
        protoCrv = crv.ToProtoType() #converts the curve to a Dynamo curve
        dynamoCrvs.append(protoCrv) #appends the Dyamo curve to the dynamoCrvs list
    else:
        crv = "Data error.\r\tGiven geometry definition is neither an arc nor a line and is not supported.\r" #generates an error message
        dynamoCrvs.append(crv) #appends the error message to the dynamoCrvs list
        errors.append(i) #appends the indext to the list of error indexes
    i+=1 #increments i
rmBnd = doc.Create.NewRoomBoundaryLines(skPl,crvArr,view) #creates the room boundaries in Revit
[rmBndResults.append(i) for i in rmBnd] #appends each curve to the list of results as Dynamo objects
TransactionManager.Instance.TransactionTaskDone() #closes the transaction

##############################Return the results to the Dynamo environment##############################
OUT = rmBndResults, dynamoCrvs, errors #returns the room boundaries, dynamo curves, and error indexes to the Dynamo environment

Hope this helps!

Hi Jacob,

Thanks for getting back to me.

  • Yes, I agree JSON (or XML?) would be a better vehicle for this data. I just went with what I know best. It’s a big reason why I want to connect with FME. Also, these spreadsheets will be used by newbie Dynamo/Revit people, so I didn’t want to confuse them.
  • The Room (and Area) lines are on different levels because it is part of an automated process to generate a specialized type of model that only contains: boundary lines, linked .dwg files per floor, and a few objects to represent antennas and parking. No walls, doors, windows, etc. We will be generating ~1,000 of these models as part of a national spatial data management program. Revit will be the tool that maintains spatial info and interfaces with a Tririga database (it’s a long story). I hope my test data didn’t throw you off.
  • The data coming out of Oracle spatial is in inches Possibly in the future I could switch by converting in my FME Workspace. It was just easier dealing with digital inches. There is a whole other process before it gets to the spreadsheet.
    I’ll give your code a try (yay, error handling!). In an earlier script, I used a single sketchplane / view and as you predicted, it did not allow rooms to be created, which is a deal-breaker. Maybe I can assign levels after the fact by grabbing rmBndResults, sort by elevation, and change the parameter? In RevitLookup, you can see a level associated with each line. This, again, begs the question: Why do Room boundaries need a CurveArray and Area boundaries do not? Legacy code?

At any rate, I always learn something from your posts and appreciate the insight!

Best,

Loren

To create the rooms after the fact, look into the PlanTopology class. If the curve loops were created at the right elevation for the level(s) as they exist in the model it will work, though it may need a separate transaction.

If there is an issue with the Z value due to rounding/other info the. You’ll face errors there, no matter which level/view you use to create the room boundaries.

The plot thickens…

Your code generated the lines no problem, but they could not be used for rooms, as previously stated. Also, you cannot change their LevelId, even though it is right there in RevitLookup. That parameter isn’t available, apparently. You said as much here.

But yet, I am able to create room lines using a DynamoMEP node:

Notice that I am using straight-up, 1:1, flattened lists of Dynamo curves and respective views.

The C# code is here.

How simonmoreau did it, I am still trying to figure out, but one of the comments is interesting:

// PB: This implementation borrows the somewhat risky notions from the original Dynamo
// implementation. In short, it has the ability to infer a sketch plane,
// which might also mean deleting the original one.

Risky, huh? Bring it!

RoomLinesTest.dyn (3 MB)

I’ll try and have a look tomorrow. :slight_smile:

Success! I changed my approach and went for the 1:1 level/line format. This script generates lines with their respective levels, so Rooms can be created. Interestingly, it was faster than the C# node from DynamoMEP. Just to be clear, you need to have an existing model with the levels and floor plans already set up.

# Load the Python Standard and DesignScript Libraries
import sys
import re
import clr

#Import module for Revit
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
#import module for the Document and transactions
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

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

#get the current document in Revit.
doc = DocumentManager.Instance.CurrentDBDocument

#get levels and level names
allLevels = FilteredElementCollector(doc) \
                       .OfCategory(BuiltInCategory.OST_Levels) \
                       .WhereElementIsNotElementType() \
                       .ToElements()

level_names = [l.Name for l in allLevels]

#Create Plane dictionary
planes = []
for level in allLevels:
    elev=level.Elevation
    point=XYZ(0,0,elev)
    planes.append(Plane.CreateByNormalAndOrigin(XYZ.BasisZ,point))

planeDict = {level_names[i]: planes[i] for i in range(len(level_names))}

#Create view dictionary
all_views = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).ToElements()

views = []
for v in all_views:
    if not v.IsTemplate:
        if (doc.GetElement(v.GetTypeId())).get_Parameter(BuiltInParameter.ALL_MODEL_TYPE_NAME).AsString() == "Floor Plan":
            views.append(v)

        viewNames = []
        for view in views:
            viewNames.append(view.Name)

viewDict = {viewNames[i]: views[i] for i in range(len(viewNames))}

data = IN[0]
lines = []
lvlNames = []
levelsNlines = []
sPlanes = []

#regex code to map coordinates
regx = re.compile(r'((?:-)?\d+\.\d+)').findall

#convert strings to model lines
for lvl, str in data:

    lvlNames.append(lvl)

    if "Line" in str:
            line = Line.CreateBound(XYZ(float(regx(str)[0])/12,float(regx(str)[1])/12,float(regx(str)[2])/12),XYZ(float(regx(str)[3])/12,float(regx(str)[4])/12,float(regx(str)[5])/12))
            lines.append(line)

    elif "Arc" in str:
            arc = Arc.Create(XYZ(float(regx(str)[4])/12,float(regx(str)[5])/12,float(regx(str)[6])/12),float(regx(str)[7])/12,float(regx(str)[8])*0.0174533,float(regx(str)[9])*0.0174533, XYZ(1.0, 0.0, 0.0), XYZ(0.0, 1.0, 0.0))
            lines.append(arc)


TransactionManager.Instance.EnsureInTransaction(doc)

#Create SketchPlane dictionary
sPlanes = []

for plane in planes:
    sPlane = SketchPlane.Create(doc, plane)
    sPlanes.append(sPlane)

sPlaneDict = {level_names[i]: sPlanes[i] for i in range(len(level_names))}
docCreation = doc.Create


elementList = []    
levelsNlines = zip(lvlNames,lines)

for key in levelsNlines:
    curvearray = CurveArray()

    curvearray.Append(key[1])
    
    rLine = docCreation.NewRoomBoundaryLines(sPlaneDict[key[0]], curvearray, viewDict[key[0]])

    elementList.append(rLine)
    
TransactionManager.Instance.TransactionTaskDone()



OUT = elementList

Enjoy!

2 Likes