Dynamo Components: Part 1

The Dynamo platform has really matured from its humble beginnings, and the use of it has really taken off within the office with people excited to be able to program Revit in a friendlier manner. However, as friendly as Dynamo is at this point, the Revit API has a lot to it, and there just aren’t enough Revit nodes to keep everyone satisfied. There’s always something more someone is going to want to do, and when they get to that point, they usually come to me.

As usage increased over the last couple of years it caused a corresponding increase in requests for custom nodes, and my library of Python scripts and nodes grew organically with no planning or organization, and was really starting to become a little messy and occasionally redundant. At the beginning of this year I had some unallotted time so I thought I would spend it evaluating what I have made and try to collect what is genuinely reusable into a package I could maintain. As part of this process I wanted to better understand and document the process of creating Dynamo nodes in each of the various methodologies Autodesk has provided for developers.

If you’ve considered developing Dynamo nodes you probably already have an awareness of the paths node development you can take: Custom Nodes – clusters of existing Dynamo nodes, Zero-Touch nodes – complied libraries with static methods Dynamo creates nodes from, and Custom UI nodes – compiled libraries with classes typically derived from the Dynamo.Graph.Nodes.NodeModel class. While the Zero-Touch and Custom Nodes provide the capabilities to do almost anything I would need on a functional level, they left a little to be desired from a UI/UX context. Having the ability to have right-click menu options, change settings, or just have other controls like buttons or dropdown controls on the node could go a long way to a better user experience. I have had plenty of success creating Custom Nodes and Zero-Touch nodes, but my limited attempts at creating Custom UI nodes dating back to sometime in 2012 were unsuccessful until this most recent push.

Starting with this post I thought I’d put together my process for developing each of the three paths of Dynamo nodes, starting with a Custom Node that’s primarily created using a Python node. I have outlines for the subsequent posts on Zero-Touch and Custom UI nodes, and they may not be as exhaustive as this turned out to be, but they should give you an idea of how these things work together, some lessons learned along the way, and how I go about developing a node.

Developing Custom Nodes

Custom Node is the terminology Dynamo’s wiki uses to refer to nodes built in the Dynamo UI constructed by nesting other nodes inside of a container node. Most of the time I’m doing a Custom Node that I want to reuse, I’m doing it with a Python node because it gives access to the Revit API which enables more opportunities when working with Revit projects. Because of that I will focus this exploration to how I develop Python nodes to work with Revit elements.

Even though Custom Nodes are a convenient way to encapsulate more complicated algorithms or groups of nodes into a single node that simplifies reuse and sharing, even for a single node like a Python node it gives you the ability to manage default input parameter values and explicitly name the inputs and outputs, making using the component much easier.

Define the Problem

The first step when developing anything is going to be defining the problem that you’re hoping to solve. For this discussion I’ll use a node I wrote to filter Revit elements by their Workset. This came to me from one of our Practice Technology leaders who wanted it as part of a Dynamo graph they were creating to help with model reviews, and they preferably wanted to do said filtering using a Workset’s name.

Initial Hypothesis

The first thing I always do is think about the easiest way to solve the problem. For Dynamo related endeavors that’s usually to see if there’s an easy way to get the workset name from the existing components. Even though I know the person making the request had done so, my first pass at this was using the default Dynamo Revit node Element.GetParameterValueByName, assuming I could use this to get a Workset object, or at least the Workset’s name as a string that I could use with an equality check. Using this node to find the value of the “Workset” parameter for a given list of elements returns a list of integers. We can probably safely assume that each unique integer value represents a unique Workset, but other than that it’s not terribly helpful if we’re looking to identify it by a name.

GetWorkset_Ints

Another quick follow-up within Dynamo is to just search for the object you want, in this case “Workset”, to see if there were any default nodes that dealt with them and came up with nothing. That’s not to say there are not Packages out there that deal with these things, as I’m sure there are, but there’s a good chance that if I’m being asked for help on something it’s because they don’t have such a Package. Plus sometimes I just like doing things the hard way.

Search_Worksets

Research

This leads us to investigating the Revit API to get a better understanding what a Workset is in the Revit API, and how it relates to a typical Revit element and it’s “Workset” parameter. Looking up Workset in the help file provided with the Revit 2018 SDK shows that a Workset has the namespace Autodesk.Revit.DB.Workset and does not inherit from the Autodesk.Revit.DB.Element class like many of the more tangible Revit objects.

WorksetClass_Docu

Looking further into the Workset class documentation shows that it has an ‘Id’ property much like other Revit elements, but clicking on that property does not take us to the definition of an ElementId like I would expect. Instead, it takes us to is the documentation on the WorksetId class, something that looks similar to, but is apparently different enough from an ElementId to need a separate class.

WorksetClass_Properties

WorksetClass_IdProperty

So what this shows us is that we can be certain a Workset is not something directly related to an Element in Revit’s API so that gives us the first clue as to what’s going on. But to get a little further clarification it’s great to use another tool: the Revit Lookup add-in that’s currently maintained by Jeremy Tammik of Building Coder/3D Web Coder fame. This gives you the easiest way to start looking at elements in a Revit file and see how it ties to the API. So to start I select a random model element in a workset-enabled Revit file and use the Revit Lookup tool to Snoop Current Selection.

Snoop

As shown below, what I selected was a FamilyInstance, just a typical family used in this Revit project. On the right side of the Snoop Objects form is a list of properties and methods that are shown for this Element as it relates to being cast as an Autodesk.Revit.DB.Element (FamilyInstance’s base class) and further down the properties and methods as relates to the Autodesk.Revit.DB.FamilyInstance class. I know just from using Revit that all elements will have an instance parameter named Workset that tells the Workset an element is tied to, so the first place to look is the Parameters field which gives the list of instance parameters associated with this element.

Snoop_FamilyInstance

Clicking on the Parameters link will open a new window that gives you a list of instance parameters related to this FamilyInstance object, mostly things that would be visible in the Properties Palette of the standard Revit UI. Scrolling down until we find the Workset parameter and selecting it will show properties and methods related to that parameter on the right side of this new window. This provides the next clue since we can see that the StorageType of the Workset parameter is set to Integer. A Revit parameter has a small number of possible storage types: None, Integer, Double, String, and ElementId, so when the GetParameterValueByName node is used in Dynamo, it’s probably reading from the parameter that it’s an integer parameter so it just returns the integer value of the Workset’s Id. So this confirms that you will have no access to the Workset itself this way.

Snoop_WorksetStorageType

Contrast all of the above to what happens when you use Dynamo’s GetParameterValueByName node to retrieve a Material parameter, a parameter type which has a StorageType of ElementId. The ElementId provides a link to another Revit Element, so what the node returns is the element that corresponds to the ElementId, in this case a Material. So it’s technically possible for the GetParameterValueByName node to return another object, but considering there are no other Workset object nodes in core Dynamo, having it return a Workset object would require more than just a modification of this one node’s code to manage the special case of the Workset parameter.

GetMaterial_Elem

There could be work arounds to this problem without getting into Python, perhaps the user selects an element that has the desired Workset and the integer that corresponds to that gets used for the equality test, but workarounds like this tend to make the tool more cumbersome to use from the user’s perspective. They will see a list of obscure integers and be forced to make seemingly random selections that must occur without knowing why or how it makes sense compared to what they are used to as non-developers, namely saying they want things on a Workset of a given name.

The last bit of research about the Workset class that can helpful before we start trying to prototype the code is in the About page for the Workset class. Many of the heavily used classes in the help documentation have a small example that shows you how you may use the object, and for the Workset class it shows you all you need to get a Workset object by just knowing the WorksetId, something we should be able to get since we know we have access to the integer value of that WorksetId.

WorksetClass_Example

Prototyping

When I start the prototyping phase of a project I start by laying out the logic of what I think I’ll be doing. From my initial experimentation with the default Dynamo nodes, trying to use the GetParameterValueByName node, I still think I will follow a similar logic when I create it as a Python node. Based on what I’ve learned so far, I see two paths to complete this project:

PATH 01

  1. Get a list of Elements(input from user)
  2. Get a Workset name (input from user)
  3. Loop through elements

    1. Get the element’s ‘Workset’ parameter
    2. Get the integer value of the parameter
    3. Create a WorksetId from the acquired integer
    4. Get the Workset from the WorksetTable, using the WorksetId
    5. Compare the name of the Workset assigned to the Element to the user provided name

      1. If the name is the same, retain the element in a new list
      2. Else continue to the next element in the list
  4. Return a list of Elements that matched the provided name
PATH 02

  1. Get a list of Elements(input from user)
  2. Get a Workset name (input from user)
  3. Itreate through the project’s Worksets

    1. Check the current iterated Workset’s name against the provided name

      1. If the name is the same, retain the Workset.Id’s integer value
      2. Else continue to the next Workset in the loop
  4. Loop through the Elements

    1. Get the integer value of the ‘Workset’ parameter
    2. Compare that integer to the one found while looping through the project’s worksets (step 3)

      1. If the integer is the same, retain the element in a new list
      2. Else continue to the next Element
  5. Return the list of Elements that matched the provided Workset name.

Of these two paths, I should have all the information I need to go with Path 01. Path 02 simplifies the loop iterating through the elements, which would probably require more iterations than the workset loop, but requires a little more research into how to get a list of Worksets from a project and how I would iterate through them. I’ll just continue by implementing what’s laid out in Path 01.

Whenever I start writing a new Revit function, particularly for things I haven’t done before, I start by creating a Revit macro using C# as a prototype. There is some translation that will be necessary between the C# macro and the Python node, but the advantage provided by the macro editor (SharpDevelop) and C# – macros can also be done in Python – is that autocomplete works really well with a .Net language like C#, which I find appealing.

One of the reasons why this is beneficial is you can serendipitously write a better algorithm by running across a property or method you may not have thought of or known about, something that actually happened while I was putting this together. What I discovered is that when I went to get the WorksetTable from the current Revit document, as described in the Workset example code, is that there is a Document.GetWorksetId method that returns a WorksetId when you pass it an ElementId. This allows us to save several steps in our loop as we no longer have to get the Parameter, then the Parameter’s value, then the worksetId from that integer value.

Code_Serendipity

In the end the macro looks something like this:

public void FilterByWorksetName()
{
    // current UIDocument and Document
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = uidoc.Document;
    
    // Get the selection. Elements must be pre-selected before running this Macro.
    ICollection<ElementId> selectionIds = uidoc.Selection.GetElementIds();
    
    // Hard coded workset name to filter by.
    string worksetName = "AR - STRUCT";
    
    // Get the WorksetTable of the current document
    WorksetTable worksetTable = doc.GetWorksetTable();
    
    // Iterate through the selection and collect the elements that match 
    // the provided worksetName.
    List<ElementId> matchingElements = new List<ElementId>();
    foreach(ElementId eid in selectionIds)
    {
        // Get the WorksetId assigned the ElementId and retrieve the 
        // Workset from the WorksetTable.
        WorksetId wsId = doc.GetWorksetId(eid);
        Workset w = worksetTable.GetWorkset(wsId);
        
        // Check to see if the Element's Workset name matches the selected name.
        if(w.Name == worksetName)
            matchingElements.Add(eid);
    }
    
    // Change the current selection set to only be the elements that pass the filter.
    uidoc.Selection.SetElementIds(matchingElements);
}

This will iterate through a selected set of elements and modify the selection to only include those elements that have a matching Workset name. In the example shown it only keeps elements that are in the “AR – STRUCT” Workset, something that would require a recompile to change. Since this is just for exploratory purposes I will leave it at that and move on.

1116 Elements Selected

1116 Elements Selected

20 Elements Selected - Only Items That Pass the Workset Filter

20 Elements Selected – Only Items That Pass the Workset Filter

Python Node

At this point I have a functional macro that works and is actually more concise than I originally planned, so it’s now time to translate this into something that’s more appropriate for Dynamo. This will retain some parts that are similar to the macro we just wrote, but sections of it will diverge. The initial setup is going to be putting together a graph that selects the same set of Elements, allows me to input a Workset name, and then a Python node that has inputs for the Elements list and Workset name.

Python_Setup

The Python we’ll be writing can be broken down into 4 sections: References, Inputs/Setup, Loop/Test, Output. For the Reference section you will see a lot of similarities with other Python scripts you’ll see on the Dynamo forum. By default, a Python node in Dynamo imports ‘clr’ and adds a reference to ‘ProtoGeometry’, the Dynamo geometry library, and then imports everything in the geometry namespace. In this Python script we will not be using any Dynamo geometry so we can remove these lines, but I’ve retained them and just commented those lines so they will not be evaluated.

import clr

# Add a reference to the RevitAPI and import the DB namespace.
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

# Add a refernce to RevitServices to gain access to the Revit Document.
# Then import the DocumentManager from RevitServices to get the document.
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager

What is not a default inclusion for the Python node is the RevitAPI nor the RevitServices, a Dynamo for Revit library that gives you access to the Revit document amongst other things, so references to those libraries must be added in. In addition to adding the references, we import the Autodesk.Revit.DB namespace in its entirety, and we import the DocumentManager from RevitServices.Persistence.

Following the references is the code where we assign the inputs to a pair of variables, get the current Revit Document from the RevitServices DocumentManager, and get the Document’s WorksetTable so that we can get a Workset object by a WorksetId later in the script. The structure is a little different, but it’s starting to look a little more like the code written for the macro.

# The inputs to this node will be stored as a list in the IN variables.
# Instead of getting all of the inputs together, we'll grab them individually.
#dataEnteringNode = IN
elements = IN[0]
worksetName = IN[1]

# Get the document from the DocumentManager
doc = DocumentManager.Instance.CurrentDBDocument

# Get the WorksetTable from the document
worksetTable = doc.GetWorksetTable()

Next we need to iterate through the input Elements, determine their Workset, and put them in an appropriate bin based on the Workset name matching/not matching the input. For this I’m creating two variables as empty lists, one to contain the elements that match the user selected workset, and one to contain those that don’t. This is primarily to follow a similar convention to the FilterByBoolMask node that’s part of the core Dynamo nodes, but it also allows us to evaluate how to handle multiple outputs from a Python node that has a single output node.

Functionally this section is very similar to what the foreach loop in the macro did previously, with the extra step necessary to unwrap the Element. An explanation at this point may start sounding confusing, but it’s sufficient to know that you must use UnwrapElement pretty much any time you’re working with Revit Elements within Dynamo and its Python nodes, and ideally when return elements from Python they should be ‘wrapped’. So when we are doing Revit API stuff, we unwrap the element (‘rElem’ in the code below) so we have access to these Revit API functions, and when we’re passing the elements on we make sure to pass on the wrapped version (‘elem’ in the code below).

# Create lists to store the matching and unmatching elements.
# The two lists are to match the convention in the FilterByBoolMask node.
matchingElements = []
nonMatchingElements = []

# Iterate through the input Elements to compare the Element's Workset
for elem in elements:
    # A Revit Element within Dynamo is wrapped into a Revit.Elements.Element class
    # that contains the Autodesk.Revit.DB.Element object so we need to use the 
    # UwnrapElement method to get the DB element we can actually get properites from.
    rElem = UnwrapElement(elem)
	
    # Get the WorksetId from the Revit Element's Id property
    wsId = doc.GetWorksetId(rElem.Id)
	
    # Get the Workset from the WorksetTable using the WorksetId
    workset = worksetTable.GetWorkset(wsId)
	
    # check the name of the workset against the input name
    if workset.Name == worksetName:
        # Add the wrapped elem to the matching list.
            matchingElements.append(elem)
    else:
	# add the wrapped elem to the non-matching list.
	nonMatchingElements.append(elem)

After this section, all that is left is to output our two lists. With Python nodes, everything gets output from the single OUT variable/output parameter. To output more than one element, in this case two different lists, we set OUT equal to both of them, using a comma to separate the variable names.

#Assign your output to the OUT variable.
OUT = matchingElements, nonMatchingElements

At this point all of the development side of things is done. We have a python script that can take a list of Revit Elements and a Workset name and filter the Elements list accordingly.

Dynamo_WorkingPy

Making a Custom Node

All that is left is to package this Python script as a Custom Node, typically a cluster of nodes that get collapsed into a single node. While our Python node is all we need to add to the Custom Node, there are other benefits that the Custom Node provides beyond consolidating many nodes into one. Custom Nodes are easier to share, can be part of a Package, and can have the inputs and outputs more clearly named, and can have default values for the inputs.

You can create a Custom Node within the current Dynamo workspace by selecting the nodes you want to cluster and going to Edit > Create Node From Selection, though I prefer to create a new Custom Node and copy the necessary parts into it so I can set up the inputs and outupts as I’m making it. While you can have only one one workspace/graph open at a time, you can have a Custom Node workspace open along with your home workspace. So go to File > New Custom Node and you will be presented with the Custom Node Properties. Here you define the name of the node, a description of it, and the Category. The first two are pretty self-explanitory, but the Category defines where the node will be located in the Node Library. You can choose an existing location, or provide your own. For me, I’m going to place it on a tab named LINE for my studio and in a subgrouping for Revit, giving me a category of ‘LINE.Revit’ that I just type in manually.

CustomNode_Properties

When you click OK, it will go to the Custom Node workspace. You will be in an empty graph with a pale yellow background. Because I have both my original Dynamo graph open and this new Custom Node graph, I can switch back to the normal graph, select the Python node, copy it to clipboard, and then switch back to the Custom Node and paste it into the graph. That way all of the Python is setup and I just need to fix up the inputs and outputs.

CustomNode_Setup

For the inputs it’s as simple as adding two ‘Input’ nodes, nodes that should only be accessible while in a Custom Node graph. They will be empty, so type the name of the inputs as you want them to appear. I’ve specified ‘elements’ for input IN[0] and ‘worksetName’ for input IN[1], corresponding to how my Python script is set up. For the outputs, rather than having the user having to separate the elements that pass from those that fail, we can do that inside this Custom Node and then output the two separately. So just like the inputs, I add two ‘Output’ nodes and give them appropriate names (‘Pass’ and ‘Fail’). Then I use a code block to easily separate the two lists from the Python script’s output and wire it up appropriately. Then save your Custom Node and close it and it will be available in your library.

CustomNode_Loaded

What Could Have Been?

At the end of this exercise I have a node that fills a specific task that was not easily done using the standard set of Dynamo nodes, but there’s always room for improvement. A more valuable component may be one that just returns a list of Workset objects that match an input list of Elements. From the Workset you could pull the Name, Id, Kind (view workset, user workset, etc.), whether it’s visible by default, whether it’s open, or any of the other handful of properties a Workset has. Many of these properties would probably have to be accessed via other custom nodes, but it would add more capability based on what was started.

Another way it may have been done better is to iterate through the elements and then just create a corresponding list of the Element’s Workset names and not try to filter within the node. This gives a little more flexibility to what can be done since rather than just filtering by an absolute match you could also do operations like filtering by a partial name match or group the elements according to Workset name, all things that could be done using standard Dynamo nodes.

Final Comments

In the end this is one way of doing things that made sense to me at the time of doing it. There’s a lot of ways something could be implemented to make the User Experience better, make the graph more efficient, provide more functionality, or any host of other criteria. This was meant to be an exploration and documentation on how I start developing something, with a focus on the final product being a Python node within a Dynamo context. As I continue documenting what knowledge I’ve acquired for developing Dynamo nodes, I will likewise continue to add additional posts to this blog.