Efficiently Determining Entry and Exit Points of a Line in a Bounding Box

Hey folks,

I need to determine the entry and exit points of a line within a bounding box.

In Dynamo, I achieved this using a polysurface generated from the bounding box, but this approach is too heavy for processing 20,000 lines and 6,000 bounding boxes. I would like to implement this in either C# or Python, preferably using a mathematical approach if no built-in methods are available.

The bounding boxes come from an Excel file, stored in a structured list with volume names and min/max XYZ coordinates. These are not actual Revit objects, and since they use survey coordinates, I first translate them to internal coordinates.

I tried using ChatGPT to generate a mathematical solution, which produced some results. However, the function sometimes returns extra points and even detects intersections when a line is entirely inside the box. Additionally, I struggle to understand the mathematical logic behind it.

import clr

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

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

# Get the document
doc = DocumentManager.Instance.CurrentDBDocument


from math import sqrt

# Function to compute distance between two points
def distance(p1, p2):
    return sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2 + (p1[2] - p2[2])**2)

# Function to check if a point is inside a bounding box
def is_inside_bbox(point, bbox_min, bbox_max):
    return all(bbox_min[i] <= point[i] <= bbox_max[i] for i in range(3))

# Function to check if an entire line is inside the bounding box
def is_line_inside_bbox(p1, p2, bbox_min, bbox_max):
    return (is_inside_bbox(p1, bbox_min, bbox_max) and
            is_inside_bbox(p2, bbox_min, bbox_max))

# Function to check line-box intersections
def line_bbox_intersection(p1, p2, bbox_min, bbox_max, epsilon=1e-6):
    # Skip the line if it is fully inside the box
    if is_line_inside_bbox(p1, p2, bbox_min, bbox_max):
        return []

    direction = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
    t_min, t_max = 0, 1  # Parameter range for line segment

    for i in range(3):  # Iterate over x, y, z dimensions
        if direction[i] != 0:  # Avoid division by zero
            t1 = (bbox_min[i] - p1[i]) / direction[i]
            t2 = (bbox_max[i] - p1[i]) / direction[i]

            t_entry, t_exit = min(t1, t2), max(t1, t2)
            t_min, t_max = max(t_min, t_entry), min(t_max, t_exit)

            if t_min > t_max:  # No valid intersection
                return []

    intersection_points = []

    if 0 < t_min <= 1:
        entry_point = XYZ(p1[0] + t_min * direction[0],
                       p1[1] + t_min * direction[1],
                       p1[2] + t_min * direction[2])
        if distance(entry_point, p1) > epsilon and distance(entry_point, p2) > epsilon:
            intersection_points.append(entry_point)

    if 0 < t_max <= 1 and t_min != t_max:
        exit_point = XYZ(p1[0] + t_max * direction[0],
                      p1[1] + t_max * direction[1],
                      p1[2] + t_max * direction[2])
        if distance(exit_point, p1) > epsilon and distance(exit_point, p2) > epsilon:
            intersection_points.append(exit_point)

    return intersection_points


# Input: List of Duct Elements and Bounding Box Data
ducts = UnwrapElement(IN[0])  # List of ducts from Dynamo input
volumes_data = IN[1]  # List of bounding box data (7 lists: Name, MinX, MinY, MinZ, MaxX, MaxY, MaxZ)

# Parse bounding boxes
bounding_boxes = []
for i in range(len(volumes_data[0])):  # Assuming first list has names
    bbox_min = volumes_data[1][i]
    bbox_max = volumes_data[2][i]
    bounding_boxes.append((bbox_min, bbox_max))

# Process ducts
procesed_ducts = []
results = []
for duct in ducts:
    location = duct.Location
    if isinstance(location, LocationCurve):
        curve = location.Curve
        p1 = (curve.GetEndPoint(0).X, curve.GetEndPoint(0).Y, curve.GetEndPoint(0).Z)
        p2 = (curve.GetEndPoint(1).X, curve.GetEndPoint(1).Y, curve.GetEndPoint(1).Z)

        intersections = []
        for bbox_min, bbox_max in bounding_boxes:
            points = line_bbox_intersection(p1, p2, bbox_min, bbox_max)
            if points:
                intersections.append(points)
        if intersections:
            procesed_ducts.append(duct)
            results.append(intersections)

# Output to Dynamo
OUT = procesed_ducts, results

What I Want to Achieve:

  1. Primary Goal:
  • Given a list of ducts and another list containing bounding boxes (with volume names and min/max XYZ coordinates), I need a list of ducts and a list of entry and exit points corresponding to each duct.
  1. Secondary Goal:
  • Structure the bounding box data efficiently.
  • Optimize checks to determine entry and exit points only for nearby boxes.
  • Explore additional optimizations.

Any suggestions or efficient approaches in C# or Python would be highly appreciated!

Hey,

You could definitely build yourself a zero touch but BiMorph have some efficient nodes which might do what you need?

Hope that helps,

Mark

Hi Mark,

Those nodes work fine, but my solution needs to be independent. So, I generally keep everything inside a single Python node or a C# add-in. I never use packages because they create dependencies that require installation. Users often overlook this—whenever a new user comes in, they simply say it’s not running without reading the documentation.

I’ve created over 150 scripts used by different people, and supporting them is challenging. So I prefer to implement solutions in C# from the start. If I initially develop something in Python, I can later convert it to C#. The end goal is to have the solution in C#, allowing me to refine the program with meaningful error messages that guide users on what went wrong and how to troubleshoot effectively.

Sticking to pure Revit API then as that is your preference, a skilled user should be able to do all of the following, if not build this map on their own. Chat GPT won’t save you here though, so if you have questions it will likely be a few dozen orders of magnitude to use the nodes noted above.

For each BB:

  • Build a mass as a solid.
  • Use a filtered element collector to get all the ducts which intersect with a given mass.
  • For each duct:
    • Extract the location curve
    • Intersect the location curve with the mass.
    • If the intersection is shorter than the original curve check if the start and end point are the same as the original start and end, adding any differing points to the output as they are an entry or exit point.
1 Like

HI @jacob.small
Thanks for you thoughts.
Sorry for the late reply, I was on a trip to the mountains.

This approach is inefficient, and I have already tried it. I used BoundingBoxIntersectsFilter in conjunction with ElementCategoryFilter to get the ducts intersecting a bounding box. However, when the number of boxes exceeds 3000 (I don’t know the exact threshold where it starts to crash, but overall, I have 6000 boxes, and I am quite sure that using BoundingBoxIntersectsFilter for a large number—probably 500+—causes Revit to become unresponsive), it crashes.

Another issue is that even if I manage to get the ducts for each bounding box using a mathematical intersection method (which is more performant), I would still be stuck at the point where I need to generate a solid from the bounding box. Solids consume a lot of memory, in my experience. I have tried creating solids for 3000 boxes in Dynamo, but it didn’t work. I haven’t tested this in the Revit API, but I suspect it might fail as well. This is the main reason I wanted to generate a mathematical function that calculates the entry and exit points instead.

The program doesn’t end there. After obtaining this list, it needs to start splitting the ducts. If a duct has insulation or lining, my custom function first stores the original values of the insulation or lining in a temporary variable and then assigns them to newly created lining and insulations. This is necessary because Revit does not provide default support for insulation and lining like it does for ducts. I demonstrated this in a separate video on LinkedIn.

Overall, this is a complex problem that requires an efficient approach. I already have a solution in Dynamo that does the following:

  1. The user provides a filter to reduce the number of boxes to around 200.
  2. The program lists the ducts that might be intersecting with these volumes.
  3. All the boxes are converted to PolySurfaces.
  4. For each duct, all PolySurfaces are iterated, and all the intersection points for that duct are stored in a separate list.
  5. Duplicates are pruned, and points that are too close to the start and end points are removed.
  6. The Revit API is then used to split the ducts and copy insulation and lining properties.

This works well, but the catch is that the user may need to run the script 10 times to process all the boxes.

Odd to hear that… I used it on an massive project with an extra zero in terms of the number of spaces and collected not just ducts but everything. It always ran in less than 30 seconds if memory serves. The filtered element collector should be exceedingly fast in all cases. Perhaps it’s got something to do with your coding method or the impact of other efforts?

Do be careful with bounding boxes as false positives can cause issues with things showing as in a box which are not. You are limited to perfect squares aligned to the internal XY axis, which may be fine today but could break the first time you have say a Y shaped building.

In any case…

While one automation to do all the work might be desirable, it may be best to put a ‘split’ parameter on the found elements, and use a second graph to split each element with a parameter value other than 0, 1, or null at that value? This would mean graph A would find the overlap and write the parameter to the overlap, and then graph B would automate the result.

If you really want to stick to one button then working in the context of the API directly (C# preferred, Python second) and managing your loops effectively is recommended from a performance and reliability standpoint based on the sales you’re indicating