17 août 2025

[Dynamo += Python] Création de raccords et d'accessoires MEP




Automatiser le placement de vannes dans Revit ?
Voici un exemple de script pour créer des raccords et accessoires sur des 
canalisations MEP 


#DynamoBIM #Python #RevitAPI  #PythonNet3 #MEP #Fittings #Vectors #Connectors #AutodeskExpertElite #AutodeskCommunity 

if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )

Le script présenté ci-dessous automatise, l’insertion de raccord MEP (de type T) ou d’accessoires (vanne, clapet…) sur des canalisations MEP Revit à des points donnés, en coupant les tronçons, orientant correctement les familles et en connectant leurs connecteurs aux éléments hôtes.

Entrées :
- IN[0] : éléments MEP linéaires (canalisations) avec une géométrie de type LocationCurve
- IN[1] : liste de listes de points (un sous-ensemble de points par élément)
- IN[2] : type de famille (raccord en T ou accessoire). 

Sorties :
- OUT : [liste des éléments d’origine (coupés), liste des familles créées]

Fonctions :
- get_shape : pour le contrôle de la structure des listes d’entrée
- get_order_connectors : ordonne les connecteurs des 3 tronçons pour créer un raccord en T valide
- get_FamilyInstance_direction : détermine la direction principale d’un accessoire via ses connecteurs pour l’orienter grâce au système de cordonnées des connecteurs  

Flux Principal :
Pour chaque élément MEP et sa liste de points
  • Coupe de la canalisation au point 
  • Calcul d’un axe de rotation en fonction de l’orientation (verticale/horizontale)
  • Cas d'un Raccord en T :
    • Duplication temporaire d’un tronçon pour créer la branche
    • Rotation de 90° de la branche autour de l’axe
    • Tri des connecteurs (A, B, C) puis création du raccord en T (NewTeeFitting) et application du type
    • Suppression de la branche temporaire

  • Cas d'un Accessoire :
    • Placement de l’élément (NewFamilyInstance) au niveau du point
    • Détermination de la direction de la famille via ses connecteurs, alignement angulaire avec l’axe de la conduite (gestion verticale/horizontale)
    • Connexion des connecteurs de l’accessoire aux deux tronçons

Notes :
  • Le code Python a été tester avec le dernier moteur Python "DynamoPythonNet3", il est possible que vous deviez faire des ajustements si vous choisissez un autre moteur.

  • Le script ne fonctionne pas pour les autres types de catégorie (exemple : Chemin de Câbles)

  • Une attention particulière est faite sur la détermination de l'angle de rotation à appliquer :
    • positif ou négatif selon le résultat de la composante Z du Produit Vectoriel (direction instance et direction de la canalisation)
    • attention à la direction de la ligne d'axe de la méthode ElementTransformUtils.RotateElement(), si la composante Z est négative, le sens de rotation sera inversé




code

    
import clr
import sys
import System
from System.Collections.Generic import List
#
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import Autodesk.DesignScript.Geometry as DS

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

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

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

import math

def get_shape(lst, shape=()):
    """
    returns the shape of nested lists 
    """
    if not hasattr(lst, "__iter__") or isinstance(lst, (str, System.String)):
        # base case
        return {"shape":shape, "ndim":len(shape)}
    if hasattr(lst[0], "__iter__") and not isinstance(lst[0], (str, System.String)):
        l = len(lst[0])
    shape += (len(lst), )
    # recurse
    shape = get_shape(lst[0], shape)
    return shape #{"shape":shape, "ndim":len(shape)} 

def get_order_connectors(elemA, elemB, elemC):
    test = elemA
    lst = [elemA, elemB, elemC]
    # create group by direction
    groupA = [i for i in lst if abs(test.Location.Curve.Direction.CrossProduct(i.Location.Curve.Direction).Z) < 0.001]
    groupB = [i for i in lst if abs(test.Location.Curve.Direction.CrossProduct(i.Location.Curve.Direction).Z) > 0.001]
    lstGroup = [groupA, groupB]
    # sort list of group by len 
    lstGroup.sort(key = len, reverse = True)
    # flat the group : the last element is the branch
    lst_sort_elem = sum(lstGroup, [])
    lst_curve_elems = [i.Location.Curve for i in lst_sort_elem]
    orderConnectors = []
    # get closest connectors
    for idxA, elem in enumerate(lst_sort_elem):
        other_curve = [c for idxB, c in enumerate(lst_curve_elems) if idxA != idxB][0]
        print(other_curve)
        closest_connectors = sorted(elem.ConnectorManager.Connectors, key = lambda c :  other_curve.Project(c.Origin).Distance)
        orderConnectors.append(closest_connectors[0])
    #
    return orderConnectors

def get_FamilyInstance_direction(elem):
    connectors = [c for c in elem.MEPModel.ConnectorManager.Connectors if c.Domain in [Domain.DomainPiping, Domain.DomainHvac]]
    i = 0
    groups_arr = [ [connectors.pop(0)] ]
    while connectors and i < 10:
        i += 1
        con = connectors.pop(0)
        is_placed = False
        for idx, group in enumerate(groups_arr):
            # if connector CoordinateSystem is parrallel add to current group
            if con.CoordinateSystem.BasisZ.CrossProduct(group[-1].CoordinateSystem.BasisZ).GetLength() < 0.02:
                group.append(con)
                is_placed = True
        if not is_placed :
            groups_arr.append([con])
    #
    print(f"{groups_arr=}")
    main_connectors = next((g for g in groups_arr if len(g) == 2), None)
    if main_connectors is not None:
        pta, ptb = sorted([c.Origin for c in main_connectors], key = lambda p : p.X)
        pta, ptb = [c.Origin for c in main_connectors]
        return DB.Line.CreateBound(pta, ptb).Direction.Normalize(), main_connectors
    return None, []

def toList(x):
    if isinstance(x, (list, dict)) or \
            (hasattr(x, "GetType") and x.GetType().GetInterface("ICollection") is not None):
        return x
    else : return [x]

#Preparing input from dynamo to revit
lst_elemCurves = [x for x in toList(UnwrapElement(IN[0])) if isinstance(x.Location, LocationCurve)]
lst_elemCurveIds = [x.Id for x in lst_elemCurves]
arr_arr_DSPoints = toList(IN[1])
arr_arr_DSPoints[0] = toList(arr_arr_DSPoints[0])
#
# check if data input structure is correct
shape_Elem = get_shape(lst_elemCurves)
shape_DSPoints = get_shape(arr_arr_DSPoints)
# 
if shape_Elem["ndim"] == 1\
            and shape_DSPoints["ndim"] == 2\
            and  shape_Elem["shape"][0] == shape_DSPoints["shape"][0]:
    pass
else:
    raise Exception("wrong list input structure")
    
fitting_type = UnwrapElement(IN[2])
out_fitting = []
debug = []
#
is_tee = fitting_type.Family.get_Parameter(BuiltInParameter.FAMILY_CONTENT_PART_TYPE).AsInteger() == 6
#Do some action in a Transaction
TransactionManager.Instance.EnsureInTransaction(doc)
for array_DSPts, elem in zip(arr_arr_DSPoints, lst_elemCurves):
    print("pass")
    # get datas from Curve 
    c_system = list(elem.ConnectorManager.Connectors)[0].CoordinateSystem
    curve_elem = elem.Location.Curve.Clone()
    curve_direction = curve_elem.Direction.Normalize()
    level = doc.GetElement(elem.LevelId)
    #
    array_pts = [p.ToXyz() for p in array_DSPts]
    array_pts.sort(key = lambda p : p.DistanceTo(curve_elem.GetEndPoint(0)) )
    elemId = elem.Id
    #
    for pt in array_pts:
        newfam = None
        try:
            newId = MechanicalUtils.BreakCurve(doc, elemId, pt)
        except Exception as ex:
            print(ex)
            newId = PlumbingUtils.BreakCurve(doc, elemId, pt)
        #
        doc.Regenerate()
        elemA = doc.GetElement(elemId)
        curveA = elemA.Location.Curve
        elemB = doc.GetElement(newId)
        curveB = elemB.Location.Curve
        try :
            v_axis = curve_direction.CrossProduct(XYZ.BasisX)
            axis = Line.CreateUnbound(pt, v_axis)
        except:
            v_axis = curve_direction.CrossProduct(XYZ.BasisY)
            axis = Line.CreateUnbound(pt, v_axis)
        # tee creation
        if is_tee :
            print("is tee")
            # create temapary the third element (branch)
            elemCId = list(DB.ElementTransformUtils.CopyElement(doc, elemA.Id, XYZ.Zero))[0]
            elemC = doc.GetElement(elemCId)
            # rotate if verticale
            if abs(c_system.BasisZ.Z) > 0.99:
                axis = Line.CreateUnbound(pt, c_system.BasisY)
                DB.ElementTransformUtils.RotateElement(doc, elemCId, axis, math.pi / 2)
            else:
                DB.ElementTransformUtils.RotateElement(doc, elemCId, axis, math.pi / 2)
            #
            doc.Regenerate()
            conA, conB, conC = get_order_connectors(elemA, elemB, elemC)
            newfam = doc.Create.NewTeeFitting(conA, conB, conC)
            newfam.ChangeTypeId(fitting_type.Id)
            out_fitting.append(newfam)
            # delete the temp branch
            doc.Delete(elemCId)
        #
        # accessory creation
        else:
            if not fitting_type.IsActive:
                fitting_type.Activate()
            newfam = doc.Create.NewFamilyInstance(pt, fitting_type, level, StructuralType.NonStructural)
            out_fitting.append(newfam)
            doc.Regenerate()
            # get direction of familyInstance by connectors position
            v1, connectors = get_FamilyInstance_direction(newfam)
            print(f"v1={v1.ToString()}")
            if v1 is not None:
                # check if vectors are Clockwise
                factor = 1 if v1.CrossProduct(curve_direction).Z > 0.01 else -1
                angle = v1.AngleTo(curve_direction)
                # align direction of familyinstance
                ## rotate if verticale
                if abs(c_system.BasisZ.Z) > 0.99 :
                    DB.ElementTransformUtils.RotateElement(doc, newfam.Id, axis, factor * angle )
                    factorX = 1 if c_system.BasisX.CrossProduct(XYZ.BasisX).Z < 0 else -1
                    DB.ElementTransformUtils.RotateElement(doc, newfam.Id, curve_elem, factorX * c_system.BasisX.AngleTo(XYZ.BasisX))
                ## if horizontal redifine the axis 
                else:
                    axis = Line.CreateBound(pt, pt + XYZ(0,0,1))
                    DB.ElementTransformUtils.RotateElement(doc, newfam.Id, axis, factor * angle )
                #
                doc.Regenerate()
                # connect connectors to pipes/ducts
                for con in connectors:
                    for curv_, elem_ in [[curveA, elemA], [curveB, elemB]]:
                        interR = curv_.Project(con.Origin)
                        #print(curv_.ComputeNormalizedParameter(interR.Parameter))
                        #
                        if interR is not None and 0.005 < curv_.ComputeNormalizedParameter(interR.Parameter) < 0.995:
                            closest_connectors = sorted(elem_.ConnectorManager.Connectors, key = lambda c :  c.Origin.DistanceTo(con.Origin))
                            closest_connector = closest_connectors[0]
                            closest_connector.ConnectTo(con)
                            closest_connector.Origin = con.Origin


TransactionManager.Instance.TransactionTaskDone()
OUT = lst_elemCurves, out_fitting



IA Note :
ont été générés par IA :
  • l'image 
  • la musique de vidéo

0 commentaires:

Enregistrer un commentaire