Friday, August 12, 2011

Using the Snoop Objects dialog from RevitLookup in RevitPythonShell

One of my main goals with RevitPythonShell is to provide a handy environment for exploring the Autodesk Revit API. But anyone seriously interested in exploring the Revit API should also be familiar with RevitLookup, formally known as RvtMgdDbg. This tool can be found in the SDK and includes a handy command for "snooping" a selected element called "Snoop Current Selection...". I use this so often, that I created a keyboard shortcut to invoke it. It provides vital information for working with Revit objects - information you probably want access to while inside an interactive RPS shell!

Unfortunately, you can't execute plugins while inside the RPS shell. Well, not through the Revit UI. You can access them via code. This blog is about a simple little module I wrote that let's you do this anywhere inside the shell:

>>>import revitsnoop

>>>snooper = revitsnoop.RevitSnoop(__revit__)

>>>snooper.snoop(doc.ProjectInformation)

And have a dialog like this popped up:

http://dl.dropbox.com/u/8112069/20110812-snoop-objects-dialog.png

Notice how you specify the object you want to snoop in code, as opposed to having to select it in the UI. Pretty nifty, eh?

How it works

The Autodesk.Revit.UI.UIApplication object has a property LoadedApplications that returns a list of all loaded external applications. As long as you have RevitLookup installed (quick test: can you find it in the Add-Ins tab?), it will show up in this list. It is pretty easy to find it:

rlapp = [app for app in uiApplication.LoadedApplications
         if app.GetType().Namespace == 'RevitLookup'
         and app.GetType().Name == 'App'][0]

I can't test with isinstance, since RPS doesn't know about RevitLookup yet and therefore I can't name the type. That is why I used GetType() and compared the namespace and name of the type.

Once we have the type, though, we can load the assembly and import RevitLookup as a module into IronPython:

import clr
clr.AddReference(rlapp.GetType().Assembly)
import RevitLookup

The statement import RevitLookup will only work after adding a reference to the assembly, but afterwards, we can start using the classes inside. To figure out what had to be done, I checked the source for RevitLookup, specifically the ExternalCommand that implements the "Snoop Current Selection..." functionality: RevitLookup.CmdSnoopModScope. It would have been possible to create an instance of this class and call its Execute method, but it turned out to be quicker and easier to just implement the body myself:

# See note in CollectorExt.cs in the RevitLookup source:
RevitLookup.Snoop.CollectorExts.CollectorExt.m_app = __revit__

# create a list of elements to snoop
elementSet = ElementSet()
elementSet.Insert(doc.ProjectInformation)

# create and show the dialog
form = RevitLookup.Snoop.Forms.Objects(elementSet)
form.ShowDialog()

This code is wrapped up in a module called revitsnoop for your convenience in the RevitPythonShell download section. To use it, place it inside one of the search paths configured in RPS.

The Code

For completeness sake, here is the whole module:

'''
revitsnoop.py

a simple library to access the RevitLookup snoop functionality
while inside the interactive shell...
'''
import clr
from Autodesk.Revit.DB import ElementSet

class RevitSnoop(object):
    def __init__(self, uiApplication):
        '''
        for RevitSnoop to function properly, it needs to be instantiated
        with a reverence to the Revit Application object.
        '''
        # find the RevitLookup plugin
        rlapp = [app for app in uiApplication.LoadedApplications
                 if app.GetType().Namespace == 'RevitLookup'
                 and app.GetType().Name == 'App'][0]
        # tell IronPython about the assembly of the RevitLookup plugin
        clr.AddReference(rlapp.GetType().Assembly)
        import RevitLookup
        self.RevitLookup = RevitLookup
        # See note in CollectorExt.cs in the RevitLookup source:
        self.RevitLookup.Snoop.CollectorExts.CollectorExt.m_app = uiApplication

    def snoop(self, element):
        elementSet = ElementSet()
        elementSet.Insert(element)
        form = self.RevitLookup.Snoop.Forms.Objects(elementSet)
        form.ShowDialog()

I think a logical further improvement would be to add some methods (e.g. snoopDocument and snoopApplication) for accessing the other useful functions of RevitLookup. But by now you should already know how to do it yourself!

Thursday, June 30, 2011

Wall.Orientation Property for walls based on arcs in Autodesk Revit 2011

This is what the documentation for the Wall.Orientation property in Autodesk Revit 2011 says:

The normal vector projected from the exterior side of the wall.

While this is tried and tested, it is only half of the story. I don't know the whole story, but this post is about one exception i found: Walls based on an arc.

Repro steps:

  1. create a new Revit document
  2. in the ribbon, on the "Home" tab click "Wall"
  3. in the "Draw" panel, choose the icon "Start-End-Radius Arc"
  4. create a wall by specifying the start, the end... well, you get it.

So, we now have a wall that isn't planar:

http://dl.dropbox.com/u/8112069/20110630-start-end-radius-arc.png

What is its orientation? What do you think it should be? Let's ask Revit! I'm going to use the RevitPythonShell (RPS) to query this wall, so we can see interactivly what is going on:

>>>collector = FilteredElementCollector(doc)

>>>wall = collector.OfClass(Wall).ToElements()[0]

>>>wall.Orientation

<Autodesk.Revit.DB.XYZ object at 0x000000000000002F [(-0.858104216, 0.513475563, 0.000000000)]>

I was expecting the orientation to be due North: (0, 1, 0). That is where the orientation handle bars are shown. But that is not what we get. What we get is a vector from the curve center point to the start point of the arc:

>>>curve = wall.Location.Curve

>>>P0 = curve.get_EndPoint(0)

>>>M = curve.Center

>>>Vector(P0, M)

IronTextBoxControl error: name 'Vector' is not defined
>>>XYZ(P0.X - M.X, P0.Y - M.Y, P0.Z - M.Z)

<Autodesk.Revit.DB.XYZ object at 0x0000000000000030 [(-34.284776903, 20.515451146, 0.000000000)]>
>>>r = XYZ(P0.X - M.X, P0.Y - M.Y, P0.Z - M.Z)

>>>r.Normalize()

<Autodesk.Revit.DB.XYZ object at 0x0000000000000031 [(-0.858104216, 0.513475563, 0.000000000)]>
>>>

As you can see, the orientation has the same direction as a vector from the arc center to the start point of the arc. This is for walls that face away from the arc center. Let's flip the wall and see what happens:

>>>t.Start()

<Autodesk.Revit.DB.TransactionStatus object at 0x0000000000000032 [Started]>
>>>wall.flip()

>>>t.Commit()

<Autodesk.Revit.DB.TransactionStatus object at 0x0000000000000033 [Committed]>
>>>wall.Orientation

<Autodesk.Revit.DB.XYZ object at 0x0000000000000034 [(0.858104216, -0.513475563, 0.000000000)]>
>>>

As you see, the vector changed it's orientation too. So, for arcs with wall orientation facing the arc center, the Wall.Orientation property is the normalized vector from the arc start point to the arc center.

Friday, May 27, 2011

Which of the parameters on the ProjectInfo are built in?

Yesterday I needed to know which of the ProjectInfo parameters are actually built in and can be relied on to be found in any Autodesk Revit Architecture document. Of course I fired up RPS and had a go at finding a solution. Here is what I came up with:

import clr
clr.AddReference('RevitAPI')
clr.AddReference('mscorlib')
from Autodesk.Revit.DB import *
from System import Enum

doc = __revit__.ActiveUIDocument.Document
pi = doc.ProjectInformation
params = list(pi.Parameters)
builtInParameterIds = set([p.Id.IntegerValue for p in params if p.Id.IntegerValue < 0])
for id in builtInParameterIds:
    parameter = pi.get_Parameter(Enum.ToObject(BuiltInParameter, id))
    name = Enum.GetName(BuiltInParameter, id)
    if parameter:
        print name, ': ', parameter.AsString()
    else:
        print name, ': <NULL>'

There are some aspects of this script I'd like to point out, since they might help you writing your own scripts:

This gives you access to the System.Enum type. The BuiltInParameter enumeration has to be marshalled back and forth to IronPython integers using Enum:

  • Enum.ToObject(BuiltInParameter, id) returns the enum constant that has the integer value id
  • Enum.GetName(BuiltInParameter, id) returns the name of the enum constant (handy for printing to the user)

Also note that all the predefined parameters in Revit have negative ids - that is how we find them:

The output on a test project on my computer is:

PROJECT_ISSUE_DATE :  Projekt Datum
GBXML_EDIT_DATA_PARAM : <NULL>
PROJECT_STATUS :  Projekt  Status
CLIENT_NAME :  Bauherr
PROJECT_ADDRESS :  Projektadresse
PROJECT_NAME :  Projekt Name
PROJECT_NUMBER :  01212

Thursday, May 19, 2011

Adding both 32 and 64 bit versions of DLLs to a Visual Studio 2010 Setup project

I ran into a snag, when trying to create an installer using a Visual Studio 2010 Setup project that would install for 32 and 64 bit versions of Autodesk Revit Architecture. Since my program depends on a DLL (in this case System.Data.SQLite.dll) that ships in two different versions, I wanted to include both in the MSI and then copy the correct one to the installation folder with a CustomAction project. The snag consists of this pesky error message whenever you add a 64 bit dll to a Setup project that is targeting x86:

File 'System.Data.SQLite.dll' of project output 'Content Files from MYPROJECT (Active)' targeting 'AMD64' is not compatible with the project's target platform 'x86'

That was easy to fix! All I had to do was include the file with a different extension, right? So... I renamed System.Data.SQLite.dll to System.Data.SQLite.txt and added it to MYPROJECT as a new content file. Same error when building the setup project:

File 'System.Data.SQLite.txt' of project output 'Content Files from MYPROJECT (Active)' targeting 'AMD64' is not compatible with the project's target platform 'x86'

It seems, the setup project figures out that you have a DLL here, even if the extension doesn't is something totally different. I tried various extensions to no avail.

So, the next logical step would be to use the old email zip file trick, right? It is rather easy to zip a DLL. Any number of tools will do that for you. It turns out to be rather tricky to unzip a DLL in C# inside your CustomAction.

It is possible to use a 3rd party library for unzipping, but I didn't really want to add a dependency just for the installer. Instead, I opted to use the DEFLATE algorithm provided in .NET here: System.IO.Compression.DeflateStream.

Given a file compressed with this algorithm, the code to inflate it back to normal size is trivial:

using (var srcFile = File.OpenRead(@"C:\MYPATH\System.Data.SQLite.x64"))
{
    using (var dstFile = File.OpenWrite(@"C:\MYPATH\System.Data.SQLite.dll"))
    {
        using (var deflator = new DeflateStream(srcFile, CompressionMode.Decompress))
        {
            CopyStream(deflator, dstFile);
        }
    }
}

Just... how to get the original file into the deflated version? I couldn't find a tool that reliably did this for me, so I created my own: http://code.google.com/p/deflate/

To quote from the projects page:

This project provides two simple programs:

deflate.exe inflate.exe They are implented using the System.IO.Compression.DeflateStream provided in .NET.

Usage:

C:\> type MYFILE | deflate.exe > MYFILE.deflated
C:\> type MYFILE.deflated | inflate.exe > MYFILE

I now have the honour to present the steps to create a DLL targeted at a different architecture as your setup:

  1. deflate the file using deflate.exe, naming it with a different extension (e.g. .x64)
  2. add it to your main project as a content file
  3. add a custom action project to your solution
  4. add the custom action to the setup projects "Install" custom actions
  5. inflate the file inside the custom actions Install method using System.IO.Compression.DeflateStream (see code above)
  6. do a little dance around your desk, down the hall, and past as many coworkers as you care to annoy :)

Thursday, April 14, 2011

Listing Window Dimensions with RevitPythonShell

I wrote this quick script this morning and thought I should share:

import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

doc = __revit__.ActiveUIDocument.Document

import revitutil

def main():
    windows = revitutil.listWindows(doc)

    for w in windows:
        instance_width = revitutil.meters(w.get_Parameter(BuiltInParameter.WINDOW_WIDTH).AsDouble())
        instance_height = revitutil.meters(w.get_Parameter(BuiltInParameter.WINDOW_HEIGHT).AsDouble())

        wt = doc.get_Element(w.GetTypeId())

        type_width = revitutil.meters(wt.get_Parameter(BuiltInParameter.WINDOW_WIDTH).AsDouble())
        type_height = revitutil.meters(wt.get_Parameter(BuiltInParameter.WINDOW_HEIGHT).AsDouble())

        rough_width = revitutil.meters(wt.get_Parameter(BuiltInParameter.FAMILY_ROUGH_WIDTH_PARAM).AsDouble())
        rough_height = revitutil.meters(wt.get_Parameter(BuiltInParameter.FAMILY_ROUGH_WIDTH_PARAM).AsDouble())

        wid = w.Id.IntegerValue
        wname = w.Name

        print "%(wid)d\t%(wname)s\t%(instance_width).2f\t%(instance_height).2f\t%(type_width)s\t%(type_height).2f\t%(rough_width).2f\t%(rough_height).2f" % locals()


if __name__ == '__main__':
    import traceback
    try:
        main()
    except:
        traceback.print_exc()

It doesn't do much more than you could do with a window schedule in the Revit GUI, except that I couldn't figure out how to distinguish instance width and type width in window schedules. As I was trying to debug some code that made that distinction, I needed some goggles! Out pops RPS to the rescue ;)

One thing I'd like to point out is the structure of the script. I tend to write my canned commands in RPS following this structure:

  • import stuff and create a doc variable (I normally just copy the contens of the InitScript for this)
  • define a main() function that handles the code
  • use the if __name__ == '__main__' idiom to wrap calling the main function in a try block

The reason for using the try block is traceback.print_exc() which prints a nice error message to the console that helps debugging!

Wednesday, April 6, 2011

List Parameters of Current Selection

This is another post about using the RevitPythonShell (RPS) for accessing the Revit API. I assume you are using a variation of the default InitScript, as that provides a handy selection variable.

The following session in the RPS shell assumes you have selected a wall in your Revit model. It then provides a list of the names of all parameters on the walls type:

>>>w

<Autodesk.Revit.DB.Wall object at 0x000000000000002B [Autodesk.Revit.DB.Wall]>
>>>wt = doc.get_Element(w.GetTypeId())

>>>plist = list(wt.Parameters)

>>>pnames = [p.Definition.Name for p in plist]

>>>pnames

['Description', 'DPV:DpvConstruction', 'Fire Rating', 'Structure', 'Assembly Description', 'Type Mark', 'Cost', 'Coarse Scale Fill Color', 'URL', 'Function', 'Width', 'Wrapping at Inserts', 'Assembly Code', 'Type Comments', 'Coarse Scale Fill Pattern', 'Keynote', 'Model', 'Wrapping at Ends', 'Manufacturer']
>>>

Thursday, March 31, 2011

Exporting Images with the Autodesk Revit API

I tried to find some sample code for using the Document.ExportImage method in Autodesk Revit 2011 but couldn't find any. So I wrote my own sample code and posted it on the RevitPythonShell wiki as a featured script. I'd like to share it here too:

'''
exportImage.py

Export the currently visible view as a PNG image to a location specified by the user.
'''
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

doc = __revit__.ActiveUIDocument.Document

# collect file location from user
clr.AddReference('System.Windows.Forms')
from System.Windows.Forms import DialogResult, SaveFileDialog
dialog = SaveFileDialog()
dialog.Title = 'Export current view as PNG'
dialog.Filter = 'PNG files (*.PNG)|*.PNG'

if dialog.ShowDialog() == DialogResult.OK:
    # set up the export options
    options = ImageExportOptions()
    options.ExportRange = ExportRange.VisibleRegionOfCurrentView
    options.FilePath = dialog.FileName
    options.HLRandWFViewsFileType = ImageFileType.PNG
    options.ImageResolution = ImageResolution.DPI_72
    options.ZoomType = ZoomFitType.Zoom
    options.ShadowViewsFileType = ImageFileType.PNG

    doc.ExportImage(options)

__window__.Close()

The above code does some stuff necessary for any canned command in RPS: It adds references to the RevitAPI and imports the Autodesk.Revit.DB types. I also open a SaveFileDialog using code found here: http://www.ironpython.info/index.php/SaveFileDialog.