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.
This batch renaming utility elaboration has been a long journey. I did a simple one long time ago which was supporting only 1 renaming pattern : http://pythoncvc.net/?p=27
Principle
We use parameters available in our views to rename them in a programmatic and logic way. My old script was using an home parameter for discipline and sub-discipline which is set by view template, level and scope box name to make it unique as much as possible.
Objectives
Flexibility : clients, contractors or someone else may ask a specific naming pattern for views. So pattern need to be easily adaptable and savable.
Multilingual : as always, to avoid language dependent behaviour, when it is possible built in parameter name or shared parameter Guid shall be used to retrieve values.
Human readable : parameters storage type may be text, number, id etc… and have a better human readable AsValueString (it is the case for scope boxe).
Free separators choice : we should be able to freely choose and change separator, prefix, suffix between parameters
Flexible selection of view to rename : be able to rename X views then select Y other views and rename it and so on.
Code behind in short
Renaming part
I was thinking about common file re-namers like Ant Renamer. User is able to type parameters as a simple string and add anything he wants between them. The string looks like the old string formatting style with %s %d etc…
So my first attempt was to use python string format like "string{Guid.<Guid>}_{bip.<BuiltInParameterName>_{<parameter_name>}".format(parameter_by_Guid.value, parameter_by_builtinparameter.value, parameter_by_name.value). That was working but somehow it felt weird and slow.
I finally used regular expressions which are very much used for this kind of task in a very efficient way (spelling correction, retrieve a phone number, verify that an email entered by a user looks like one etc…).
So we needed a regular expression to retrieve parameters by Guid :
# Create a regular expression to retrieve Guid (including constructor) in a string
parameterGuidRegex = re.compile(r'''
Guid\( # Guid constructor. Example : Guid("F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4")
[\" | \'] # Single or double quote required in Guid constructor
([0-9a-f]{8} # First 8 hexa
(-?[0-9a-f]{4}){3} # Optional separator + 4 hexa 3 times
-?[0-9a-f]{12}) # Optional separator + 12 hexa
[\" | \'] # Single or double quote required in Guid constructor
\) # Close parenthesis necessary to build a Guid
''', re.IGNORECASE | re.VERBOSE)
… by BuiltInParameter (way far simpler) :
# Create a regular expression to retrieve BuiltInParameter in a string
parameterBipRegex = re.compile(r"bip\((\w+)\)")
… and finally by name :
# Create a regular expression to retrieve named parameters in a string
parameterNameRegex = re.compile(r"name\(([\w\s]+)\)") # Retrieve group(1)
There is mainly 3 class of view (excluding schedules, legends etc…) : ViewPlan, View3D, ViewSection. Each kind don’t have the same available parameter. For example there is no level parameter in 3D and section views. So 3 pattern definition was required to batch rename views. To show available parameters to user a sample view of each class is retrieved and parameters Guid, BuiltInParameterName, and Name are stored as a dictionary.
To help user add parameter to pattern the best way. A function try to retrieve Guid else BuiltInParameter name else name.
Another function replace pattern by their associated parameter name and value to show user a preview.
A last function is in charge of actually renaming views.
GUI part
A WPF GUI splitted in 4 part, 3 for each view classes, 1 for saving and validation options.
Saving/loading part
Thanks again to Ehsan, there is a very easy way to save script parameter to pyRevit config file. Don’t forget to encode and decode your string !
my_config = this_script.config
# Then in WPF class we have following method :
def save_config_click(self, sender, e):
for view_class in self.view_class_dict.keys():
view_class_checkbox = eval('self.cb_{}'.format(view_class))
if view_class_checkbox.IsChecked:
pattern_textbox = eval('self.{}_pattern'.format(view_class))
setattr(my_config, view_class, pattern_textbox.Text.encode('utf8'))
this_script.save_config()
But I wanted also to be able to save to a parameter for project specific naming rules. Unfortunately with Revit API it is still currently not possible to create a project parameter which is not shared. So based on Spiderinnet samples I wrote a function to create a shared parameter and store configuration in it as a string which is formatted in a dictionary/json style.
When script is launched. It tries to load configuration from project parameter (as project specific case should be use first) else from pyRevit config file (the way you name it on your office) else from default values (which are for most people just samples).
Modeless / ExternalEvent part
To be able to rename successively multiple selections of views it was mandatory to use a modelless WPF window and so to use ExternalEvent for any Transaction to Revit : create shared parameter, save to parameter, rename views. I described how to do so in a previous article but as there was 3 different action to make it was a good idea to enhance it. I made 2 class to be able to execute any function or method through the external event using *args and *kwargs :
class CustomizableEvent:
def __init__(self):
self.function_or_method = None
self.args = ()
self.kwargs = {}
def raised_method(self):
self.function_or_method(*self.args, **self.kwargs)
def raise_event(self, function_or_method, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.function_or_method = function_or_method
custom_event.Raise()
customizable_event = CustomizableEvent()
# Create a subclass of IExternalEventHandler
class CustomHandler(IExternalEventHandler):
"""Input : function or method. Execute input in a IExternalEventHandler"""
# Execute method run in Revit API environment.
# noinspection PyPep8Naming, PyUnusedLocal
def Execute(self, application):
try:
customizable_event.raised_method()
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 an handler instance and his associated ExternalEvent
custom_handler = CustomHandler()
custom_event = ExternalEvent.Create(custom_handler)
Thanks to his creator Ehsan Iran-Nejad and all credits we have a great tool named pyRevit. This tool allows us to easily make python scripts for Revit and distribute them via the extensions manager.
And it is exactly what I did by creating pyRevitMEP extension which has been added by Ehsan to pyRevit extensions manager last week-end :
pyRevitMEP philosophy : encourage MEP people to group-up and develop common tools. You want to contribute by coding : send a pull request on github or more (do not hesitate to contact me). If you prefer to manage your own extension, add your tools in the /pyRevitMEP.tab/ and follow instructions on pyRevit blog to add it to extensions manager. You want to contribute but don’t know how, check FAQ page. License is and will remain open source. It is currently under GNU GPL v3.0 License.
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
Some Revit addins and extensions are adding many parameters to you project. Some are not even visible to the user. It means that you have to use API to remove it. In most common case you’ll never see it unless you use Revit Lookup. But when you export your model or do a duct pressure loss report it appears on it. So I made a script to get quickly rid of unwanted parameters (hidden or not).
from Autodesk.Revit.DB import *
from System import Guid
uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application
#Retrieve all parameters in the document
params = FilteredElementCollector(doc).OfClass(ParameterElement)
filteredparams = []
#Store parameters which has a name starting with "magi" or "MC"
for param in params:
if param.Name.startswith(("magi", "MC")): #startswith method accept tuple
filteredparams.append(param)
print param.Name #To check if a parameter in the list is not supposed to be deleted
#Delete all parameters in the list
t = Transaction(doc, "Delete parameters")
t.Start()
for param in filteredparams:
doc.Delete(param.Id)
t.Commit()
A common pain in Revit is to manage object’s reference level :
If you change a duct/pipe reference level. It stays at the same location which is great.
If you change any fitting/accessory reference level. It move at the same offset on the defined level. It generates many errors when you change a level elevation during a project…
from Autodesk.Revit.DB import *
uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
getselection = uidoc.Selection.GetElementIds
#Get current selection and store it
selection = getselection()
#Ask user to pick an object which has the desired reference level
def pickobject():
from Autodesk.Revit.UI.Selection import ObjectType
__window__.Hide()
picked = uidoc.Selection.PickObject(ObjectType.Element, "Sélectionnez la référence")
__window__.Show()
return picked
#Retrieve needed information from reference object
ref_object = doc.GetElement(pickobject().ElementId)
ref_level = ref_object.ReferenceLevel
ref_levelid = ref_level.Id
t = Transaction(doc, "Change reference level")
t.Start()
#Change reference level and relative offset for each selected object in order to change reference plane without moving the object
for e in selection:
object = doc.GetElement(e)
object_param_level = object.get_Parameter(BuiltInParameter.FAMILY_LEVEL_PARAM)
object_Level = doc.GetElement(object_param_level.AsElementId())
object_param_offset = object.get_Parameter(BuiltInParameter.INSTANCE_FREE_HOST_OFFSET_PARAM)
object_newoffset = object_param_offset.AsDouble() + object_Level.Elevation - ref_level.Elevation
object_param_level.Set(ref_levelid)
object_param_offset.Set(object_newoffset)
t.Commit()
It is very useful to be able to copy shared parameter from a linked file to you own file especially if you want to make a stable link to an external database or maintain data consistency between different models.
The Autodesk Space Naming Utility is very useful but limited to room/space number and name. With the following script methodology you are able to copy any shared or built in parameter.
Unfortunately, Space property «Room» return None when the room is in a linked file. Hopefully there is a workaround which has been highlighted here by Revitalizer. You can use the GetRoomAtPoint as followed (same in the other way to get space from room).
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Mechanical import Space
from Autodesk.Revit.DB.Architecture import Room
from Autodesk.Revit.UI import UIApplication
from System import Guid
uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
getselection = uidoc.Selection.GetElementIds
app = __revit__.Application
#reference desired link
for e in app.Documents:
if e.Title == "[LinkName].rvt":
lien = e
#Reference the parameter you want to copy by GUID or BuiltInParameter
paramguid = Guid("88938699-b86d-4efa-aeb6-ce66d17d7755")
t = Transaction(doc, "Copy shared parameter from rooms to spaces")
t.Start()
#Get all spaces in the current project
for space in FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_MEPSpaces):
if space.Location != None: #Check if the space is placed
#Get room at space insertion point location. Credit to Revitalizer : https://forums.autodesk.com/t5/revit-api/mep-space-class-room-property-returns-null-with-linked-models/td-p/3650268
room = lien.GetRoomAtPoint(space.Location.Point)
#Check if there is actually a room at this location
if room != None:
#Call desired parameter in both room and space
spaceparam = space.get_Parameter(paramguid)
roomparam = room.get_Parameter(paramguid)
try:
#Try to set space parameter value with room parameter value. It can fail if value is null for exemple
spaceparam.Set(roomparam.AsString())
except:
pass
t.Commit()
__window__.Close()