thanks @sovitek
Issue was with the duct splitting logic, i have updated the code
import clr
import math
# Revit Services
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
# Revit API
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Mechanical import *
# Geometry Conversion
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)
# ------------------------------------------------------
# 1. INPUT HANDLING (Standardizes Single vs List)
# ------------------------------------------------------
def to_list(input_item):
if isinstance(input_item, list):
return input_item
if hasattr(input_item, "__iter__") and not isinstance(input_item, str):
return input_item
return [input_item]
ducts_in = to_list(UnwrapElement(IN[0]))
points_in = to_list(IN[1])
fam_type = UnwrapElement(IN[2]) # Single Family Type
if isinstance(fam_type, list): fam_type = fam_type[0]
# ------------------------------------------------------
# 2. HELPER FUNCTIONS
# ------------------------------------------------------
def IsParallel(dir1,dir2):
if dir1.Normalize().IsAlmostEqualTo(dir2.Normalize()): return True
if dir1.Normalize().Negate().IsAlmostEqualTo(dir2.Normalize()): return True
return False
def GetDirection(faminstance):
for c in faminstance.MEPModel.ConnectorManager.Connectors:
return c.CoordinateSystem.BasisZ
def GetClosestDirection(faminstance, lineDirection):
conndir = None
flat_linedir = XYZ(lineDirection.X,lineDirection.Y,0).Normalize()
for conn in faminstance.MEPModel.ConnectorManager.Connectors:
conndir = conn.CoordinateSystem.BasisZ
if flat_linedir.IsAlmostEqualTo(conndir):
return conndir
return conndir
def SetInstanceElevation(inst, targetZ, level):
offset = targetZ - level.Elevation
for pid in (
BuiltInParameter.INSTANCE_FREE_HOST_OFFSET_PARAM,
BuiltInParameter.INSTANCE_ELEVATION_PARAM,
BuiltInParameter.FAMILY_BASE_LEVEL_OFFSET_PARAM
):
p = inst.get_Parameter(pid)
if p and not p.IsReadOnly:
try:
p.Set(offset)
return
except:
pass
def to_xyz(pt):
if hasattr(pt, "ToXyz"): return pt.ToXyz()
return pt
def get_connectors(element):
if isinstance(element, FamilyInstance):
return [c for c in element.MEPModel.ConnectorManager.Connectors]
elif isinstance(element, Duct):
return [c for c in element.ConnectorManager.Connectors]
return []
def connect_duct_to_damper(duct_elem, damper_elem):
"""Connects a duct to a damper if their connectors are co-located."""
duct_conns = get_connectors(duct_elem)
damper_conns = get_connectors(damper_elem)
for dc in duct_conns:
for fc in damper_conns:
if dc.Origin.DistanceTo(fc.Origin) < 0.01:
if not dc.IsConnectedTo(fc):
try:
dc.ConnectTo(fc)
return
except:
pass
# ------------------------------------------------------
# 3. PLACEMENT & ROTATION LOGIC
# ------------------------------------------------------
tempfamtype = None
xAxis = XYZ(1,0,0)
def placeFitting(duct, point_xyz, familytype, lineDirection):
toggle = False
isVertical = False
global tempfamtype
global xAxis
if tempfamtype == None:
tempfamtype = familytype
toggle = True
elif tempfamtype.Id != familytype.Id:
toggle = True
tempfamtype = familytype
level = duct.ReferenceLevel
width = 4
height = 4
radius = 2
round = False
connectors = duct.ConnectorManager.Connectors
for c in connectors:
if c.ConnectorType != ConnectorType.End: continue
shape = c.Shape
if shape == ConnectorProfileType.Round:
radius = c.Radius
round = True
break
elif shape == ConnectorProfileType.Rectangular or shape == ConnectorProfileType.Oval:
if abs(lineDirection.Z) == 1:
isVertical = True
yDir = c.CoordinateSystem.BasisY
width = c.Width
height = c.Height
break
# Place Instance
placementPoint = XYZ(point_xyz.X, point_xyz.Y, level.Elevation)
newfam = doc.Create.NewFamilyInstance(
placementPoint, familytype, level, Structure.StructuralType.NonStructural
)
SetInstanceElevation(newfam, point_xyz.Z, level)
doc.Regenerate()
# Rotation
transform = newfam.GetTransform()
axis = Line.CreateUnbound(transform.Origin, transform.BasisZ)
if toggle:
xAxis = GetDirection(newfam)
zAxis = XYZ(0,0,1)
if isVertical:
angle = xAxis.AngleOnPlaneTo(yDir,zAxis)
else:
angle = xAxis.AngleOnPlaneTo(lineDirection,zAxis)
ElementTransformUtils.RotateElement(doc,newfam.Id,axis,angle)
doc.Regenerate()
if lineDirection.Z != 0:
newAxis = GetClosestDirection(newfam,lineDirection)
yAxis = newAxis.CrossProduct(zAxis)
angle2 = newAxis.AngleOnPlaneTo(lineDirection,yAxis)
axis2 = Line.CreateUnbound(transform.Origin, yAxis)
ElementTransformUtils.RotateElement(doc,newfam.Id,axis2,angle2)
# Set Sizes
famconns = newfam.MEPModel.ConnectorManager.Connectors
if round:
for conn in famconns:
if not IsParallel(lineDirection,conn.CoordinateSystem.BasisZ): continue
if conn.Shape != shape: continue
try: conn.Radius = radius
except: pass
else:
for conn in famconns:
if not IsParallel(lineDirection,conn.CoordinateSystem.BasisZ): continue
if conn.Shape != shape: continue
try:
conn.Width = width
conn.Height = height
except: pass
return newfam
# ------------------------------------------------------
# 4. MAIN EXECUTION LOOP
# ------------------------------------------------------
TransactionManager.Instance.EnsureInTransaction(doc)
# Activate Family Once
if not fam_type.IsActive:
fam_type.Activate()
doc.Regenerate()
output_list = []
# Iterate through Ducts
for i, duct in enumerate(ducts_in):
placed_dampers_for_this_duct = []
try:
# Handle mapped points (Single point or List of points)
pt_data = points_in[i] if i < len(points_in) else None
# Normalize points to a list of XYZs
xyz_points = []
if isinstance(pt_data, list):
for p in pt_data: xyz_points.append(to_xyz(p))
else:
xyz_points.append(to_xyz(pt_data))
# Sort points from Start to End so we process downstream correctly
loc_curve = duct.Location.Curve
duct_start = loc_curve.GetEndPoint(0)
xyz_points.sort(key=lambda p: p.DistanceTo(duct_start))
current_duct_element = duct
# Process each point for this duct
for xyz_pt in xyz_points:
# Re-fetch curve (it changes after every split!)
curr_curve = current_duct_element.Location.Curve
lineDirection = curr_curve.Direction
proj_result = curr_curve.Project(xyz_pt)
if proj_result:
center_point = proj_result.XYZPoint
# A. PLACE & ROTATE
damper = placeFitting(current_duct_element, center_point, fam_type, lineDirection)
doc.Regenerate()
# B. GET CUT POINTS
damper_conns = get_connectors(damper)
curr_start = curr_curve.GetEndPoint(0)
sorted_conns = sorted(damper_conns, key=lambda c: c.Origin.DistanceTo(curr_start))
cut_pt_A = sorted_conns[0].Origin
cut_pt_B = sorted_conns[-1].Origin
# C. SPLIT 1
proj_A = curr_curve.Project(cut_pt_A)
p1 = proj_A.XYZPoint if proj_A else cut_pt_A
new_id_1 = MechanicalUtils.BreakCurve(doc, current_duct_element.Id, p1)
# D. SPLIT 2 (Try Both Logic)
ducts_to_check = [current_duct_element.Id, new_id_1]
final_duct_ids = []
second_split_success = False
for d_id in ducts_to_check:
d_elem = doc.GetElement(d_id)
if not d_elem: continue
if not second_split_success:
try:
c_curve = d_elem.Location.Curve
proj_B = c_curve.Project(cut_pt_B)
if proj_B:
p2 = proj_B.XYZPoint
# Prevent zero-length split errors
if not (p2.IsAlmostEqualTo(c_curve.GetEndPoint(0)) or p2.IsAlmostEqualTo(c_curve.GetEndPoint(1))):
new_id_2 = MechanicalUtils.BreakCurve(doc, d_id, p2)
final_duct_ids.append(d_id)
final_duct_ids.append(new_id_2)
second_split_success = True
continue
except: pass
final_duct_ids.append(d_id)
# E. FIND MIDDLE & DELETE
duct_objs = [doc.GetElement(ide) for ide in final_duct_ids]
mid_duct = None
side_ducts = []
closest_dist = 10000.0
for d in duct_objs:
if d is None: continue
crv = d.Location.Curve
mid_pt = (crv.GetEndPoint(0) + crv.GetEndPoint(1)) / 2
dist = mid_pt.DistanceTo(center_point)
if dist < 0.5:
if dist < closest_dist:
if mid_duct: side_ducts.append(mid_duct)
mid_duct = d
closest_dist = dist
else:
side_ducts.append(d)
else:
side_ducts.append(d)
if mid_duct: doc.Delete(mid_duct.Id)
# F. CONNECT
for d in side_ducts:
connect_duct_to_damper(d, damper)
placed_dampers_for_this_duct.append(damper)
# G. UPDATE CURRENT DUCT (For next point in loop)
# The duct continuing downstream is the one furthest from the original start
# or simply the one that isn't the "upstream" piece.
# A safe heuristic: The duct whose *Start Point* is closest to the damper is the downstream one.
downstream_duct = None
min_dist_to_damper = 10000.0
for d in side_ducts:
try:
# Check start point of this duct segment
d_start = d.Location.Curve.GetEndPoint(0)
dist = d_start.DistanceTo(center_point)
if dist < min_dist_to_damper:
min_dist_to_damper = dist
downstream_duct = d
except: pass
if downstream_duct:
current_duct_element = downstream_duct
except Exception as e:
placed_dampers_for_this_duct.append("Error: " + str(e))
output_list.append(placed_dampers_for_this_duct)
TransactionManager.Instance.TransactionTaskDone()
OUT = output_list