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")