Beams length from outer faces

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.