Distribute Pipes Equally

This is a draft script to Equally Space Selected Pipes (if parallel).
I was asked by of of the students on how to fix this.

The script will
- Prompt User to Select Pipes
- Check if they are Parallel
- Sort Them based on their location
- Distribute pipes equally between First and Last one based on CenterLine distance.

Here is the Draft Tool in Action:


Here is the code:

# -*- coding: utf-8 -*-
__title__ = 'Distribute Pipes'
__author__ = 'Erik Frits'

#⬇️IMPORTS
# ==================================================
import math
from pyrevit import forms
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI.Selection import ObjectType, ISelectionFilter


#📦VARIABLES
# ==================================================
doc       = __revit__.ActiveUIDocument.Document  # type: Document
uidoc     = __revit__.ActiveUIDocument  # type: UIDocument
selection = uidoc.Selection  # type: Selection

from pyrevit import script
output = script.get_output()

#🥚 CLASSES
# ==================================================

# Class
class CustomFilter(ISelectionFilter):
    def AllowElement(self, element):
        return element.Category and element.Category.BuiltInCategory == BuiltInCategory.OST_PipeCurves

#🧬 FUNCTIONS

# Function to retrieve insulation and diameter of a pipe
def get_pipe_insulation_and_diameter(pipe):
    insulation_param     = pipe.LookupParameter("Insulation Thickness")
    insulation_thickness = insulation_param.AsDouble() if insulation_param else 0.0
    diameter_param       = pipe.LookupParameter("Outside Diameter")
    diameter             = diameter_param.AsDouble() if diameter_param else 0.0
    return insulation_thickness, diameter



#╔╦╗╔═╗╦╔╗╔
#║║║╠═╣║║║║
#╩ ╩╩ ╩╩╝╚╝
# ==================================================
#1️⃣ Get Pipes
with forms.WarningBar(title='Select Pipes'):
    custom_filter = CustomFilter()
    pipe_refs = selection.PickObjects(ObjectType.Element, custom_filter, "Select pipes by dragging a box.")
    pipes     = [doc.GetElement(ref) for ref in pipe_refs]

# Ensure Pipes
if len(pipes) < 2:
    forms.alert("Error: Select at least two pipes.", title="Selection Error", exitscript=True)






#2️⃣ Ensure Pipes Are Parallel
def get_pipe_direction(pipe):
    """Returns the normalized direction vector of a pipe."""
    location_curve = pipe.Location
    if isinstance(location_curve, LocationCurve):
        curve = location_curve.Curve
        direction = curve.GetEndPoint(1) - curve.GetEndPoint(0)
        return direction.Normalize()
    return None

def are_vectors_parallel(v1, v2, tolerance=1e-5):
    """Checks if two vectors are parallel within a given tolerance."""
    cross_prod = v1.CrossProduct(v2).GetLength()
    return cross_prod < tolerance

# Get the direction of the first pipe as a reference
reference_direction = get_pipe_direction(pipes[0])

# Check if all pipes are parallel to the reference direction
all_parallel = True
for pipe in pipes[1:]:
    direction = get_pipe_direction(pipe)
    if not are_vectors_parallel(reference_direction, direction):
        all_parallel = False
        break

# Check Parallels
if not all_parallel:
    forms.alert('Selected pipes are not parallel.', exitscript=True)



#3️⃣ Order Pipes
def get_pipe_midpoint(pipe):
    """Returns the midpoint of a pipe's LocationCurve."""
    location_curve = pipe.Location
    if isinstance(location_curve, LocationCurve):
        curve = location_curve.Curve
        start_point = curve.GetEndPoint(0)
        end_point   = curve.GetEndPoint(1)
        midpoint    = (start_point + end_point) / 2
        return midpoint
    return None

def project_point_onto_line(point, line_point, line_direction):
    """Projects a point onto a line defined by line_point and line_direction."""
    vector = point - line_point
    projection_length = vector.DotProduct(line_direction)
    projected_point = line_point + line_direction.Multiply(projection_length)
    return projected_point

# Use the direction vector perpendicular to the pipes for sorting
# For simplicity, find a vector perpendicular to the pipe direction
if abs(reference_direction.Z) < 1.0:
    # Pipes are not vertical; use Z-axis as up vector
    up_vector = XYZ.BasisZ
else:
    # Pipes are vertical; use X-axis
    up_vector = XYZ.BasisX

perpendicular_direction = reference_direction.CrossProduct(up_vector).Normalize()

# Get the projected positions of the pipes onto the perpendicular direction
pipe_positions = []
for pipe in pipes:
    midpoint = get_pipe_midpoint(pipe)
    # Project the midpoint onto a line along the perpendicular direction
    projected_point = project_point_onto_line(midpoint, XYZ(0, 0, 0), perpendicular_direction)
    distance_along_perpendicular = projected_point.DotProduct(perpendicular_direction)
    pipe_positions.append((pipe, distance_along_perpendicular))

# Sort the pipes based on their position along the perpendicular direction
pipe_positions_sorted = sorted(pipe_positions, key=lambda x: x[1])

# Extract the sorted pipes
pipes_ordered = [item[0] for item in pipe_positions_sorted]

# Verify the order Manually
for idx, (pipe, position) in enumerate(pipe_positions_sorted):
    # print("Pipe {}: Position along perpendicular = {}".format(idx, position))
    print(output.linkify(pipe.Id))



#4️⃣ Space Pipes (Center Line)
# Get the positions along the perpendicular direction
positions    = [pos for _, pos in pipe_positions_sorted]
first_position = positions[0]
last_position = positions[-1]
total_distance = last_position - first_position

number_of_intervals = len(pipes_ordered) - 1

if number_of_intervals == 0:
    print("Only one pipe selected. No spacing needed.")
else:
    equal_spacing = total_distance / number_of_intervals

    t = Transaction(doc, "Equal Spacing of Pipes")
    t.Start()

    for idx, (pipe, _) in enumerate(pipe_positions_sorted):
        # Calculate the target position along the perpendicular direction
        target_position = first_position + equal_spacing * idx
        # Calculate the displacement along the perpendicular direction
        displacement = target_position - positions[idx]
        # Compute the translation vector
        translation_vector = perpendicular_direction.Multiply(displacement)
        # Move the pipe
        ElementTransformUtils.MoveElement(doc, pipe.Id, translation_vector)

    t.Commit()
    print("Pipes have been spaced equally.")

⌨️ Happy Coding!
Erik Frits