Spliting curves for creating beams rebars

Hi all, @c.poupin

In the code below, I am trying to create rebars for intersecting beams by splitting and overlapping each beam’s curve so that the Bar Length parameter of every rebar does not exceed 12.00 m, including hooks, for that I need to handle three main cases in the split_curve function, wher I defined three maximum straight segments to manage these scenarios:

  • case1 : total_length <= unit_length2

    • a single U-shaped rebar is generated
  • case2 : unit_length2 < total_length <= ref_length

    • two overlapped L-shaped rebar are generated where four sub-cases are considered:
      • sub_case1: total_length <= unit_length1: to avoid a single L-shaped rebar the main curve is shortened with a minimun distance eqaul to tol_near_full allowing the creation of a second overlapping L-shaped rebar
      • sub_case2: total_length == ref_length: Exactly two overlapping L-shaped rebars are generated, each with a length of 12.00 m.
      • sub_case3: ref_length - total_length <= tol_near_full: Similar to Sub-case 2, I force the creation of two overlapping L-shaped rebars, each 12.00 m long, this is achieved by extending the second rebar at its start point by a distance equal to tol_near_full and in this case the overlap length becomes lr + tol_near_full
      • sub_case4: ref_length - total_length > tol_near_full:two overlapping L-shaped rebar are generated where the straight segment for the first rebar is equal to unit_length1
  • case3: a list of multiples overlapping rebars are generated where:

    • At least one intermediate straight rebar of 12.00 m is created
    • The first rebar is L-shaped, with hook_90 at the start and None at the end.
    • The last rebar is L-shaped, with None at the start and hook_90 at the end.
    • the same rules in sub_case2 and sub_case3 are applied for the last bar in the list.

In the attached revit model below, I tested the code for the specific cases sub_case1 and sub_case2 where the results are as follow:

  • For both beams of type 400 x 800 along the X direction:

    • As expected, two overlapping L-shaped rebars (top and bottom) are generated, following the logic of Sub-case 1.
  • For both beams of type 300 x 600 along the Y direction:

    • Two overlapping L-shaped rebars are generated for the top rebars, following Sub-case 4.
    • No bottom rebar is generated, although I am handling Sub-case 2, where I expect exactly two overlapping L-shaped rebars, each 12.00 m long.

My question:

Could this issue be caused by floating-point imprecision when evaluating
total_length == ref_length, or is it more likely due to incorrect logic in my implementation?

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

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

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

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

doc = DocumentManager.Instance.CurrentDBDocument

# System.Core
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
import functools

# collecting rebar type in the active document
rebar_types = (
    FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Rebar)
    .WhereElementIsElementType()
    .WherePasses(ElementClassFilter(RebarBarType))
    .ToElements()
)

top_bar_type = next((r for r in rebar_types if r.LookupParameter('Diamètre de barre').AsValueString() == "14 mm"), None)
btm_bar_type = next((r for r in rebar_types if r.LookupParameter('Diamètre de barre').AsValueString() == "16 mm"), None)

hook_types = list(FilteredElementCollector(doc).OfClass(RebarHookType).ToElements())

standard_hook_types = [hk for hk in hook_types if hk.Style == RebarStyle.Standard]

hook_90 = next((h for h in standard_hook_types if Element.Name.GetValue(h) == "Standard - 90 deg."), None)

# collecting beams cover BuiltInParameter
cover_faces = {
    "Top": BuiltInParameter.CLEAR_COVER_TOP,
    "Bottom": BuiltInParameter.CLEAR_COVER_BOTTOM,
    "Other": BuiltInParameter.CLEAR_COVER_OTHER
}

cover_types = list(FilteredElementCollector(doc).OfClass(RebarCoverType).ToElements())

def decoTransaction(commit):
    def subDecoTransaction(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            TransactionManager.Instance.ForceCloseTransaction()
            t = Transaction(doc, func.__name__)
            t.Start()
            ret = func(*args, **kwargs)
            if commit:
                t.Commit()
            else:
                t.RollBack()            
            t.Dispose()
            return ret      
        return wrapper  
    return subDecoTransaction
    
    
    
    
def get_cover_type(distance):
    for ct in cover_types:
        if abs(ct.CoverDistance - distance) < 1e-6:
            return ct
    name = "Enrobage_{:.0f}cm".format(distance / 0.0328084)
    new_ct = RebarCoverType.Create(doc, name, distance)
    cover_types.append(new_ct)
    return new_ct

@decoTransaction(commit = True)
def set_beam_covers(beam, top, btm, side):
    cover_cm = {"Top": top, "Bottom": btm, "Other": side}
    cover_ft = {face: cover_cm[face] * 0.0328084 for face in cover_cm}
    for face, bip in cover_faces.items():
        param = beam.get_Parameter(bip)
        if param and param.StorageType == StorageType.ElementId:
            ct = get_cover_type(cover_ft[face])
            if ct:
                param.Set(ct.Id)
            else:
                print("test")

def get_beam_covers(beam):
    result = {}
    for face, bip in cover_faces.items():
        param = beam.get_Parameter(bip)
        if param and param.StorageType == StorageType.ElementId:
            ct = doc.GetElement(param.AsElementId())
            if isinstance(ct, RebarCoverType):
                result[face] = ct.CoverDistance
    return result

# getting beams solid geometry

def get_solid(elem):
    opt = Options()
    opt.ComputeReferences = True
    opt.IncludeNonVisibleObjects = True
    geoSet = elem.get_Geometry(opt)
    for geo in geoSet:
        if isinstance(geo, Solid) and geo.Volume > 0:
            return geo
    for geo in geoSet:
        if isinstance(geo, GeometryInstance):
            geoSetInst = geo.GetInstanceGeometry()
            for geoI in geoSetInst:
                if isinstance(geoI, Solid) and geoI.Volume > 0:
                    return geoI
    return None

#Retrieving the beam’s side face to extract its longest curve.
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 is_curves_parallel(curve1, curve2, tolerance=1e-6):
    v1 = curve1.Direction.Normalize()
    v2 = curve2.Direction.Normalize()
    return v1.CrossProduct(v2).GetLength() <= tolerance

# Retrieving data from the ends of each intersected beam to generate the main rebar curve.
def get_intersecting_end_beams_data(single_beam, all_beams, tolerance=1e-3):
    
    loc_curve = single_beam.Location.Curve
    w = single_beam.Symbol.LookupParameter("b").AsDouble()
    h = single_beam.Symbol.LookupParameter("h").AsDouble()

    covers = get_beam_covers(single_beam)
    top_cover = covers["Top"]
    btm_cover = covers["Bottom"]
    side_cover = covers["Other"]

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

    w_start = w_end = None
    start_side_cover = end_side_cover = None
    perpendicular_beams = []

    for beam in all_beams:
        if beam.Id == single_beam.Id:
            continue

        other_curve = beam.Location.Curve

        # Skip parallel
        if is_curves_parallel(loc_curve, other_curve):
            continue

        results = clr.Reference[IntersectionResultArray]()
        result = loc_curve.Intersect(other_curve, results)

        if result != SetComparisonResult.Overlap:
            continue

        intersection_points = []
        interResult = results.Value
        intersection_points.append(interResult[0].XYZPoint)

        if not intersection_points:
            continue

        # Handle each intersection point
        for pt in intersection_points:
            b = beam.Symbol.LookupParameter("b").AsDouble()

            other_covers = get_beam_covers(beam)
            other_side_cover = other_covers["Other"]

            if start.DistanceTo(pt) < tolerance and w_start is None:
                w_start = b / 2
                start_side_cover = other_side_cover
            elif end.DistanceTo(pt) < tolerance and w_end is None:
                w_end = b / 2
                end_side_cover = other_side_cover


    return {
        "beam_width": w,
        "beam_height": h,
        "beam_top_cover": top_cover,
        "beam_btm_cover": btm_cover,
        "beam_side_cover": side_cover,
        "width_start": w_start,
        "width_end": w_end,
        "beam_start_side_cover": start_side_cover,
        "beam_end_side_cover": end_side_cover
    }

# transform function to create top and bottom rebar curve for each beam
def curve_multiply_offset(curve, vector, intersection_data):
    h = intersection_data["beam_height"]
    top_cover = intersection_data["beam_top_cover"]
    btm_cover = intersection_data["beam_btm_cover"]
    side_cover = intersection_data["beam_side_cover"]

    curve = curve.CreateTransformed(Transform.CreateTranslation(XYZ(0, 0, -1).Multiply(top_cover)))
    vertical_offset = h - (top_cover + btm_cover)
    trans = Transform.CreateTranslation(vector.Negate().Multiply(side_cover))
    top_curve = curve.CreateTransformed(trans)
    bottom_trans = Transform.CreateTranslation(XYZ(0, 0, -1).Multiply(vertical_offset))
    btm_curve = top_curve.CreateTransformed(bottom_trans)
    btm_curve = Line.CreateBound(btm_curve.GetEndPoint(1), btm_curve.GetEndPoint(0))

    return top_curve, btm_curve

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

    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

    beam_width = intersection_data["beam_width"]
    beam_height = intersection_data["beam_height"]
    top_cover = intersection_data["beam_top_cover"]
    btm_cover = intersection_data["beam_btm_cover"]
    side_cover = intersection_data["beam_side_cover"]
    w_start = intersection_data["width_start"] or 0
    w_end = intersection_data["width_end"] or 0
    start_side_cover = intersection_data["beam_start_side_cover"] or 0
    end_side_cover = intersection_data["beam_end_side_cover"] or 0

    # Identify main face and edge
    max_edge = max(solid.Edges, key=lambda x: x.ApproximateLength)
    max_edge_curve = max_edge.AsCurve()
    max_edge_direction = max_edge_curve.Direction
    max_edge_length = max_edge_curve.Length
    face_normal = get_side_face_normal(solid, max_edge)
    if face_normal is None:
        return None

    if max_edge_length > beam_length:
        if not max_edge_curve.GetEndPoint(0).Z == beam_curve.GetEndPoint(0).Z:
            max_edge_curve = Line.CreateBound(
                XYZ(max_edge_curve.GetEndPoint(0).X, max_edge_curve.GetEndPoint(0).Y, beam_curve.GetEndPoint(0).Z),
                XYZ(max_edge_curve.GetEndPoint(1).X, max_edge_curve.GetEndPoint(1).Y, beam_curve.GetEndPoint(1).Z)
            )
        if not max_edge_direction.IsAlmostEqualTo(direction):
            max_edge_curve = Line.CreateBound(max_edge_curve.GetEndPoint(1), max_edge_curve.GetEndPoint(0))

        if max_edge_length <= 1e-9:
            return None

        new_start = max_edge_curve.Evaluate(start_side_cover / max_edge_length, True)
        new_end = max_edge_curve.Evaluate(1 - (end_side_cover / max_edge_length), True)
        new_line = Line.CreateBound(new_start, new_end)

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

    # Generate top/bottom offset curves
    top_curve, btm_curve = curve_multiply_offset(new_line, face_normal, intersection_data)

    return top_curve, btm_curve, face_normal
    

# splitting top/bottom curve for each beam 
def split_curve(curve, bar_type, hook_type):
    # the main curve length
    total_length = curve.Length
    print(total_length * 0.3048)
    # bar diameter
    d = bar_type.BarModelDiameter
    # standard hook Bend Diameter
    D = bar_type.StandardHookBendDiameter
    # hook extension length
    l = hook_type.GetHookExtensionLength(bar_type)
    # hook total length
    b = l + D / 2 + d
    # referance length to generate a staright bar without hooks.  
    unit_length = 12 / 0.3048
    # referance length to generate an L-shapped rebar.  
    unit_length1 = (12 / 0.3048) - b - ((D + d) * math.pi / 4) + D + 2 * d
    # referance length to generate an U-shapped rebar.
    unit_length2 = (12 / 0.3048) - 2 * b - ((D + d) * math.pi / 2) + 2 * D + 4 * d
    # overlap distance
    lr = 50 * d
    # tolerence distance needed to forcing creation of an L-shapped rebar with 12.00m long
    tol_near_full = 0.15 / 0.3048
    # referance length to exactely generate two overlaped L-shapped rebar with 12.00m long
    ref_length = 2 * unit_length1 - lr
    print(ref_length * 0.3048)

    direction = curve.Direction
    curves = []

    start = curve.GetEndPoint(0)
    end = curve.GetEndPoint(1)
    k = 0
    n = int((total_length-unit_length1-lr) // unit_length)
    
    # Case 1: a single U-shaped rebar is generated
    if total_length <= unit_length2:
        curves.append(curve)

    # Case 2: Two overlapped L-shaped rebars are generated
    elif unit_length2 < total_length <= ref_length:
        if total_length <= unit_length1:
            end1 = start + direction.Multiply(total_length - tol_near_full)
            start2 = end1 - direction.Multiply(lr)  
            new_line1 = Line.CreateBound(start, end1)
            new_line2 = Line.CreateBound(start2, end)
            curves.append(new_line1)
            curves.append(new_line2)
            
        else:
            if ref_length - total_length <= 1e-3:
                end1 = start + direction.Multiply(unit_length1)
                start2 = end1 - direction.Multiply(lr)
                new_line1 = Line.CreateBound(start, end1)
                new_line2 = Line.CreateBound(start2, end)
                curves.append(new_line1)
                curves.append(new_line2)
                
            elif ref_length - total_length <= tol_near_full:
                end1 = start + direction.Multiply(unit_length1)
                start2 = end1 - direction.Multiply(lr + tol_near_full)
                new_line1 = Line.CreateBound(start, end1)
                new_line2 = Line.CreateBound(start2, end)
                curves.append(new_line1)
                curves.append(new_line2)                
            else:
                end1 = start + direction.Multiply(unit_length1)
                start2 = end1 - direction.Multiply(lr)
                new_line1 = Line.CreateBound(start, end1)
                new_line2 = Line.CreateBound(start2, end)
                curves.append(new_line1)
                curves.append(new_line2)


    return curves


# Collect beams
beams = FilteredElementCollector(doc)\
    .OfCategory(BuiltInCategory.OST_StructuralFraming)\
    .WhereElementIsNotElementType()\
    .Where(System.Func[DB.Element, System.Boolean](lambda b: b.StructuralUsage == StructuralInstanceUsage.Girder))\
    .ToList()

btm_curves = []
top_curves = []
normal_vects = []

for beam in beams:
    
    set_beam_covers(beam, 3, 3, 5)

for beam in beams:
    
    intersection_data = get_intersecting_end_beams_data(beam, beams)
    result = get_corrected_beam_length(beam, intersection_data, beams)
    if not result:
        continue
    top_curve, btm_curve, normal_vect = result
    
    top_curves.append(split_curve(top_curve, top_bar_type, hook_90))
    
    btm_curves.append(split_curve(btm_curve, btm_bar_type, hook_90))

    normal_vects.append(normal_vect)

# function to create rebars
def create_rebars_from_segments(beam, top_sublist, btm_sublist, normal_vect, top_bar_type, btm_bar_type, hook_type,n):
    rebars = []
    rebars_length = []
    direction = beam.Location.Curve.Direction
    width = beam.Symbol.LookupParameter("b").AsDouble()
    covers = get_beam_covers(beam)
    side_cover = covers["Other"]
    first_top = top_sublist[0]
    
    if normal_vect.CrossProduct(direction).IsAlmostEqualTo(XYZ(0, 0, -1)):
        norm  = first_top.Direction.CrossProduct(XYZ.BasisZ)
        hook_orient = RebarHookOrientation.Right
        
    else:
        norm = first_top.Direction.CrossProduct(XYZ.BasisZ).Negate()
        hook_orient = RebarHookOrientation.Left
        

    l1 = len(top_sublist)
    l2 = len(btm_sublist)

    for idx,top in enumerate(top_sublist):
        
        if l1 <= 2:  
            if l1 ==1:
                start_hook = hook_type
                end_hook = hook_type
            else:  
                if idx == 0:
                    start_hook = hook_type
                    end_hook = None
                else:
                    start_hook = None
                    end_hook = hook_type
        else:  
            if idx == 0:
                start_hook = hook_type
                end_hook = None
            elif idx == l1 - 1:
                start_hook = None
                end_hook = hook_type
            else:
                start_hook = None
                end_hook = None

        # --- create top rebar ---
        top_list = List[Curve]()
        top_list.Add(top)
        top_rebar = Rebar.CreateFromCurves(
            doc,
            RebarStyle.Standard,
            top_bar_type,
            start_hook,
            end_hook,
            beam,
            norm,
            top_list,
            hook_orient,
            hook_orient,
            True, True
        )
        top_rebar.GetShapeDrivenAccessor().SetLayoutAsFixedNumber(
            n, width - 2 * side_cover, True, True, True
        )
        rebars.append(top_rebar)
        rebars_length.append(top_rebar.get_Parameter(BuiltInParameter.REBAR_ELEM_LENGTH).AsDouble() * 304.8)

    for idx,btm in enumerate(btm_sublist):
        
        if l2 <= 2: 
            if l2 ==1:
                start_hook = hook_type
                end_hook = hook_type
            else:  
                if idx == 0:
                    start_hook = hook_type
                    end_hook = None
                else:
                    start_hook = None
                    end_hook = hook_type
        else:  
            if idx == 0:
                start_hook = hook_type
                end_hook = None
            elif idx == l2 - 1:
                start_hook = None
                end_hook = hook_type
            else:
                start_hook = None
                end_hook = None
        # --- create bottom rebar ---
        btm_list = List[Curve]()
        btm_list.Add(btm)
        btm_rebar = Rebar.CreateFromCurves(
            doc,
            RebarStyle.Standard,
            btm_bar_type,
            start_hook,
            end_hook,
            beam,
            norm,
            btm_list,
            hook_orient,
            hook_orient,
            True, True
        )
        btm_rebar.GetShapeDrivenAccessor().SetLayoutAsFixedNumber(
            n, width - 2 * side_cover, True, True, True
        )
        rebars.append(btm_rebar)
        rebars_length.append(btm_rebar.get_Parameter(BuiltInParameter.REBAR_ELEM_LENGTH).AsDouble() * 304.8)

    return rebars, rebars_length
    
all_beam_rebars = []
all_beam_rebars_length = []
with Transaction(doc, "Create Beam Rebars from Segments") as t:
    t.Start()
    for beam, top_curves_list, btm_curves_list, normal_vect in zip(beams, top_curves, btm_curves, normal_vects):
        beam_rebars, rebars_length = create_rebars_from_segments(
            beam, top_curves_list, btm_curves_list, normal_vect,
            top_bar_type, btm_bar_type, hook_90, 3
        )
        all_beam_rebars.extend(beam_rebars)
        all_beam_rebars_length.extend(rebars_length)
    t.Commit()

OUT = all_beam_rebars, all_beam_rebars_length, [c.ToProtoType() for lst in top_curves for c in lst], [c.ToProtoType() for lst in btm_curves for c in lst]

beams_rebar_test4.rvt (3.6 MB)

Any help would be appreciated

Thanks.

Hey,

It looks like you have a lot of progress and you are nearly there! Great work!

I find that if I am getting unexpected results, I need to focus on commenting my code so I am clear on exactly what everything is doing.

I try to output variable values throughout the code, at places where I need to make sure that everything is working as expected, to ensure that I am getting the correct results.

If the code is too long or unmageable, I might even split it down into different python nodes, and then I can move on with confidence.

Once the code is working as I expect, I will go back and modularise it, add in functions, classes etc. to make it run more smoothly. I find it quite hard to debug while building with lots of functions.

It can be laborious to make all the processes, but ultimately if it gets your code working, it was worth it!

I hope that helps,

Mark

3 Likes

Yes. You should ALWAYS round before doing a comparison after any type of unit or source comparison, and as you’re using Python you are converting from the C# Revit API (which itself is a conversion from true native Revit) to Python in Dynamo. Otherwise it isn’t a question of IF floating point math will burn you, but rather WHEN floating point math will burn you.

2 Likes

Hi @Mark.Ackerley

First of all, have you had a chance to test the code to see how it behaves, and could you suggest a possible solution?

Do you think my explanation of the different parts of the code was not clear enough?

I agree with you…the code was indeed too long, so I had to split it into different Python nodes to make it more readable and to isolate the section where I’m encountering the issue, which should help with debugging.

Thanks.

Hi @jacob.small

In the split_curve function I replaced this line

if ref_length - total_length <= 1e-3:
.
By this line

if round(ref_length - total_length, 3) <= 1e-3:

However I end up with the same issue!

Thanks.

Might not have been impacting this case directly, but always best practice to round before comparing.

Sadly I’m on a phone and we’re looking at… Well it’s more lines of code then I can count in this UI. It might be best to write each of these ‘cases’ as a single function, test those one by one on a particular beam to make sure the inner workings are set up right, and then build the if statement logic gates but have them just log which case would be used to an output.

By pulling the parts pulled out into modules you can test individual components instead of limiting it to the output alone.