Someone told me that the common add-in they were using to make transition between 2 elements was discontinued in Revit 2018. So I made mine I’ve been surprised about how easy and short it is :
with rpw.db.Transaction("Create transition"):
doc.Create.NewTransitionFitting(connector1, connector2)
So the only thing you need is to prompt user to pick 2 elements (targeting desired connectors) and it is exactly what I described in my previous article.
Even when you master it. Connecting MEP elements in order to get a fully functional system with flow and pressure loss calculation is a pain. So learn Revit API ways to connect things is a good way help you manage this.
Connecting 2 elements and possible funny possible results
Get connector manager
Connector manager is the way to retrieve connectors and all info about it. When you talk about MEP elements there is 2 main kind of elements. MEPCurve (duct, pipe, cable tray, wire etc…) and FamilyInstance (fittings, mechanical equipments, plug, lights etc…) and ConnectorManager is retrieved in 2 different ways :
MEPCurve -> MEPCurve.ConnectorManager
FamilyInstance -> FamilyInstance.MEPModel.ConnectorManager
So it is handy to have a function to retrieve it on any object :
def get_connector_manager(element):
# type: (Element) -> ConnectorManager
"""Return element connector manager"""
try:
# Return ConnectorManager for pipes, ducts etc…
return element.ConnectorManager
except AttributeError:
pass
try:
# Return ConnectorManager for family instances etc…
return element.MEPModel.ConnectorManager
except AttributeError:
raise AttributeError("Cannot find connector manager in given element")
Get a connector
As much as I know there is no way to ask user to select a connector. So you need to figure out what connector user want to designate. A good way is to check which connector is the closest (e.g. closest to user selection point or to an other object). A function to check this is then also handy :
def get_connector_closest_to(connectors, xyz):
# type: (ConnectorSet, XYZ) -> Connector
"""Get connector from connector set iterable closest to an XYZ point"""
min_distance = float("inf")
closest_connector = None
for connector in connectors:
distance = connector.Origin.DistanceTo(xyz)
if distance < min_distance:
min_distance = distance
closest_connector = connector
return closest_connector
Connecting 2 connectors
This is actually really easy. Every connector has a Connector.ConnectTo(<other_connector>) method. This method is very permissive both connectors do not need to be at the same place which is a good thing. That’t how you can connect lights, plugs etc… to their panel without any physical connection. And it can lead to funny things :
How many architect have dreamed of wireless piping and ventilation ?
Physical connection
Logical connection only for duct and pipes is funny but also unstable and it is not what we want to achieve right ? So we also want them to be physically connected.
Each connectors has it’s own coordinate system. If you do families you have probably noticed that if you don’t set it correctly it leads to strange behaviour. Basis X and Y define the plane to land the other connector. Basis Z show in which direction connection is made. When 2 connectors are connected their origin has same coordinates, XY planes are coplanar and their Z basis are in opposite direction.
Rotate
We need to handle 3 cases :
connectors z basis are collinear and :
have opposite direction -> good, nothing to do.
have same direction -> not good, we need to rotate it of 180° (π) on their X or Y axis.
connectors coordinate systems are not collinear -> not good, we need to determine the angle and axis to make them collinear and in opposite direction. To determine the angle XYZ has an handy method xyz1.AngleTo(xyz2) which return the angle between 2 vectors, we then just need add or subtract π. To determine the axis we use xyz1.CrossProduct(xyz2) which return an XYZ (vector) perpendicular to the plane defined by the 2 XYZ.
# If connector direction is same, rotate it
angle = moved_direction.AngleTo(target_direction)
if angle != pi:
if angle == 0:
vector = moved_connector.CoordinateSystem.BasisY
else:
vector = moved_direction.CrossProduct(target_direction)
try:
line = Line.CreateBound(moved_point, moved_point+vector)
moved_element.Location.Rotate(line, angle - pi)
# Revit don't like angle and distance too close to 0
except Exceptions.ArgumentsInconsistentException:
logger.debug("Vector : {} ; Angle : {}".format(vector, angle))
Move
Easy part. We just need to translate one of the object by the difference of both connectors origin.
We can now ask user to select the object+connector he wants to connect and the target object+connector.
PickObject and awesome pyRevit WarningBar
PickObject from Autodesk.Revit.UI.Selection namespace do not return an element as we could expect but a Reference. In our case, it is pretty useful as a Reference has a GlobalPoint property which stores where (XYZ) the user clicked on the object. Then we just use our get_connector_closest_to function.
I also wanted to point out one of the pyRevit feature introduced in 4.5 : the WarningBar which is a much more visible way to show user what to do. It looks like this :
It can be used very easily with in a with statement. It shows up when you enter and vanishes when you exit :
with forms.WarningBar(title="Pick element to move and connect"):
reference = uidoc.Selection.PickObject(ObjectType.Element, NoInsulation(), "Pick element to move")
Facilitate selection
You maybe noticed the NoInsulation ISelectionFilter I introduced as a parameter in PickObject prompt. It just prevent user from selecting insulation and objects that don’t have a connector manager.
Some very simple script can help you achieve tasks of missing features in Revit. As much as I know there is no way to accurately place a label to origin manually. A way to place it as well as possible is to put 0 as sample value and try to align it as well as possible by zooming. Sometimes when you activate a leader your line isn’t straight even if your annotation is perfectly perpendicular to your object just because your annotation family is not perfectly aligned on 0,0. The following very simple script will solve it your annotation family will be perfectly aligned on 0,0 :
# coding: utf8
import rpw
from rpw import DB
__doc__ = "Designed for annotation families. It moves selected annotation to center (set 0,0,0 coordinates)"
__title__ = "Center Text"
__author__ = "Cyril Waechter"
__context__ = 'Selection'
with rpw.db.Transaction():
for text_element in rpw.ui.Selection():
text_element.Coord = DB.XYZ()
The tool is available in pyRevitMEP. Just update it from pyRevit tab.
More than 2 years ago I made an article to show a way to rotate element using Revit API. Using external events in a modeless form as described in my previous article you can for example make a GUI to get axis and angle from user inputs. It is also using ISelectionFilter as described in this previous article. The new thing is that I use a standard class to store rotation parameters. This way parameters are dynamically feed to methods which are run in external events.
Let’s see this in action :
Full source code with comments (designed to be used in pyRevit) :
from revitutils import doc, uidoc
from scriptutils.userinput import WPFWindow
# noinspection PyUnresolvedReferences
from Autodesk.Revit.DB import Transaction, ElementTransformUtils, Line, XYZ, Location, UnitType, UnitUtils
# noinspection PyUnresolvedReferences
from Autodesk.Revit.UI.Selection import ObjectType, ISelectionFilter
# noinspection PyUnresolvedReferences
from Autodesk.Revit.UI import IExternalEventHandler, IExternalApplication, Result, ExternalEvent, IExternalCommand
# noinspection PyUnresolvedReferences
from Autodesk.Revit.Exceptions import InvalidOperationException, OperationCanceledException
__doc__ = "Rotate object in any direction"
__title__ = "3D Rotate"
__author__ = "Cyril Waechter"
# Get current project units for angles
angle_unit = doc.GetUnits().GetFormatOptions(UnitType.UT_Angle).DisplayUnits
def xyz_axis(element_id):
"""Input : Element, Output : xyz axis of the element"""
origin = doc.GetElement(element_id).Location.Point
xyz_direction = [XYZ(origin.X + 1, origin.Y, origin.Z),
XYZ(origin.X, origin.Y + 1, origin.Z),
XYZ(origin.X, origin.Y, origin.Z + 1)]
axis = []
for direction in xyz_direction:
axis.append(Line.CreateBound(origin, direction))
return axis
class AxisISelectionFilter(ISelectionFilter):
"""ISelectionFilter that allow only which have an axis (Line)"""
# noinspection PyMethodMayBeStatic, PyPep8Naming
def AllowElement(self, element):
if isinstance(element.Location.Curve, Line):
return True
else:
return False
def axis_selection():
"""Ask user to select an element, return the axis of the element"""
try:
reference = uidoc.Selection.PickObject(ObjectType.Element, AxisISelectionFilter(), "Select an axis")
except OperationCanceledException:
pass
else:
axis = doc.GetElement(reference).Location.Curve
return axis
class RotateElement(object):
"""class used to store rotation parameters. Methods then rotate elements."""
def __init__(self):
self.selection = uidoc.Selection.GetElementIds()
self.angles = [0]
def around_itself(self):
"""Method used to rotate elements on themselves"""
try:
t = Transaction(doc, "Rotate around itself")
t.Start()
for elid in self.selection:
el_axis = xyz_axis(elid)
for i in range(3):
if self.angles[i] == 0:
pass
else:
ElementTransformUtils.RotateElement(doc, elid, el_axis[i], self.angles[i])
t.Commit()
except InvalidOperationException:
import traceback
traceback.print_exc()
except:
import traceback
traceback.print_exc()
def around_axis(self):
"""Method used to rotate elements around selected axis"""
try:
axis = axis_selection()
t = Transaction(doc, "Rotate around axis")
t.Start()
ElementTransformUtils.RotateElements(doc, self.selection, axis, self.angles)
t.Commit()
except InvalidOperationException:
import traceback
traceback.print_exc()
except:
import traceback
traceback.print_exc()
finally:
uidoc.Selection.SetElementIds(rotate_elements.selection)
rotate_elements = RotateElement()
# Create a subclass of IExternalEventHandler
class RotateElementHandler(IExternalEventHandler):
"""Input : function or method. Execute input in a IExternalEventHandler"""
# __init__ is used to make function from outside of the class to be executed by the handler. \
# Instructions could be simply written under Execute method only
def __init__(self, do_this):
self.do_this = do_this
# Execute method run in Revit API environment.
# noinspection PyPep8Naming, PyUnusedLocal
def Execute(self, application):
try:
self.do_this()
except InvalidOperationException:
# If you don't catch this exeption Revit may crash.
print "InvalidOperationException catched"
# noinspection PyMethodMayBeStatic, PyPep8Naming
def GetName(self):
return "Execute an function or method in a IExternalHandler"
# Create handler instances. Same class (2 instance) is used to call 2 different method.
around_itself_handler = RotateElementHandler(rotate_elements.around_itself)
around_axis_handler = RotateElementHandler(rotate_elements.around_axis)
# Create ExternalEvent instance which pass these handlers
around_itself_event = ExternalEvent.Create(around_itself_handler)
around_axis_event = ExternalEvent.Create(around_axis_handler)
class RotateOptions(WPFWindow):
"""
Modeless WPF form used for rotation angle input
"""
def __init__(self, xaml_file_name):
WPFWindow.__init__(self, xaml_file_name)
self.set_image_source("xyz_img", "XYZ.png")
self.set_image_source("plusminus_img", "PlusMinusRotation.png")
# noinspection PyUnusedLocal
def around_itself_click(self, sender, e):
try:
rotate_elements.selection = uidoc.Selection.GetElementIds()
angles = [self.x_axis.Text, self.y_axis.Text, self.z_axis.Text]
for i in range(3):
angles[i] = UnitUtils.ConvertToInternalUnits(float(angles[i]), angle_unit)
rotate_elements.angles = angles
except ValueError:
self.warning.Text = "Incorrect angles, input format required '0.0'"
else:
self.warning.Text = ""
around_itself_event.Raise()
# noinspection PyUnusedLocal
def around_axis_click(self, sender, e):
try:
rotate_elements.angles = UnitUtils.ConvertToInternalUnits(float(self.rotation_angle.Text), angle_unit)
rotate_elements.selection = uidoc.Selection.GetElementIds()
except ValueError:
self.warning.Text = "Incorrect angles, input format required '0.0'"
else:
around_axis_event.Raise()
RotateOptions('RotateOptions.xaml').Show()
Warning : Some change in Revit API and Revit behaviour makes this article obsolete with newer version of Revit. I advise you to install pyRevitMEP extension and study updated code : FormExternalEventHandler.pushbutton. For usage in your script I recommend to use my CustomizableEvent from pyRevitMEP library.
I struggled for a while to make a modeless form. Why did I need it ? Because each time I was trying to get user to select object after WPF appear I was going out of Revit API thread and got this very common exception «Autodesk.Revit.Exceptions.InvalidOperationException: Attempting to create an ExternalEvent outside of a standard API execution». As Jeremy Tammik says :
One of the most frequently raised questions around the Revit API is how to drive Revit from outside, e.g., from a separate thread, a modeless dialogue, or a stand-alone executable.
I have read many examples on the subject. Most on them were in C#.
Some post pointed out the ModelessDialog sample available in Revit SDK : it helps me to understand how it should behave but it was still a bit too complex for my C# knowledge.
This AEC DevBlog article make me wonder «Is my code really working or I missed something and I was only lucky in situation I tested ?»
So I made a very simple form to make a very simple ExternalEventHandler sample as pyRevit script. It will help me and I hope it will help some hackers to struggle less than I did.
Let’s start with common import statement using built-in pyRevit utils :
# noinspection PyUnresolvedReferences
from Autodesk.Revit.UI import IExternalEventHandler, ExternalEvent
# noinspection PyUnresolvedReferences
from Autodesk.Revit.DB import Transaction
# noinspection PyUnresolvedReferences
from Autodesk.Revit.Exceptions import InvalidOperationException
from revitutils import selection, uidoc, doc
from scriptutils.userinput import WPFWindow
__doc__ = "A simple modeless form sample"
__title__ = "Modeless Form"
__author__ = "Cyril Waechter"
Then let’s write a simple function we want to execute modeless (here it just delete selected elements) :
# Simple function we want to run
def delete_elements():
t = Transaction(doc, "Failing script")
t.Start()
for elid in uidoc.Selection.GetElementIds():
print elid
doc.Delete(elid)
t.Commit()
And now come the new magic thing that let you enter in a valid Revit API context. The «ExternalEvent» class with his «IExternalEventHandler» class :
# Create a subclass of IExternalEventHandler
class SimpleEventHandler(IExternalEventHandler):
"""
Simple IExternalEventHandler sample
"""
# __init__ is used to make function from outside of the class to be executed by the handler. \
# Instructions could be simply written under Execute method only
def __init__(self, do_this):
self.do_this = do_this
# Execute method run in Revit API environment.
def Execute(self, uiapp):
try:
self.do_this()
except InvalidOperationException:
# If you don't catch this exeption Revit may crash.
print "InvalidOperationException catched"
def GetName(self):
return "simple function executed by an IExternalEventHandler in a Form"
# Now we need to make an instance of this handler. Moreover, it shows that the same class could be used to for
# different functions using different handler class instances
simple_event_handler = SimpleEventHandler(delete_elements)
# We now need to create the ExternalEvent
ext_event = ExternalEvent.Create(simple_event_handler)
Let’s do a simple form so easily created thanks to pyRevit in order to use our new toy :
# A simple WPF form used to call the ExternalEvent
class ModelessForm(WPFWindow):
"""
Simple modeless form sample
"""
def __init__(self, xaml_file_name):
WPFWindow.__init__(self, xaml_file_name)
self.simple_text.Text = "Hello World"
self.Show()
def delete_click(self, sender, e):
# This Raise() method launch a signal to Revit to tell him you want to do something in the API context
ext_event.Raise()
# Let's launch our beautiful and useful form !
modeless_form = ModelessForm("ModelessForm.xaml")
import clr
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Plumbing import PipeSegment
clr.AddReference("RevitServices")
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
#Les entrées effectuées dans ce noeud sont stockées sous forme de liste dans les variables IN.
dataEnteringNode = IN
elem = doc.GetElement(IN[0])
TransactionManager.Instance.EnsureInTransaction(doc)
for dn, di, de in zip(IN[1], IN[2], IN[3]):
DN = UnitUtils.ConvertToInternalUnits(dn , DisplayUnitType.DUT_MILLIMETERS)
Di = UnitUtils.ConvertToInternalUnits(di , DisplayUnitType.DUT_MILLIMETERS)
De = UnitUtils.ConvertToInternalUnits(de , DisplayUnitType.DUT_MILLIMETERS)
try:
elem.AddSize(MEPSize(DN,Di,De,True,True))
except:
elem.RemoveSize(DN)
elem.AddSize(MEPSize(DN,Di,De,True,True))
#Affectez la sortie à la variable OUT.
OUT = IN