dynamo

From Dynamo to C# – Get and Set Parameters

I’m the kind of guy that uses Dynamo for practical things, an indispensable swiss army knife that I used to manipulate data.

Getting and setting parameters is obviously key. This time around we look at how this is done in Dynamo and then how we can achieve that same thing with code.

In this example I will look at how to build up an asset code based on the level name and the mark parameter, combining those into a single new parameter value.

We will start with the code from the previous Dynamo to C# post where we collected all the AHU families in the model. For convenience, here is the complete code from the previous post:

public void getMechanicalEquipment()
{
	//get current document
	Document doc = this.ActiveUIDocument.Document;
	
	//setup collector and filter
	var collector = new FilteredElementCollector(doc);
	var filter = new ElementCategoryFilter(BuiltInCategory.OST_MechanicalEquipment);

	//collect all mechanical equipment
	var allMechanicalEquipment = collector.WherePasses(filter).WhereElementIsNotElementType().Cast<FamilyInstance>();

	//use LINQ to filter for familyInstances containing AHU in the family name
	var ahuEquipment = from fi in allMechanicalEquipment
		where fi.Symbol.FamilyName.ToUpper().Contains("AHU")
		select fi;

	//setup string builder to store data
	StringBuilder displayInfo = new StringBuilder();

	//loop through, collect data
	foreach (FamilyInstance fi in ahuEquipment)
		{
			var elementinfo = string.Format("Family Name: {0} Element Id: {1}", fi.Name, fi.Id);
			displayInfo.AppendLine(elementinfo);
		}

	//display results
	TaskDialog.Show("Watch", displayInfo.ToString());
}

Getting the Level

To do this in Dynamo, you would need to string together the following nodes:

In Dynamo we need to use the Level.Name node to get the actual name. The reason for this is that when we feed Level into the Element.GetParameterByName node, the output is actually an element id. Feeding that element id into the Level.Name node returns the name without any further work required.

So how do we replicate this in C#?

First, we need to focus on the foreach loop, as this is where the work is done for each element that we have selected. We have already done the groundwork to select our elements, but how do we find out what level each element is associated with?

We need to use the level class from the API, this allows us to retrieve the name of the level as you would see it when inspecting the element you’re working with.

Using the level class, we create a variable theLevel which we find from the family instance fi.

Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;

This line is doing half the work of our Level.Name Dynamo node. It is taking the element id of the level that is reported from the Level parameter of the selected AHU family, then selecting the level element.

From there we can simply use theLevel.Name to display the level name.

//loop through, collect data
foreach (FamilyInstance fi in ahuEquipment)
{
	Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;
	var elementinfo = string.Format("Family Name: {0} Level: {1}", fi.Name, theLevel.Name);
	displayInfo.AppendLine(elementinfo);
}

Note that instead of using the elementinfo variable to display the family name element id of the AHU in the dialogue box as per the previous example, we are now displaying the family name and the associated level name.

Getting the Mark

There isn’t a whole lot of change between the process for picking up the mark parameter in Dynamo compared to the level name; the only real change is that we can drop the Level.Name node.

This is because when we tell the Element.GetParameterValueByName node that we want a parameter named Mark, it returns the result as a string.

In C# we want to use get_Parameter on the family instance fi.

But before we get too far, we need to understand that built in parameters are handled slightly differently in Revit to user defined parameters. Built in parameters are contained within an enumerated list, you can review the list of built in parameters on Revit API Docs.

In the API, the mark parameter is stored as ALL_MODEL_MARK

Retrieving built in parameters this way is more reliable as there is no risk that you will pick up a user defined parameter of the same name.

Now that we know this, our equivalent line of code to read the mark parameter in C# looks like this:

Parameter theMark = fi.get_Parameter(BuiltInParameter.ALL_MODEL_MARK);

But we still don’t have the parameter value. To retrieve the parameter value, we need to use the AsString method on our paramter theMark which will return the parameter value as a string.

It is important to use the correct method to avoid any potential errors returning the data, for numbers use AsDouble, for integers use AsInteger and element ids use AsElementId.

Our foreach loop should now look like so:

//loop through, collect data
foreach (FamilyInstance fi in ahuEquipment)
{
	//get the level
	Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;
	//get the mark
	Parameter theMark = fi.get_Parameter(BuiltInParameter.ALL_MODEL_MARK);

	var elementinfo = string.Format("Family Name: {0} Level: {1}", fi.Name, theLevel.Name);
	displayInfo.AppendLine(elementinfo);
}

Again, adding this to our task dialogue, we end up with the following results.

Building the New String

We can build our new parameter value a few different ways in Dynamo, either by using the String.Concat node, or by using a code block which is my preferred method.

There are also a few different way you can approach building the new string in C# as well. The simplest way is to approach it the same way as we do in the Dynamo code block.

First you define the string, in this example we are naming it newValue and we take our level name theLevel.Name, add the string _AHU_ and the we take our mark value with theMark.AsString().

string newValue = theLevel.Name + "_AHU_" + theMark.AsString();

The other method is to use a string builder which we have been using in our examples to create our dialogue box message. The string builder version would look like so:

var newValue = string.Format("{0}_AHU_{1}", theLevel.Name, theMark.AsString());

Either method is acceptable, but if you’re building more complex strings the string builder is the recommended way to go. Both options give the exact same results:

Setting the New Parameter

In Dynamo, the job here is done by the trusty Element.SetParameterByName node seen above, but in C# there is a tiny bit more to do.

The first thing we need to do to set out parameter is define which parameter we want to set. In this example I’m going to use the built in parameter Comments which is stored in Revit as
ALL_MODEL_INSTANCE_COMMENTS.

Parameter theComments = fi.get_Parameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS);

The next step is to start a transaction. Every time you want to change something in a Revit file via the API, you need to make the changes inside a transaction. If you have ever looked into Dynamo nodes containing Python code, you would have seen something similar to this before.

//set the parameter
using(Transaction t = new Transaction(doc, "Set Parameter"))
{
	t.Start();
	//do stuff here
	t.Commit();
}

We define a new transaction t which is being performed in the current document doc which will appear in the undo/redo list as Set Parameter.

Inside our transaction, we set the parameter by using the Set method on our parameter theComments.

//set the parameter
using(Transaction t = new Transaction(doc, "Set Parameter"))
{
	t.Start();
	
	theComments.Set(newValue);
	
	t.Commit();
}

And that’s it! We have successfully pulled information from multiple parameters, generated new string data and set another parameter using C#.

The complete code is below:

public void getsetParameters()
{
//get current document
Document doc = this.ActiveUIDocument.Document;

//setup collector and filter
var collector = new FilteredElementCollector(doc);
var filter = new ElementCategoryFilter(BuiltInCategory.OST_MechanicalEquipment);

//collect all mechanical equipment
var allMechanicalEquipment = collector.WherePasses(filter).WhereElementIsNotElementType().Cast<FamilyInstance>();

//use LINQ to filter for familyInstances containing AHU in the family name
var ahuEquipment = from fi in allMechanicalEquipment
	where fi.Symbol.FamilyName.ToUpper().Contains("AHU")
	select fi;

//setup string builder to store data
StringBuilder displayInfo = new StringBuilder();

//loop through, collect data
foreach (FamilyInstance fi in ahuEquipment)
	{

		//get the level
		Level theLevel = fi.Document.GetElement(fi.LevelId) as Level;

		//get the mark
		Parameter theMark = fi.get_Parameter(BuiltInParameter.ALL_MODEL_MARK);

		//get the comments
		Parameter theComments = fi.get_Parameter(BuiltInParameter.ALL_MODEL_INSTANCE_COMMENTS);

		var newValue = string.Format("{0}_AHU_{1}", theLevel.Name, theMark.AsString());

		var elementinfo = string.Format(newValue);
		displayInfo.AppendLine(elementinfo);

		//set the parameter
		using(Transaction t = new Transaction(doc, "Set Parameter"))
		{
			t.Start();

			theComments.Set(newValue.ToString());

			t.Commit();
		}

	}

}

Building Healthy Asset Models

Understanding how to integrate with your client’s asset and facility management requirements at first seems like a daunting task, however getting it right can be as simple as asking the right questions.

From the perspective of a project manager or even an asset owner, some of the questions that should be asked are:

  • When will the facilities management team get access to the model? Will it be during the design phases of the project, or will it be once the design is complete?
  • Will the facilities management ream be able to dictate to the design team what information is incorporated in the model?
  • Will the facilities team be able to review the model before construction and commissioning?
  • Will the facilities management team own the model when it is completed?
  • Who maintains the model once handover is completed?

Then there are questions that the more technically minded team members tend to focus on around information requirements

  • What information does the facilities team require?
  • Do the facilities team actually need all of this information?
  • What data formats does the facilities team require?
  • Do they have existing non-BIM facilities packages that require integration into the new BIM enabled system? Can it be integrated at all?

If you’re interested in how I approached the problem of recording existing assets in one of Australia’s largest health precincts, take some time out to check out my AU2018 presentation

https://www.autodesk.com/autodesk-university/class/Building-Healthy-Asset-Models-Case-Study-Existing-Asset-Recording-BIM-2018

With a special guest appearance about halfway through from probably Autodesk University’s most famous cat, Burrito. One of the dangers of presenting remotely.

Don’t Have Dimensions in Families for COBie? Don’t Worry!

So you’re new to COBie and a deadline is approaching, your favourite project BIM manager comes up to you a few hours before the deadline and tells you “We have to do dimensions.. on every element in the COBie drop. You have your dimensions ready right?”

Well there is no need to stress, as always there is potential for Dynamo to come to the rescue. I put this one together in Dynamo v1.3, but I have tested it in v2.x as well and it still works just fine, just a note though. If you save your old 1.3 graphs in 2.x, it’s now a 2.x graph forever.

The way that I approach the majority of my COBie work is through a 3D view and a schedule with a set of very hand filters, so I’ve continued down this route for my Dynamo graph and I start it off by getting all the elements in the active view – my 3D COBie view.

Just in case there is anything in the view that isn’t a family I’m getting the element type of each element, convert that value to a string and then filter the list based on the string “family”. This is because every family in the view will be prefixed with Family Type: whereas non-family objects will not.

Once we have the filtered list of families, we need to take the bounding box of each element, we do this with an Element.BoundingBox node. Using Spring Nodes, we next use Springs.Geometry.Extents to separate out each dimension individually.

The next step is a bit of simple math. I want to ensure that the length parameter is always longer than the width, so with a few if statements and some greater than and less than nodes, the top pair of nodes always provide the smaller number that will populate our width parameter and the lower set of nodes now provide the large number which will feed into our length.

Finally, we populate our parameters with the correct information. Note that your parameters must be set correctly to type parameters for this to work, if you have incorrectly made them instance parameters the script will not work but if you’re paying attention you’ll see that the hint is in the name of the parameter.

Now, don’t forget that with the COBie element data, you should be nominating dimensions inclusive of the maintenance requirements for that object, you could always add a little bit of extra fat to your dimensions, but I would highly recommend approaching COBie and BIM in general the right way and including spatial elements that indicate the overall dimensions including maintenance access similar to what is shown in the electrical switchboard below.

 

For those that want to get stared a bit quicker, I’ve provided my graph for download below

Placing Multiple Views on Sheets With Dynamo

Keeping on the theme of Dynamo and drawing setup, I had a series of MEP models that I needed to setup that had 2 views that needed to be placed on each sheet, a main view and a smaller inset view.

I started from my sheet generation Dynamo graph and ran through a number of different options to enable to graph to place multiple sheets on views in the correct location.

The method I found posed the least problems in the process was to add an extra column to my Excel file for the names of the inset views

As a bit of ground work, I needed to figure out where I wanted my views to be located, so I made up mock sheet with the views placed where I wanted them to sit on the sheet

I then threw together a quick graph that allowed me to select the viewport I’ve placed on the sheet with the Select Model Element node and running that through the Rhythm node Viewport.LocationData I can get the centre point of the viewport box. This centre point of each viewport will be the values used when we move the viewports later.

Once the viewport locations are picked up, we can get to modifying our original sheet creation graph.

The first modification that we need to make is with the additional column in Excel. Copy the original section of the graph and change the code block to 3 so that we are reading from column D of the excel file.

The next step we filter our views again, but this time we’re filtering two separate lists of views, things get a little messy but it’s still reasonably easy to manage. Note that I dropped a List.Clean node in the mix as I was having views return with no data, the List.Clean node removed empty and null values.

Remember that if you’re going to clean the list, you need to feed the other nodes with the cleaned list, do not mix and match between clean and unclean lists, otherwise you’ll have a bad time.

Now we should have two lists of element ids, one for our main views and another for our inset views.

Our main views get fed through the same series of nodes from the moving views on sheets post.

So while all of this is happening, where the output of the Sheet.ByNameNumberTitleBlockAndViews node shoots off a second time to tell our insets what sheets they need to be placed on.

The problem you will stumble into when placing views in this method is that if the sheet hasn’t yet been created, the inset views won’t be placed. The way that I decided to handle it was by using a Passthrough node from the Clockwork package. The Passthrough node implements an order of execution. It will wait for the node threaded into the waitFor input to complete before sending on the data threaded into the passThrough input.

I fed the Passthrough node with the results of moving the main viewport on each sheet, once this main views have been moved the sheet numbers are sent through to the Viewport.Create node.

Running the script, we end up with views placed exactly where they want them. The example GIF below is recorded in real time and runs for 14 seconds from start to finish, which includes checking each sheet has been correctly created.

 

This is great and all, but what happens when you have two difference types of “main” views, one where you want placed along with an inset like the above example and another where you ant the views placed centrally?

The way I found best to handle this scenario is to add a String.Contains node along with an if node to control the location of the viewports depending on the name of the view itself.

In this particular example, the names of the “main” views are being checked for if they contain the string Platform Level_ and if they do, the views are being placed centrally on the sheet, otherwise they’re being placed offset from centre to allow for the inset view to be placed on the same sheet.

The nodes labelled platform view x, main view x, platform view y and  main view y are simply code blocks that I have renamed so I know exactly what they are.

Practical Dynamo – Moving Views Based on Another View

Okay, so we’re on a roll with practical Dynamo usage. Last week we looked at placing views centrally on our sheets, but what if you didn’t want the view centrally placed? What if you wanted views placed in the same location on all sheets maybe in the top left of the page?

As always with Dyanmo, there is a solution for that. Again we’re going to be using the Rhythm custom node package to get the work done. This method requires one sheet to be used as a template that all the following sheets are based on.

In our example this time around, where we want the view located is in the top left (shown on the left) but by default Dynamo places our views in the bottom left (shown on the right)

 

This workflow can be easily integrated into our previous graph where we created new sheets in Dynamo using Excel however for this example we’re going to create a standalone graph. For this example though, it’s assumed that this time around though that you already have all the sheets and views required created.

 

First we start by taking all of our sheets, we do this simply by using Categories and then All Elements of Category, after that we get into our Rhythm nodes.

First we get a list of all of the viewports on our sheets using the Sheet.GetViewportsAndViews node. Run the list through a List.Clean node to remove the empty list entries. This leaves us with just the viewport entries.

Meanwhile, we also need to get the viewport from our template sheet. In this instance our template sheet will be drawing H101 which we’ll select using the Sheets node and then we’ll feed that node into the Sheet.GetViewportsAndViews node which are both from the Rhythm package.

And finally we feed our data lists into the Viewport.SetLocationBasedOnOther node, which again is from Rhythm. It’s as simple as that.

Hit the run button and watch the magic happen.

Update to MisterMEP Dynamo Package (0.1.4)

I’ve just published a small update to the MisterMEP Dynamo node package. A quick changelog:

  • Fixed the problem with the Set Project Information node not doing anything.
  • Removed the Lunchbox dependency from the Pipe From Strings node.

You should be able to update the package using the package manager within Dynamo.

Note that all inputs for the Set Project Information node need to be populated for it to work.

Practical Dynamo – Moving Views on Sheets

For those of you that read through my previous post last week on creating sheets using Dynamo, you might have come to the end of the post only to realise that the views haven’t placed where you want them to be on the sheets.

For example, my sheet with the automatically placed view now looks like this

The first method I’m going to use nodes from both the Rhythm and Lunchbox packages which you can download from your package manager. Simply install the latest version.

 

The Rhythm package has some super useful tools for a whole range of different actions in Revit, but today we’re going to focus on the nodes that can help us manipulate the location of our views on the sheet.

To get started, we use the Sheet.GetViewportsAndViews node, we want to feed the sheets from our previous steps into this node and the node will give you the viewports, views and schedules as separate outputs. For this exercise, we’re only interested in the viewports. As always, while you’re reading through just click on the images to see them full size.

Next you need to use the Viewport.LocationData node from Rhythm. The outputs from this node are

bBox which returns the minimum (bottom left) and maximum points (top right) of the viewport bounding box.
boxCenter which returns the centre point of the viewport bounding box
boxOutline which returns the start and end points of each side of the viewport bounding box

For this example, we’re going to use the boxCenter option because we’re going to get tricky with it a bit later on. For those earlier that were wondering what the Use Levels option actually on the nodes, as you can see in my animation it changes the level of the list that we’re working with. Without the Use Levels option you would need to either use GetItemAtIndex or List.Deconstruct to get the data that you want to manipulate.

Next use the Points.DeconstructPoint node from the Lunchbox package, this will deconstruct your point into it’s individual X, Y & Z coordinates.

Now this is where we get too smart for our own good. I want my view to be placed in the middle of the available space on my titleblock. For my particular titleblock I know that the centre point is located at 378, 297 (yours may be different) and we already have the centre of the viewport from our Rhythm node.

To find how far we need to move the viewport, we need to subtract the view X centre value from the sheet X centre and the view Y centre value from the sheet Y centre. The code block is simply values I’ve chosen, you could think of them much like a parameter in a family.

The next step is to move the views. The vector gives the distance in X & Y coordinates that the view needs to be moved, the Vector.ByCoordinates and Element.MoveByVector nodes are both standard nodes within Dynamo.

And finally, the whole thing is tied together by pushing the viewport elements into the Element.MoveByVector node via a List.GetItemAtIndex, from which we’re taking the list elements at index 2.

Now sometimes when I run this script, I’ll see the following “Attempt to modify the model out side of transaction” error.

There is a simple solution to this. Just save your changes in Dynamo, close Dynamo and then re-open. Simply run the script again and everything will work!

 

An overview of our extension to the original graph from last week, I’ve highlighted the nodes from custom packages to make things a little easier as well, Captain BIMCAD actually called me out on last week’s example for not grouping my nodes!

Practical Dynamo – Generate Sheets from Excel

I was discussing Dynamo workflows with good old Captain BIMCAD the other night and we got to the topic of project setup.

Personally I don’t use Dynamo in my everyday project setup workflow, I use Ideate BIMLink, Omnia Scope Box Synchroniser and Sheet Duplicator but if you don’t have access to this software; especially BIMLink as it’s a bit pricey, Dynamo is definitely a viable option. Here’s how to get it done.

First we need to create a list of sheets in Excel with Name and Number information. Starting with a blank workbook in Excel, create a list with sheet numbers in column A and sheet names in column B.

From here we need to generate new sheets with this Excel data. Don’t forget the File.FromPath node, you can not feed the File Path node directly into the Excel.ReadFromFile node. Note that the name of the sheet in the Excel workbook is case sensitive. You can click on the image to view in full size


 

The next step is to remove the headers from our Excel file. They’re useful to us as it makes the Excel file more readable, however they need to be removed when used in Dynamo.

To achieve this we’re doing to use 2 nodes, List.FirstItem and List.RestOfItems.

 

Next we need to transpose our list so that we can feed in our sheet details into the sheet creation node. You can see once we run the list through the List.Transpose node that we now have a list of sheet numbers and a list of sheet names which sets us up for our next step.

Most of the magic happens at the next node which is the Sheet.ByNameNumberTitleBlockAndView node.

For the node to work, we need to input the sheet name, sheet number, the titleblock family which you can see how we achieve this in the next screenshot.

While you’ve been reading, I’ve taken it upon myself to generate some views in our model and add them to our original Excel file.

We can copy what we’ve already created in Dynamo for the sheet names and numbers and we simply take index 2 from the list, giving us the view names. Note that these will be case sensitive.

The next step is to actually find those views in the model to drop onto the sheets. We do this by creating a list of all the views within the model. Take the Categories node and select Views from the drop down, feed this into the All Elements of Category node and then finally feed this into an Element.GetParameterValueByName node. For the parameter name, we want to get the value for the View Name parameter.

From here we need to search the list of view names in Excel with the list of view names in the model. To do this, use an IndexOf node.

When you run this though, you’ll end up with a result of -1 instead of a list of indices. To fix this, change the level of list in the node. To do this, click on the right arrow on the element input of the node, select Use Levels and select @L1. Run the graph again and you’ll see the list of indices.

But what happens if you have a model where you don’t have the views setup yet? In our example we don’t have a view for the cover sheet or site plan yet which is why the view name is represented as null. You can see that the null view names give a -1 index result. If we feed this data into the Sheet.ByNameNumberTitleBlockAndView node as it is, it won’t create the sheets with the null views.

You can still use the same node, but there is a trick to it.

First, grab the Manage.ReplaceNulls node. Feed the list for views into the data section.

Next, create an empty drafting view, I’m just going to leave mine as the default Drafting 1. Feed the ReplaceWith input of the Manage.RemoveNulls node with the string Drafting 1.

Now when we search our views in the model, we’ll have the correct indices returned.

But hold on there a minute! We can’t drop drafting views on multiple sheets, how is this even going to work? To be honest, I’m not quite sure why but if you feed an empty drafting view into the Sheet.ByNameNumberTitleBlockAndView node it will generate an empty sheet. Whatever the reason, that’s a win for us!

Simply feed Manage.ReplaceNulls into the Sheet.ByNameNumberTitleBlockAndView node and we’re done!

 

If you’ve had automatic run selected, you’ll have a nice set of shiny new sheets created, otherwise simply click run and watch the magic happen.

Blink and you’ll miss it!

The end result. Click the image for the full resolution version.

Site Extraction with flux.io and Dynamo

By now, most people in the industry would have heard of flux.io, a spin-off from X (formerly Google X). Recently, flux.io updated their site extraction tool which pulls data from free open source datasets, Open Street Map and NASA. When combining with Dynamo, it couldn’t be any simpler to pull in topography information to your Revit model.

So how do we get started with this new-fangled technology?

Firstly, you’ll need a flux.io account. Once you have that sorted head on over to https://extractor.flux.io/ Once there you’ll be greeted with a Google map where you can search for your location. The map system works exactly as you expect it to. Simply drag and resize the selection box around the area you’re interested in and then select what you want from the menu on the top right of your screen.

When your data is ready, you can open it in flux and review the results. You simply drag and drop your keys from the column on the left into the space on the right. You can pan, zoom and rotate your way around the the 3D preview although as someone that works in Revit and Navisworks all day long I found that the controls aren’t the easiest.

Struggling with the navigation?
right mouse button = pan
left mouse button = orbit
scroll button = zoom

So all of this is great, but how do you get this into Revit? It’s actually incredibly simple.

You will need to have both Dynamo and the flux.io plugin suite installed, but once you have those installed you’re only a few minutes away from generating a Revit topography.

To get started you will need to login to flux.io through Revit and Dynamo, if it’s your first time using flux.io you might have to approve the connection between Revit/Dynamo and flux similar to what you would when sharing account information with online services and Google or Facebook.

Find the Flux package within Dynamo and first drop in the Flux Project node.

Once you have your flux project selected, it’s just three more nodes. Drop in the Receive from Flux node and select topographic mesh from the drop down. From there push the flux topography into Mesh.VertexPositions and then finally into Topography.ByPoints

Comparing the flux topography in red against the professional survey in blue, we can see that the flux topography is no replacement for a real survey, we are looking at a 5-8m difference between the survey and the flux data. Thankfully, surveyors aren’t going to be out of the job any time soon. This is the case on the example site in Sydney only though, other sites are far more accurate depending on where the source data is coming from. Remember the flux data is coming from a combination of sources including survey from satellites which leads to varying levels of accuracy. You shouldn’t rely on open source data like this as your sole source of information. You should be referring to relevant site survey information to verify the data against.

The inaccuracy of the data though doesn’t mean that the flux data is useless. Provided that you’re able to reference the flux data with known survey data and adjust to suit, this provides an excellent opportunity for using the flux data to fill in missing information surrounding your known survey and site. You then have opportunity to use the data for visualisation in concept stages or flyover presentations of large sites or precincts.

 

Using Dynamo to Generate Pipework Hangers

If you’re finding yourself modelling more detailed models that reflect proposed fabrication or constructed works, Cesare Caoduro over at the BIM and Others blog has a great step by step tutorial on how to use Dynamo to generate Unistrut style pipework supports.

If you’re mechanical or electrical it would be quite easy to adapt the first portion of the script to generate the same type of supports for ductwork and cable trays.

You can check out Cesare’s post here