Dynamo Components: Part 2b – Zero Touch Revit

In this installment of my posts covering Dynamo component development, I’m going to be covering the creation of components for Revit using Zero Touch and how their requirements differ from making components that do not work with Revit. Much of what goes into setting up a project will be the same as the vanilla Dynamo/Zero Touch experience, but I felt it would be easiest to keep them separate so the differences are clear.

Developing Zero Touch Nodes For Revit

If you don’t know what Zero Touch is or why you may want to use it, check out the previous post. For now, lets just dig into the problem…

Defining the Problem

For this Revit node I wanted to make something to fit in with the Mesh Join node I made for the non-Revit Dynamo node, so I’m going to make a node to extract the meshes from a Revit element. There are already components that are part of the core Dynamo for Revit that allow you to pull out Curves or Solids from Revit elements, but nothing for getting Meshes. And since there’s no way to easily convert a Solid to a Mesh in native Dynamo, I’ll see what I can do to make one.

Initial Hypothesis

I’m not starting with much of an idea with this node. The MeshToolkit package has a component that allows you to generate a MeshToolkit Mesh (different from a Dynamo Mesh) from a Dynamo Solid so it could be used to go from Revit Element to Dynamo Solid to Meshkit Mesh, and then convert that to a Dynamo Mesh, but it is not quite what I’m after. Knowing that, the only avenue that comes to mind for solving this problem is to figure out how to subdivide an arbitrary solid object into a reasonably accurate polygon mesh, but that seems like overkill.

Research

Since I don’t know a whole lot about how I’m going to resolve this, I start by just searching the Revit API for ‘Mesh’ just to see what comes up. For the sake of convenience, I start with Revit API Docs, an online searchable version of the Revit API help file. Searching for ‘Mesh’ shows at least enough information to know that there are Mesh objects in the Revit API, but even going into the Mesh class documentation shows nothing about how we go from a Revit element to a mesh.

RevitAPIDocs_Search

Just to run though the easy options, I also ran a search in the help file provided with the Revit SDK. That turned out to be more useful since the first ranked result is exactly what we need to proceed. It shows the Face.Trangulate method, which we can trace back to the Face class to see that there are two Triangulate methods – one to run the default triangulate and the other to triangulate with a specified level of detail. In either case, they both return an Autodesk.Revit.DB.Mesh object. The documentation for a Face describes it as “a bounded face of a 3d solid or open shell”, so rather than being a Mesh Face, this represents the face of a Solid or at least a joined set of surfaces.

RevitAPI_Triangulate

Looking through the documentation on the Solid class, we get a handy example that shows how to start with an Element, extract the Solid geometry from it, then iterate through the Faces, and finally triangulate the Faces to get a Mesh. This Mesh object is not quite what we want on its own, in part because it is a Revit mesh instead of a Dynamo mesh, but also because I’m going to want a single mesh per solid and not a different mesh for each face. Based on this information and some of what I’ve learned in the previous post about how meshes are structured, I’m fairly confident I can extract a Revit mesh, convert it to a Dynamo mesh, and then join it together without too much effort.

Prototyping

Since I feel like I know mostly what I need to do based on this example and the mesh related research I did for the last post, I’m going to focus this prototype on seeing if there’s any difference with what happens when I try to run a similar process as shown in the Solid example code on a varying set of elements. Revit has many types of objects that all inherit from the Autodesk.Revit.DB.Element class but have different syntax or requirements, so I’ll put together a sample project with a lot of different types of objects and create a macro that runs through these objects to see what happens when it runs into different types of Revit elements.

Revit_SelectedElements

I started with a group of 14 objects that each represent types of Revit elements that I felt would be a good sampling of what you may run across in Revit, but is not by any means an exhaustive list. I know some elements should have solid geometry and others should not, so I created a Macro that would allow me to iterate through selected Revit elements and report back where along the path, derived from the example in the Solid class documentation, from element to Solid it failed. I stopped at Solid because once I get there I feel like iterating through the faces and generating the meshes will be easy, but I’ve run into all sorts of problems because I didn’t consider an element type and it causes errors or even crashes in a plugin for Revit.

Revit_MacroPrototype_01

The results were surprising in that a lot of the elements that I felt should report back as having found solid geometry did not. Another point of interest is that some elements that I expected to have no Solid geometry, but still have some kind of geometry, didn’t appear to have any at all since their get_Geometry() method returned null. I would have expected that a tag or dimension would at least have some curve geometry that I could find, but while that may be an interesting avenue to explore, it’s not relevant to the component I’m trying to make so I’ll leave that side quest for another day. Since the macro at this point does not produce the desired results I won’t reproduce it in full, but a shortened version of it may be useful for further explanation and our continuing exploration.

// Set up a new set of geometry Options, and get an Element
Options opt = doc.Application.Create.NewGeometryOptions();
Element e = doc.GetElement(eid);

// Get GeometryElement of the selected element and iterate through it's GeometryObjects.
GeometryElement geoElement = e.get_Geometry(opt);
foreach (GeometryObject geoObject in geoElement)
{
    // Convert the GeometryObject to a GeometryInstance and iterate through its GeometryObjects
    // and see if any of them are Solid objects.
    GeometryInstance instance = geoObject as GeometryInstance;
    foreach (GeometryObject instObj in instance.SymbolGeometry)
    {
        Solid solid = instObj as Solid;
    }
}

More Research

A good place to start when you don’t know how some elements differ is to use the Revit Lookup plugin mentioned in an earlier post, and particularly Snoop Current Selection. I grab a sampling of the elements in the model and run the Snoop Current Selection command to see where they start to diverge.

FamilyInstance_SnoopDB

The first element I look at is a Generic Model family that I made for testing this. It’s a simple family made up of a single cube created by way of the extrusion command in the Family Editor. I immediately see a Geometry property that I can click on (bold properties) and do so to see the GeometryElement. So this is what is retrieved via the Element.get_Geometry(Options) method that I used in my macro. So far so good. The GeometryElement also allows me to click on it, revealing that it’s pointing to a GeometryInstance.

So this goes along with the processes taken in the macro to go from an Element to GeometryElement to GeometryInstance to Solid, but something interesting is that while the example found in the API help file and my subsequent macro used the GeometryInstance property SymbolGeometry to get the geometry, there are also a couple of methods worth exploring to GetInstanceGeometry and GetSymbolGeometry. The example code uses the SymbolGeometry property, but it also looks like it’s having to transform the results so it may be easier to see about getting the instance geometry instead and save ourselves the trouble of transforming.

InstanceGeometry_Snoop

Looking at all three of those options, it looks like they all return a pair of Solid objects. Of this pair, one of them is always empty, with no edges or faces, and one of them is probably representative of the cube I modeled (more on this later). For the two Symbol geometry options the valid Solid had a centroid somewhere around (10,43) for the (x,y) coordinates of it’s location in space, and the valid Solid that’s returned by the GetInstanceGeometry method is at roughly (4,19).

LocationPoint

And for one last check with the generic model family, I use Revit Lookup to check the location of the family within the project by way of its LocationPoint property and see that it is also at (4,19), and since the placement point for the family is at the center of the cube’s base, that’s all the confirmation that I need that I can use the GetInstanceGeometry() method to retrieve an appropriately located Solid and subsequent Mesh.

SystemFamily_SnoopDB

The next set type of object I’m interested in exploring is a System family, so I go back and check what Revit Lookup can tell me about it. This shows a very different process than what was seen with the family, and provides a good indication of how System Families differ from other Families in regards to how you get the geometry. For these you get the Solid geometry directly from the GeometryElement rather than needing to go through a GeometryInstance (which doesn’t exist for it). So with that information in hand, we can try fixing the prototype and see if we can get it to produce the results that we’re after.

Another Prototype

Now I’m really thinking we know enough to continue fixing the prototype and make sure we’re on the right track for developing the node. I’ve taken what I’ve learned and added a couple of checks as we’re iterating through the GeometryElement that is initially retrieved from the Revit Element. It checks to see if the GeometryObject is a GeometryInstance, a Solid, or Mesh. If it’s a GeometryInstance it iterates through its GeometryObjects and looks again for Solid or Mesh objects. At the end it reports back as ‘Null’ if it was unable to find any Mesh or Solid geometry in the object, or it reports back how many Mesh or Solid objects it was able to find.

public void GetMeshesRedux()
{
    // 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();

    // Setup the options.
    Options opt = doc.Application.Create.NewGeometryOptions();
    System.Text.StringBuilder report = new System.Text.StringBuilder();
    foreach(ElementId eid in selectionIds)
    {
        Element e = doc.GetElement(eid);
		
        // Get geometry element of the selected element
        GeometryElement geoElement = e.get_Geometry(opt);
		
        // If the geometryElement is null, it's probably an annotation or other object
        // without any solid or mesh geometry.
        if(null == geoElement)
        {
            report.AppendLine(eid.IntegerValue.ToString() + 
                " :: " + e.Category.Name + " :: Null");
            continue;
        }
	    
        // Counters to store how many geometry elements are found
        int solidCount = 0;
        int meshCount = 0;
	    
        // Get geometry object
        foreach (GeometryObject geoObject in geoElement)
        {
            // First check to see if the element is a solid. This will be for things
            // like system families that do not have a GeometryInstance.
            // If it is increment the Solid counter.
            if(geoObject is Solid)
                solidCount++;
	    	
            // Then check to see if the elment is a mesh. This will be for things
            // like topographies that are already a mesh object and don't 
            // need to be triangulated. If so increment the Mesh counter.
            else if (geoObject is Mesh)
                meshCount++;
	    	
            // Then check to see if it's a GeometryInstance. This will be for a 
            // typical family or an import
            else if(geoObject is GeometryInstance)
            {
                GeometryInstance instance = geoObject as GeometryInstance;
                foreach(GeometryObject instObj in instance.GetInstanceGeometry())
                {
                    // If the GeometryObject is a Solid, incrment the Solid counter.
                    if(instObj is Solid)
                        solidCount++;

                    // If the GeometryObject is a Mesh, increment the Mesh counter.
                    else if(instObj is Mesh)
                        meshCount++;
                }
            }    	
        }
        // Append the report
        if(solidCount > 0 && meshCount > 0)
        {
            report.AppendLine(eid.IntegerValue.ToString() + " :: " + 
                e.Category.Name + " :: Element contains " + solidCount.ToString() + 
                " solid geometry object(s)\n\t\tand " + 
                meshCount.ToString() + " mesh object(s)");
        }
        else if(solidCount > 0)
        {
            report.AppendLine(eid.IntegerValue.ToString() + " :: " + 
                e.Category.Name + " :: Element contains " + solidCount.ToString() + 
                " solid geometry object(s)");
        }
        else if(meshCount > 0)
        {
            report.AppendLine(eid.IntegerValue.ToString() + " :: " + e.Category.Name + 
                " :: Element contains " + meshCount.ToString() + 
                " mesh geometry object(s)");
        }
        else
        {
            report.AppendLine(eid.IntegerValue.ToString() + " :: " 
                + e.Category.Name + " :: Null");
        }
    }
    TaskDialog dlg = new TaskDialog("Geometry Macro");
    dlg.TitleAutoPrefix = false;
    dlg.MainInstruction = "Geometry Retrieval Results";
    dlg.MainContent = report.ToString();
    dlg.Show();
}

Overall this is working great and the results that it’s giving me now aligns pretty well with what I was expecting. The elements that I expect to have geometry are correctly reporting that Mesh or Solid geometry was found within the element, and the elements that I expected no Solid or Mesh geometry to be found are in fact showing as null.

Revit_MacroPrototype_02

I mentioned earlier that the generic model family I created with a single cube was showing as having two solid objects, with one of the solids being invalid since it had no faces or edges. I haven’t researched why this is the case, but the example that was in the Solid class documentation showed a way to manage this, which makes me think it’s a somewhat common issue. It does so with a simple check to see if the number of Faces and Edges are greater than 0 before it tries to do anything, and if that is not the case it just skips that solid.

Solid solid = instObj as Solid;
if (null == solid || 0 == solid.Faces.Size || 0 == solid.Edges.Size)
    continue;
Project Setup

I’m finally confident that I can proceed with creating a node that provides a Dynamo Mesh from a Revit element, at least whenever it’s able to find a Solid or Mesh within the given Element. This component is still going to follow the Zero Touch methodology so the initial setup is going to be similar to the previous post. I’ll create a new project and add the NuGet package for DynamoVisualProgramming.ZeroTouchLibrary which will also include the DynamoVisualProgramming.DynamoServices package.

NuGet_Packages

Looking at the rest of the Dynamo related packages available via NuGet, you may notice that there is nothing that mentions ‘Revit’. Because of this we’ll need to add our own references to the Revit API and to Dynamo Revit libraries. For this I’m going to use the current versions that are for Revit 2018, but from the paths shown you can probably work out where to find them for other versions of Revit. To add the references, right-click on the References section for the project in the Solution Explorer and click on ‘Add Reference…’ from the right-click menu.

Add_references

That will pop-up the ReferenceManager where you can add some of the more common .Net libraries as well specific ones that you may have downloaded. The ones we’re interested in here are the ones for the RevitAPI and the ones for Dynamo for Revit. On the RevitAPI side, there are two DLL’s that are commonly used when you’re developing a Revit plugin, RevitAPI.dll and RevitAPIUI.dll. For the purposes of working in Dynamo we usually only need the RevitAPI library since we’re not typically going to need access to the Revit UI. There are some situations where you will, but for now I’m just going to add the RevitAPI.dll file that can be found in the Revit install directory. For the Revit for Dynamo side, there are a couple of libraries that we’ll need to reference. One will give us access to the Revit.Elements and GeometryConversion (RevitNodes.dll) classes to go back and forth between Dynamo and Revit, and the other will give us access to things like transactions or the Revit document (RevitSerivces.dll).

References_Needed

These can both be found in the Dynamo for Revit install directory. You can use the path’s shown in the image below to try and track them down on your own computer. To add them, use the Browse button in the lower right and navigate to the correct directory per your installs. Look for the RevitAPI.dll in the Revit install directory and the RevitServices.dll and RevitNodes.dll files in the Dynamo Revit install directory. Once they’ve been added to the project you’ll need to make sure the Copy Local property is set to False. This is the same process that we do for the NuGet packages and the references they add, except now it needs to be done for the four references from NuGet and the three references we’ve just manually added.

Developing the Node

The project is set up and we’re ready to begin developing our new node. This project is up on GitHub so I’m not going to post all of the code here on the blog because it’s a running at about 5 times the code of the previous Zero Touch and Python examples, but I do want to highlight a couple of places that are of particular interest, starting with the using statements at the top.

using System.Collections.Generic;
using Autodesk.Revit.DB;
using dsGeo = Autodesk.DesignScript.Geometry;
using rElems = Revit.Elements;
using Revit.GeometryConversion;
using RevitServices.Persistence;

This particular class is using two of using statements from the previous example, System.Collections.Generic for lists and Autoodesk.DesignScript.Geometry for Dynamo geometry, but it also has three new statements to bring in Revit API (Autodesk.Revit.DB), Dynamo’s version of Revit elements (Revit.Elements), a conversion library to go from Revit to Dynamo or Dynamo to Revit (Revit.GeometryConversion), and access to the Revit document (RevitServices.Persistence). The other notable differences is that some of these are set up a little differently, such as the “using dsGeo = Autodesk.DesignScript.Geometry;” line. This is setting the Autodesk.DesignScript.Geometry namespace to a variable named dsGeo. I’m doing this here because The Dynamo Geometry and Revit Geometry often have the same name, particularly for something like Mesh, and so we don’t confuse ourselves or the program we’re using this to keep them separate. dsGeo.Mesh will be a Dynamo Mesh, and just Mesh will be an Autodesk.Revit.DB.Mesh. Likewise the rElems variable being used for Revit.Elements is to remove the ambiguity between an Autodesk.Revit.DB.Element and a Revit.Elements.Element. The other option would be to type the full namespace for the object types, but this is less to write and for me it’s easier to read.

Mesh_Flowchart

Other than the using statements, most of what is within the code follows a similar logic to what we already saw in the prototype. Do we have a valid Revit element? Does it have a valid GeometryElement? Does that GeometryElement have a GeometryInstance? If not how about a Solid or mesh? If it does have a GeometryInstance does that have a Solid or Mesh?

The other interesting parts I wanted to point out are the Revit.GeometryConversion operations, and the acquisition of the Revit document. For the Revit document, we have to use the RevitServices library to acquire the document. With a typical Revit plugin, or even a macro, you would typically have access to this through the plugin entry method, but since we don’t have that we’ll utilize RevitServices. The use of it is basically the same as when you need access to the Revit document in a Python node, but since we’ve added the RevitServices.Persistence napespace to our using statements we just need to use DocumentManager.Instance.CurrentDBDocument.

// Get the current Revit document by using the RevitServices reference. This is similar
// to how you would get the Revit document in a Python node within Dynamo.
rDoc = DocumentManager.Instance.CurrentDBDocument;

For the GeometryConversion stuff, we use the Revit.GeometryConversion class from the RevitNodes.dll reference. GeometryConversion provides a convenient methodology that translates between the differing geometry libraries that Dynamo and Revit use. While some things it may seem like it’s just as easy to do the conversion yourself, such as from an Autodesk.Revit.DB.XYZ to an Autodesk.DesignScript.Geometry.Point, the convenient thing that the GeometryConversion class provides is unit handling. The RevitAPI internally assumes imperial feet as the unit type, so if you use the API directly to create a new point at (1,1,1) then it will always be (1′,1′,1′), even if your project is in millimeters unless you specifically account for it. Dynamo itself is currently unitless, so if you say something is 3×3, what ‘3’ is depends on the context of the Revit model’s units. So with GeometryConversion you could just say ‘turn this XYZ into a Point’ and it will take care of that for you and not worry about converting 3m to 9.84ft. For something more complicated like a Mesh which could require several lines of code to translate, not even taking into account unit handling, this gets condensed down to one line.

// Each face of a solid is tessellated separately into a unique mesh, so we first
// Collect all of the meshes that represent a solid's face.
List<dsGeo.Mesh> unjoinedMeshes = new List<dsGeo.Mesh*gt;();
foreach (Face f in solid.Faces)
{
    // call the Triangulate method for the Autodesk.Revit.DB.Face
    Mesh rMesh = f.Triangulate();

    // Use Revit.GeometryConversion.RevitToProtoMesh.ToProtoType method to convert the 
    // Autodesk.Revit.DB.Mesh to an Autodesk.DesignScript.Geometry.Mesh
    dsGeo.Mesh dMesh = RevitToProtoMesh.ToProtoType(rMesh, true);
    unjoinedMeshes.Add(dMesh);
}

Final Comments

As far as the differences go between Revit and Dynamo centric nodes I think everything above covers at least the basics of what you need to consider. In addition to what’s shown in this post you should also consider looking at the sections of the previous post covering the Adding Icons and Adding a Customization File to cover how to fine tune how the Zero Touch components integrate into Dynamo.

All of the customization and icon implementation is in the code shared in the ZeroTouchRVT folder of my DynamoExperiements GitHub repository for anyone that wants to see how it all works.

Dynamo_WorkingExample