Numbering Ducts

Last week I’d an encounter of the third kind with a common issue on my company. We needed to number all ducts using a code name per branch. In the past we solved it with an specific add-in or uncountable hours doing one by one by hand.

If you have done it before you know you can’t use a schedule or the mark numbers because it will not follow the order of the branch.

So I decided to spend a few hours of my weekend finding a way to do it in Dynamo at later port it to a Revit Add-In. As usual, it took me 3 days to get with the solution.

https://www.explainxkcd.com/wiki/index.php/974:_The_General_Problem

Disclaimer: Please don’t use any of this scripts in production. I can’t assure it will don’t not take control over your computer and start skynet or something.


The idea:

Having an element at the beginning of the branch and all the elements that conform it as two inicial inputs, we start iterating finding the next object that connects or clashes with it.

In the path I found different ways of doing it wrong and I would like to share them with you.

Wrong Way 1:

Extract the MEPCurve points to find coincidences but they don’t convey in the same point even if the elements are connected.

We could try to order the points by location and use is to change the order of elements. 


Problem:

The problem is with the fittings that have more than 2 connectors and the time processing inside Dynamo.

Wrong way 2 :

Next I try using the node Geometry.DoesIntersect() included in dynamo. This function iterates over a list of ducts (y) and the first duct on branch (x)

def order(x,y):
    m=0
    y.Remove(x)
    output = []
    output.append(x)
    for e in y:
        for i in y:
            opt = Options()
            g = UnwrapElement(x).Geometry(opt)
            if g.DoesIntersect(UnwrapElement(i).Geometry(opt)[0]): 
                if i not in output :
                    x=i
                    output.append(i)
                    break
        m+=1

    return output

Problems:

First some duct fittings includes more than one geometry – solid, lines –  and the method changes regarding the type of geometry used.

Besides this  problem, the connection between elements could be really close to each other but not actually touching each other.

Wrong Way 3:

The next attempt was using FilteredElementCollector with a ElementIntersectsSolidFilter. The filter is a quick way of getting a list of elements.

def order(x,y):
    n, m=0 , 0
    y.Remove(x)
    output = []
    output.append(x)
    ids = []
    for i in y:
        ids.append(ElementId(i.Id))
    ids = List[ElementId](ids)
    while len(y) > m:
        opt= Options()
        out = x.Geometry()
        for i in out:
            try:
                filter =  ElementIntersectsSolidFilter(i)
                collector = FilteredElementCollector(doc,ids).WherePasses(filter).ToElements()
                for e in collector:
                    if e not in output:
                        output.append(e)
                        ids.Remove(e.Id)
                        x = y[m]
            except:
                pass
        m+=1

    return output

Problem:

The loop While randomly clashes Dynamo when running. This could be solve using For instead but then we need two list to avoid removing items of  the one iterating.

Again, if the element does not really intersects the filter won’t detect it.

Right Way (aka less wrong):

Each element has a bounding box around that wraps up it a little bigger than the object geometry. Using FilteredElementCollector with a BoundingBoxIntersectsFilter.

Note I’ve added a function to create a collector of ElementIds in order to use it to filter just the elements I prompt the user to select.

In order to be sure the Bounding boxes intersect the elements I’ve make it a little bit bigger – +/- XYZ(1,1,1) .

def listids(y):
    output = []
    for i in y:
        output.append(ElementId(i.Id))
    # convert list into List[ElementId] to create a specific Id collections needed by the filter
    output =ids = List[ElementId](output)
    return output

def order(x,y):
    m=0
    y.Remove(x)
    output = []
    output.append(x)
    ids = listids(y)
    while len(y) > m:
        el = UnwrapElement(x)
        min = el.get_BoundingBox(doc.ActiveView).Min - XYZ(1,1,1)
        max = el.get_BoundingBox(doc.ActiveView).Max +XYZ(1,1,1)
        out = Outline(min,max)
        filter =  BoundingBoxIntersectsFilter(out)
        if len(ids) > 0:
            collector = FilteredElementCollector(doc,ids).WherePasses(filter).ToElements()
            for i in collector:
                if i not in output:
                    output.append(i)
                    ids.Remove(i.Id)
                    x = i
        m+=1
    return output


Result: 

The result of this will be a list of elements in geometrical order defined using an Element as start.

Have in mind that Bounding boxes cover all objects included in the element, even lines so you need to have a really clean family for this to work.

If is not the case you always can run the script twice, before and after the fitting in question.

Hope you find any of this helpful and let me know if you have an idea to improve it.

EDIT: Tons Typos, hate blogspot

Leave a Reply

Your email address will not be published. Required fields are marked *