Beams length from outer faces

Hi all,

In my Revit model, I have an orthogonal beam network with different symbols, as shown in the image below.

To create longitudinal rebars on the top and bottom faces of each beam, I need to retrieve their total length, measured along their local longitudinal axis from outer face to outer face, as illustrated in the image.

However, since the beams use different symbol, where their b (width) and h (height) parameters are flipped along either the X or Y axis, I haven’t found a reliable way to get the total length.

I tried using Location.Curve, but it only returns the length between the beam’s endpoints, which doesn’t account for the full length from outer face to outer face?

import sys
import clr
import math
from System.Collections.Generic import IList, List

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

clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *

clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

# Collect structural framing elements (beams)
beams = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_StructuralFraming)\
    .WhereElementIsNotElementType()\
    .ToElements()

# Build dictionary with beam names and lengths


OUT = beams_length = [
    (b.Name, b.Location.Curve.ToProtoType().Length)
    for b in beams
]```

Hi, you won’t be able to get what you’re looking for from the solid (then the faces and separating them based on their vector components).

Sincerely,
Christian.stan

Hi @christian.stan

Have you an example hwo to do that?

Thanks.

Hi, there’s no shortage of examples on the forum for obtaining solids and faces from elements.

(I’ll let you search a bit…)
edit:
The vector components will be Z = 1 for the upper face and Z = -1 for the lower face.

Outgoing normal to the material (that’s cool, it’s immutable).

Sincerely,
christian.stan

setting up perimeters by picking the outer faces (4 in this case), projecting each mid point onto the faces which their normal direction is either the same or opposite of the direction of beam’s location curve, then DistanceTo(proj 1, proj 2) to get the total length. each length would be paired nicely with its corresponding beam.

@christian.stan

I tried this code where I got faces and their Normal vectors for each solid geometry representing a beam as you can see in the output below, what’s next and how can I filter them to get only the outer faces as suggested by @BimAmbit here:

import sys
import clr
import math
from System.Collections.Generic import IList, List

clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB
from Autodesk.Revit.DB.Structure import *

clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

# Collect structural framing elements (beams)
beams = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_StructuralFraming)\
    .WhereElementIsNotElementType()\
    .ToElements()


def get_side_face(beam):
    options = Options()
    options.IncludeNonVisibleObjects = False
    options.ComputeReferences = True
    options.DetailLevel = ViewDetailLevel.Fine

    geoElement = beam.get_Geometry(options)
    solid = next((g for g in geoElement if isinstance(g, Solid) and g.Volume > 0), None)
    if not solid:
        return None
    
    if solid:
        Solid_faces = [f for f in solid.Faces]
        faces_normal = [f.FaceNormal for f in Solid_faces]
        return Solid_faces,faces_normal
        
OUT = [get_side_face(beam) for beam in beams]

Thanks.

here is a possibility

edit:
I was using vectors, but if your beam is oriented at 45°, you need to test the x and y components with their sign compared to the direction of the curve, whether oriented or flipped (a bit tedious).

With surfaces (less problems after trimming up and down), except in special cases, it’s faster.

python
Solid_faces = [f for f in solid.Faces if f.FaceNormal == 1 or f.FaceNormal== -1]
Face up an down

other_faces= [f for f in solid.Faces if f.FaceNormal == 0]

cordially
christian.stan

@christian.stan
I tried this updated code, where I was able to get the side faces of beams along their locations by filtering them using FaceNormal and Area properties, as shown in the output below. However, I’m only getting the visible face in the current view!..hidden faces are not being taken into account.?.
In the image below, you can see that I’m getting only one face matching the criteria I’m targeting.

import sys
import clr
import math
from System.Collections.Generic import IList, List

clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB
from Autodesk.Revit.DB.Structure import *

clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *
from Autodesk.Revit.UI.Selection import *

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

# Collect structural framing elements (beams)
beams = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_StructuralFraming)\
    .WhereElementIsNotElementType()\
    .ToElements()


def get_side_face(beam):
    options = Options()
    options.IncludeNonVisibleObjects = False
    options.ComputeReferences = True
    options.DetailLevel = ViewDetailLevel.Fine
	
    geoElement = beam.get_Geometry(options)
    solid = next((g for g in geoElement if isinstance(g, Solid) and g.Volume > 0), None)
    if not solid:
        return None
    
    if solid:
        Solid_faces = []
        for face in solid.Faces:
            local_x = beam.Location.Curve.Direction
            product_vect = face.FaceNormal.CrossProduct(local_x)
            if product_vect.IsAlmostEqualTo(XYZ(0,0,1)) or product_vect.IsAlmostEqualTo(XYZ(0,0,-1)):
                Solid_faces.append(face)        
                faces_normal = [f.FaceNormal for f in Solid_faces]
                faces_area = [f.Area*(0.3048**2) for f in Solid_faces]
    return Solid_faces,faces_normal, faces_area
        
OUT = [get_side_face(beam) for beam in beams]

Thanks.

@c.poupin

Have you an idea how to solve this ?
Thanks

try this example, the workaround is reverse temporary all jonctions to get full beam solid uncuted

import clr
import sys
import System
from System.Collections.Generic import List, IList, Dictionary
#
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DS

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

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

#import transactionManager and DocumentManager (RevitServices is specific to Dynamo)
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

import functools

def rollbackTransaction(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        TransactionManager.Instance.ForceCloseTransaction()
        t = Transaction(doc, func.__name__)
        t.Start()
        ret = func(*args, **kwargs)
        t.RollBack()
        t.Dispose()
        return ret      
    return wrapper  

def get_solid(beam):
    options = Options()
    options.IncludeNonVisibleObjects = False
    options.DetailLevel = ViewDetailLevel.Fine
    geoElement = beam.get_Geometry(options)
    solid = next((g for g in geoElement if isinstance(g, DB.Solid) and g.Volume > 0), None)
    return solid

@rollbackTransaction   
def get_full_length_beam(elem):
    cut_elems = [doc.GetElement(xId) for xId in DB.SolidSolidCutUtils.GetCuttingSolids(elem)]
    # reverse all jonctions to get full beam solid uncuted
    for e in cut_elems:
        DB.JoinGeometryUtils.SwitchJoinOrder(doc, elem, e)
    doc.Regenerate()
    max_edge = max(get_solid(elem).Edges, key = lambda x : x.ApproximateLength )
    return max_edge.AsCurve().ToProtoType().Length
#
beam = UnwrapElement(IN[0])

OUT = get_full_length_beam(beam)

@c.poupin

I tested your code, and it works for the beams located at the ends, where I was able to obtain the desired edge with the max length except for the intersecting intermediate beams, as you can see in the image below.

Is there a way to get the original edges (where beams are intersected) from the solid uncuted or using another method?

Here is a variant using BoundingBox.ByMinimumVolume (Dynamo 2.16+)

code Python (PythonNet3), need to adapt for IronPython

import clr
import sys
import System
from System.Collections.Generic import List, IList, Dictionary
#
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DS

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

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

#import transactionManager and DocumentManager (RevitServices is specific to Dynamo)
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

import functools

def rollbackTransaction(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        TransactionManager.Instance.ForceCloseTransaction()
        t = Transaction(doc, func.__name__)
        t.Start()
        ret = func(*args, **kwargs)
        t.RollBack()
        t.Dispose()
        return ret      
    return wrapper  

def get_solid(beam):
    options = Options()
    options.IncludeNonVisibleObjects = False
    options.DetailLevel = ViewDetailLevel.Fine
    geoElement = beam.get_Geometry(options)
    solid = next((g for g in geoElement if isinstance(g, DB.Solid) and g.Volume > 0), None)
    return solid

@rollbackTransaction   
def get_full_length_beam(elem):
    curve_beam = elem.Location.Curve
    cut_elems = [doc.GetElement(xId) for xId in DB.SolidSolidCutUtils.GetCuttingSolids(elem)]
    # reverse all jonctions to get full beam solid uncuted
    for e in cut_elems:
        DB.JoinGeometryUtils.SwitchJoinOrder(doc, elem, e)
    doc.Regenerate()
    # convert to prototype and get aligned solid
    ds_solid = get_solid(elem).ToProtoType()
    ds_align_bbx = DS.BoundingBox.ByMinimumVolume([ds_solid])
    ds_align_solid = DS.BoundingBox.ToCuboid(ds_align_bbx)
    # get max edge
    max_ds_edge = max(ds_align_solid.Edges, key = lambda x : x.CurveGeometry.Length )
    return round(max_ds_edge.CurveGeometry.Length , 2)
#
beam = UnwrapElement(IN[0])

OUT = get_full_length_beam(beam)

@c.poupin

Thanks for your solution. I should mention that my main goal is to implement a pyrevit tool that automatically creates rebars for all beams in the model. (I’m currently using Dynamo just to test the logic I should later use in pyrevit.) Unfortunately, I can’t use your approach that relies on BoundingBox.ByMinimumVolume since pyrevit runs on the IronPython 2.7 engine.

In this case, could you please guide me toward a similar approach that works with IronPython, or suggest an alternative?

Thanks.

you can try grouping the edges by collinearity, then calculating the greatest distance between edge points for each group (not tested).

try this all Dynamo



You could look at FamilyInstance.GetOriginalGeometry method to get the beam before interactions with other elements. Unfortunately it is in ‘Family’ geometry space and needs to be translated

@Mike.Buttery @c.poupin

As shown in the image below, I noticed that the intersection points of beams Location.Curve @B-B and @2-2 align with the midpoints of perpendicular beams’ Location.Curve.
To address the unexpected lengths of beams @B-B and @2-2, I implemented an alternative approach: I extracted the width parameter b from the perpendicular beams at each end of the current beam. In my code, these are referred to as b_start and b_end, which I then use to generate a new_line with the expected corrected length.

When I run the code, the new_line for beam @B-B was generated correctly, with accurate coordinates for both EndPoints, as shown in the output below:

However, for beam @2-2, although the new_line had the correct length, the coordinates of both EndPoints were incorrect. I’ve struggled to resolve this and suspect that an incorrect edge direction is causing the start point of the new_line to be miscalculated.

Here my tested code:

import clr
import sys
import System
from System.Collections.Generic import List, IList, Dictionary
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

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

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

import functools

def rollbackTransaction(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        TransactionManager.Instance.ForceCloseTransaction()
        t = Transaction(doc, func.__name__)
        t.Start()
        try:
            result = func(*args, **kwargs)
        finally:
            t.RollBack()
            t.Dispose()
        return result
    return wrapper

def get_solid(beam):
    options = Options()
    options.IncludeNonVisibleObjects = False
    options.DetailLevel = ViewDetailLevel.Fine
    geoElement = beam.get_Geometry(options)
    solid = next((g for g in geoElement if isinstance(g, Solid) and g.Volume > 0), None)
    return solid

def get_beam_widths_at_midpoint_connections(single_beam, all_beams):
    loc_curve = single_beam.Location.Curve
    start = loc_curve.GetEndPoint(0)
    end = loc_curve.GetEndPoint(1)

    width_info = []
    b_start = 0.0
    b_end = 0.0

    for beam in all_beams:
        if beam.Id == single_beam.Id:
            continue
        mid = beam.Location.Curve.Evaluate(0.5, True)
        b = beam.Symbol.LookupParameter("b").AsDouble() * 0.3048

        if start.IsAlmostEqualTo(mid):
            width_info.append(("start", b))
            b_start = b
        elif end.IsAlmostEqualTo(mid):
            width_info.append(("end", b))
            b_end = b

    return width_info, b_start, b_end

@rollbackTransaction
def get_corrected_beam_length(beam, all_beams):
    beam_curve = beam.Location.Curve
    beam_length = beam_curve.Length * 0.3048

    start = beam_curve.GetEndPoint(0)
    end = beam_curve.GetEndPoint(1)

    
    cut_elems = [doc.GetElement(xId) for xId in SolidSolidCutUtils.GetCuttingSolids(beam)]
    for e in cut_elems:
        JoinGeometryUtils.SwitchJoinOrder(doc, beam, e)
    doc.Regenerate()

    solid = get_solid(beam)
    if not solid:
        return {
            "beam_id": str(beam.Id),
            "error": "No solid found"
        }

    max_edge = max(solid.Edges, key=lambda x: x.ApproximateLength)
    max_edge_length = max_edge.ApproximateLength * 0.3048
    max_edge_curve = max_edge.AsCurve()

    edge_direction = (max_edge_curve.GetEndPoint(1) - max_edge_curve.GetEndPoint(0)).Normalize()
    edge_start = max_edge_curve.GetEndPoint(0).ToPoint()
    edge_end = max_edge_curve.GetEndPoint(1).ToPoint()

    width_info, b_start, b_end = get_beam_widths_at_midpoint_connections(beam, all_beams)

    if max_edge_length > beam_length:
        total_length = max_edge_length
        new_line = max_edge_curve.ToProtoType()
    else:
        direction = (end - start).Normalize()

        total_length = beam_length + b_start / 2 + b_end / 2  
        new_start = max_edge_curve.GetEndPoint(0)
        new_end = new_start + direction.Multiply(total_length / 0.3048)

        new_line = Line.CreateBound(new_start, new_end).ToProtoType()

    return {
        "beam_id": str(beam.Id),
        "edge_start": edge_start,
        "edge_end": edge_end,
        "edge_direction": edge_direction,
        "width_info": width_info,
        "b_start": b_start,
        "b_end": b_end,
        "total_length": total_length, 
        "new_line": new_line
    }


# Collect all beams
beams = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_StructuralFraming)\
    .WhereElementIsNotElementType()\
    .ToElements()

OUT = [get_corrected_beam_length(beam, beams) for beam in beams]

Hi all, @Mike.Buttery,

I finally managed to solve my issue and found an alternative solution to @c.poupin’s approach that uses BoundingBox.ByMinimumVolume.

The idea was to create a new_line for cases where the concerned beam fails to return the expected max_edge. This was done by projecting the start point start of the beam’s beam_curve location onto the face where max_edge belongs. I used the face’s FaceNormal and projected it with a distance equal to beam_width / 2.

Then, using beam_curve.Direction, I projected the endpoints of new_line (new_start, new_end) to their final positions, with distances equal to half the width (width / 2) of the intersecting beams at the start and end points… which I refer to as w_start and w_end in my code.

The desired length in this case is:
total_length = beam_length + w_start + w_end

As you can see in the image below, I tested the code and it works for both orthogonal and skewed beams (with a slight difference for skewed beams).

here my final code:

beams_max_edge
import clr
import sys
import System
from System.Collections.Generic import List, Dictionary
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

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

clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

import functools
doc = DocumentManager.Instance.CurrentDBDocument

def rollbackTransaction(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        TransactionManager.Instance.ForceCloseTransaction()
        t = Transaction(doc, func.__name__)
        t.Start()
        try:
            result = func(*args, **kwargs)
        finally:
            t.RollBack()
            t.Dispose()
        return result
    return wrapper

def get_solid(beam):
    options = Options()
    options.IncludeNonVisibleObjects = False
    options.DetailLevel = ViewDetailLevel.Fine
    geoElement = beam.get_Geometry(options)
    return next((g for g in geoElement if isinstance(g, Solid) and g.Volume > 0), None)

def get_side_face_normal(solid, edge):
    for face in solid.Faces:
        if edge in [e for loop in face.EdgeLoops for e in loop]:
            normal = face.FaceNormal
            if not (normal.IsAlmostEqualTo(XYZ.BasisZ) or normal.IsAlmostEqualTo(-XYZ.BasisZ)):
                return normal
    return None

def get_beam_width(beam):
    return beam.Symbol.LookupParameter("b").AsDouble()

def get_intersecting_end_beams(single_beam, all_beams, tolerance=0.001):
    
    loc_curve = single_beam.Location.Curve
    w = single_beam.Symbol.LookupParameter("b").AsDouble()
    start = loc_curve.GetEndPoint(0)
    end = loc_curve.GetEndPoint(1)

    w_start = None
    w_end = None
    id_start = None
    id_end = None
    proj_pt_start = None
    proj_pt_end = None

    for beam in all_beams:
        if beam.Id == single_beam.Id:
            continue
        other_curve = beam.Location.Curve
        b = beam.Symbol.LookupParameter("b").AsDouble()

        # Project start point
        projected_start = other_curve.Project(start)
        if projected_start and projected_start.Distance < tolerance and w_start is None:
            w_start = b/2
            id_start = str(beam.Id)
            proj_pt_start = projected_start.XYZPoint.ToPoint()

        # Project end point
        projected_end = other_curve.Project(end)
        if projected_end and projected_end.Distance < tolerance and w_end is None:
            w_end = b/2
            id_end = str(beam.Id)
            proj_pt_end = projected_end.XYZPoint.ToPoint()

    return {
        "beam_id": str(single_beam.Id),
        "beam_width": w,
        "width_start": w_start,
        "width_end": w_end,
        "start_id": id_start,
        "end_id": id_end,
        "projected_start_point": proj_pt_start,
        "projected_end_point": proj_pt_end
    }


@rollbackTransaction
def get_corrected_beam_length(beam, all_beams):
    beam_curve = beam.Location.Curve
    direction = beam_curve.Direction
    beam_length = beam_curve.Length
    start = beam_curve.GetEndPoint(0)

    # Switch join order to ensure clean solid
    cut_elems = [doc.GetElement(xId) for xId in SolidSolidCutUtils.GetCuttingSolids(beam)]
    for e in cut_elems:
        JoinGeometryUtils.SwitchJoinOrder(doc, beam, e)
    doc.Regenerate()

    solid = get_solid(beam)
    if not solid:
        return None

    max_edge = max(solid.Edges, key=lambda x: x.ApproximateLength)
    max_edge_curve = max_edge.AsCurve()
    max_edge_length = max_edge_curve.Length
    face_normal = get_side_face_normal(solid, max_edge)

    # Get beam intersection data (needed in both cases)
    intersection_data = get_intersecting_end_beams(beam, all_beams, tolerance=0.001)
    beam_width = intersection_data["beam_width"]

    if max_edge_length > beam_length:
        new_line = max_edge_curve.ToProtoType()
        w_start = intersection_data["width_start"] or 0
        w_end = intersection_data["width_end"] or 0
    else:
        w_start = intersection_data["width_start"] or 0
        w_end = intersection_data["width_end"] or 0

        total_length = beam_length + w_start + w_end
        project_point = start + face_normal.Multiply(beam_width / 2)
        new_start = project_point - direction.Multiply(w_start)
        new_end = new_start + direction.Multiply(total_length)
        new_line = Line.CreateBound(new_start, new_end).ToProtoType()

    return {
        "beam_id": intersection_data["beam_id"],
        "start_beam_id": intersection_data["start_id"],
        "start_beam_width": round(w_start * 0.3048, 3),
        "end_beam_id": intersection_data["end_id"],
        "end_beam_width": round(w_end * 0.3048, 3),
        "new_line": new_line,
        "total_length": round(new_line.Length, 3)
    }

# Collect beams
beams = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_StructuralFraming)\
    .WhereElementIsNotElementType()\
    .ToElements()

filtered = [res for res in (get_corrected_beam_length(beam, beams) for beam in beams) if res is not None]

OUT = ([res["new_line"] for res in filtered], filtered)

Thanks.