Category Archives: Script

[PyRevitMEP] Shared Parameter Manager

Managing shared parameters is a pain. The standard Revit GUI makes hard to create/manage a lot of parameters. The first version of the shared parameter manager is pretty old but was not as handy to use. Making an easy to use one was a tough job.

In this script is used a WPF DataGrid which nicely displays objects data and is able to auto generate a column for each attribute of an object including drop-down menu (aka combo-box) for enumeration, checkbox for boolean, textbox for sub-objects which can be represented by a string. But for some reason you have to click once to select the row and a second time to edit (check a box, activate drop-down or text edition) which makes it pretty unusable for edition. To solve this you need manually generate all columns with a data template, add handler to make bidirectional communication between displayed data and data in the background really used, sorting etc…

Some subjects are more complex that expected like actually sorting. As often I found answers on stack overflow : https://stackoverflow.com/questions/16956251/sort-a-wpf-datagrid-programmatically

As always source code is available. You’ll find it on github : https://github.com/CyrilWaechter/pyRevitMEP

My first objective with this tool and other related tools is to be able to easily add IFC parameters to objects (families) and project in order to produce better IFC files thereby improving interoperability.

[pyRevitMEP] Transition between 2 elements

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.

[Revit] Rotate elements in any direction script

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

Enjoy !

[Revit API] Simple Modeless Form (External Event Handler) in pyRevit

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#.

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

Here is the xaml code :

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 Title="Delete things:" Height="150" Width="300" ShowInTaskbar="False" Topmost="True"
 WindowStartupLocation="CenterScreen" ScrollViewer.VerticalScrollBarVisibility="Disabled" HorizontalContentAlignment="Center">
 <StackPanel Margin="20" HorizontalAlignment="Stretch">
 <TextBlock x:Name="simple_text" Text="" Grid.Column="0" HorizontalAlignment="Center" FontWeight="Bold"/>
 <Button Content="Delete selected elements" Height="30" Margin="10,10" Click="delete_click"/>
 </StackPanel>
</Window>

Thanks a lot to all people mentioned in this article and linked article and stuffs.

[Revit] Delete parameters including hidden ones

In some case you need add or delete many parameters. Spiderinnet did many great articles about parameters, here are 2 of them :

Create shared parameter

Create project parameter

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

Cheers !

[Revit] Change fittings reference level without moving it

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

I hope you’ll enjoy it as much as I do.

[Revit] Copy shared parameters from rooms of a linked file to your own spaces

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

 

 

[Revit] Adding fluids using CoolProp

Sometimes you need to add new temperature in your project for at least 3 reasons :

  • When you use SI units Revit default fluids are bugged. I have seen them bugged for more than 5 years… Templates were made in Fahrenheit and some temperatures just don’t work when converted into °C or K.
  • Revit default fluids don’t have enough temperatures. About 5K between each and they usually just don’t fit the temperature you need in your project.
  • Water, ethylene and propylene glycol are the only default fluid available.

Here is a script example to add water to Revit. This way you can add any fluid available in CoolProp to Revit :

from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Architecture import *
from Autodesk.Revit.DB.Analysis import *
from Autodesk.Revit.DB.Plumbing import *
from Autodesk.Revit.Exceptions import *
from Autodesk.Revit.UI import TaskDialogCommonButtons
from Autodesk.Revit.UI import TaskDialogResult
import ctypes

#Load CoolProp shared library and configure PropsSI c_types units
CP = ctypes.cdll.LoadLibrary(r"E:\Cyril\Dropbox\CVC\BIM_Revit\ScriptsPython\dll\CoolProp.dll")
PropsSI = CP.PropsSI
PropsSI.argtypes = (ctypes.c_char_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_char_p, ctypes.c_double, ctypes.c_char_p)
PropsSI.restype = ctypes.c_double

uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document

#Set desired fluid, initial temperature(freezing temperature ?), desired pressure for properties call
fluid = 'Water'
t_init = 273.15
pressure = 101325

#Check if fluid_type exist and create it if not
fluid_type = FluidType.GetFluidType(doc, fluid)
if fluid_type == None:
	t = Transaction(doc, "Create fluid type")
	t.Start()
	FluidType.Create(doc, fluid)
	t.Commit()
	fluid_type = FluidType.GetFluidType(doc, fluid)

#Add new temperature with associated heat capacity and viscosity
t = Transaction(doc, "Add temperature")
t.Start()
for i in range(1,100):
	#Call CoolProp to get fluid properties and convert it to internal units if necessary 
	temperature = 273.15+i
	viscosity = UnitUtils.ConvertToInternalUnits(PropsSI('V','T',t_init+i,'P',pressure,fluid),DisplayUnitType.DUT_PASCAL_SECONDS)
	density = UnitUtils.ConvertToInternalUnits(PropsSI('D','T',t_init+i,'P',pressure,fluid),DisplayUnitType.DUT_KILOGRAMS_PER_CUBIC_METER)
	#Catching exceptions and trying to overwrite temperature if asked by user in the TaskDialog
	try:
		fluid_type.AddTemperature(FluidTemperature(temperature,viscosity,density))
	except ArgumentException:
		result = TaskDialog.Show("Error", "Temperature already exist, do you want to overwrite it ?",TaskDialogCommonButtons.Yes | TaskDialogCommonButtons.No | TaskDialogCommonButtons.Cancel, TaskDialogResult.Yes)
		if result == TaskDialogResult.Yes:
			try:
				fluid_type.RemoveTemperature(temperature)
				fluid_type.AddTemperature(FluidTemperature(temperature,viscosity,density))
			except ArgumentException:
				TaskDialog.Show("Overwrite error", "Temperature is currently in use and cannot be overwritten")
		elif result == TaskDialogResult.No:
			pass
		else:
			break
t.Commit()

Enjoy !

[Revit] ISelectionFilter example using python

Someone asked an exemple of ISelectionFilter on RevitPythonShell group almost a week ago. I don’t know if he still needs it but anyway, it’s interesting  to show this other way to filter. In this exemple we allow user only to select a duct. Let see the code snippet :

from Autodesk.Revit.UI.Selection import *

class CustomISelectionFilter(ISelectionFilter):
	def __init__(self, nom_categorie):
		self.nom_categorie = nom_categorie
	def AllowElement(self, e):
		if e.Category.Name == self.nom_categorie:
			return True
		else:
			return False
	def AllowReference(self, ref, point):
		return true

try:
	ductsel = uidoc.Selection.PickObject(ObjectType.Element,
	CustomISelectionFilter("Ducts"),
	"Select a Duct")			
except Exceptions.OperationCanceledException:
	TaskDialog.Show("Opération annulée","Annulée par l'utilisateur")

__window__.Close()

Here in action :

Here is another exemple to select only objects inherited from MEPCurve (Cable Tray, Wire, InsulationLiningBase, Duct, FlexDuct, FlexPipe, Pipe) :

from Autodesk.Revit.UI.Selection import *
class CustomISelectionFilter(ISelectionFilter):
    def __init__(self, element_class):
        self.element_class = element_class
    def AllowElement(self, e):
        if isinstance(e, self.element_class):
            return True
        else:
            return False
    def AllowReference(self, ref, point):
        return true

try:
    ductsel = uidoc.Selection.PickObject(ObjectType.Element,
    CustomISelectionFilter(MEPCurve),
    "Select a Duct")            
except Exceptions.OperationCanceledException:
    TaskDialog.Show("Opération annulée","Annulée par l'utilisateur")

__window__.Close()