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.

Wednesday, March 16, 2011

How to install Trac on a Synology DS209+II

These are some short notes on how I (successfully) installed Trac on a Synology DS209+II. The process should be similar for many other Synology servers. Please comment on any errors you find or snags you run into.

I modified my Synology server from the original Synology setup:

Configuration of Apache for Subversion

I appended following lines to /opt/etc/apache2/httpd.conf:

# Subversion
Include etc/apache2/conf.d/mod_dav_svn.conf

Also, I created a file /opt/etc/apache2/conf.d/mod_dav_svn.conf with following contents:

LoadModule dav_svn_module     libexec/mod_dav_svn.so
LoadModule authz_svn_module   libexec/mod_authz_svn.so

<Location /svn>
   DAV svn
   SVNParentPath /volume1/svn
   AuthType Basic
   AuthName "Subversion repository"
   AuthUserFile /volume1/svn/svn-auth-file
   Require valid-user
</Location>

# allow Apache to be used for authenticating Trac
<Directory /opt/share/www/trac>
   AuthType Basic
   AuthName "Subversion repository"
   AuthUserFile /volume1/svn/svn-auth-file
   Require valid-user
</Directory>

This expects a file /volume1/svn/svn-auth-file to be present. That file contains the passwords used for Subversion (and, via Apache, Trac authentication). This file is generated with apaches htpasswd -c command. I used the cm too, to force md5 hashing of the passwords. Also, I added the folder /opt/share/www/trac` - this is used later on for Trac authentication, as I instruct Apache to authenticate this URL and tell Trac to use it for authentication.

Trac configuration

I am running Trac version 0.11.7. The Trac environments can be found at /volume1/trac-env. They are called "MYTRAC1" and "MYTRAC2". Access to them is via the following URLs:

Trac is run using tracd. This is configured to start automatically on system start by creation of the following file: /opt/etc/init.d/S81trac:

#!/bin/sh
# run tracd
/opt/bin/tracd -d -p 8888 -auth=*,/volume1/svn/svn-auth-file,myserver.example.com -e /volume1/trac-env

The file must be set to executable for root (see documentation for chmod), so that it will be run at system start.

Trac configuration files can be found at

  • /volume1/trac-env/MYTRAC1/conf/trac.ini
  • /volume1/trac-env/MYTRAC2/conf/trac.ini

Authentication via Apache

Authentication is done with the AccountManager plugin for trac (http://trac-hacks.org/wiki/AccountManagerPlugin). The website has a good explanation on how that works. See the copies of the trac.ini files for reference. I'd like to point out the last section:

[account-manager]
password_store = HttpAuthStore
authentication_url = http://myserver.example.com:8000/trac

This is where we tell Trac to use our Apache Server for authentication.

Backup

Backing up Trac

Trac is backed up with the trac-admin hotcopy command. The configuration of the backup is in /etc/crontab. It is done by executing the script /volume1/trac-env/tracbackup:

#!/bin/sh

/opt/bin/trac-admin /v9olume1/trac-env/MYTRAC1 hotcopy `date +/volumeUSB1/usbshare/TracBackup/%Y%m%d_trac_hotcopy_MYTRAC1`
/opt/bin/trac-admin /v9olume1/trac-env/MYTRAC2 hotcopy `date +/volumeUSB1/usbshare/TracBackup/%Y%m%d_trac_hotcopy_MYTRAC2`

# make sure other computers connecting to the share
# can delete these files
chmod 777 /volumeUSB1/usbshare/TracBackup/*

FIXME: these still have to be deleted regularly. I do this from my PC, so as to keep the complexity on the server minimal.

Backing up Subversion

Subversion is backed up with the svnadmin dump command, since this can then be read in from other subversion versions. The configuration of the backup is in /etc/crontab. It is done by executing the script /volume1/svn/svnbackup:

#!/bin/sh

/opt/bin/svnadmin dump /volume1/svn/MYREPO1 > `date +/volumeUSB1/usbshare/SvnBackup/%Y%m%d_svn_dump_MYREPO1`
/opt/bin/svnadmin dump /volume1/svn/MYREPO2 > `date +/volumeUSB1/usbshare/SvnBackup/%Y%m%d_svn_dump_MYREPO2`

# make sure other computers connecting to the share
# can delete these files
chmod 777 /volumeUSB1/usbshare/SvnBackup/*

FIXME: these still have to be deleted regularly. I do this from my PC, so as to keep the complexity on the server minimal.

Thursday, February 10, 2011

Status Bar Text and WPF

Yesterday, Jeremy Tammik posted how to change the status bar text in Revit. Toward the end of his post, he mentions using GetCurrentProcess to find the HWND of the main Revit window.

Yay! says I: Now I can print out status text, without cluttering up my shiny WPF window.

Nay! says WPF: It doesn't work.

Imagine my frustration... I spent some time analysing this using SPY++ and found out, that WPF dialogs are not children of the main application window. If your IExternalCommand starts a WPF window, then you should note the HWND first and then show the dialog. You can keep the HWND in a class variable, since it won't change. I ended up with a utility static class StatusText:

public static class StatusText
{
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern int SetWindowText(IntPtr hWnd, string lpString);

    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter,
                                      string lpszClass, string lpszWindow);

    private static IntPtr _hwndRevit = new IntPtr();

    /// <summary>
    /// Set the handle to the revit main window here before calling
    /// SetStatusText.
    /// (also, set it in the IExternalCommand.Execute thread, not
    /// in your WPF thread)
    /// </summary>
    public static void SetRevitWindowHandle(IntPtr hwnd)
    {
        _hwndRevit = hwnd;
    }

    /// <summary>
    /// Set the status text of the current revit instance.
    /// </summary>
    public static void SetStatusText(string text)
    {
        IntPtr statusBar = FindWindowEx(_hwndRevit, IntPtr.Zero,
                                        "msctls_statusbar32", "");

        if (statusBar != IntPtr.Zero)
        {
            SetWindowText(statusBar, text);
        }
    }

}

You can then just issue a

StatusText.SetStatusText("hello, world!");

whenever you need to display something cool. Bonus points for hooking this up to a TraceListener ;)

Friday, November 26, 2010

A Pattern For Asynchronously Updating Revit Documents

With Revit 2011 we got a new event: UIApplication.Idling. This post is about how to use the Idling event to update a Revit document outside the scope of an IExternalCommand.

Imagine a dialog, opened by an IExternalCommand. Let us leave the dialog open and return Result.Succeeded. Let us also set the dialog to stay on top always. A bit like a tool window with some funky gadgets and commands. We want those commands to alter something in the Revit document and then update its gadgets to display stuff.

If you just stash away the ExternalCommandData and try to access the Revit application using that, you will crash Revit sooner or later. I forget the exact message, but something along the lines of reading/writing memory you shouldn't be. But you were warned by the docs, so, stop whining: Outside of the IExternalCommand.Execute method, the command data becomes stale and should not be used anymore.

Create a static Queue of Action<UIApplication>. You can store this in your IExternalApplication implementation and initialize it to an empty list on startup. This queue has to be static as you want it to persist while the class is loaded in memory (the class will be loaded by Revit on startup).

Next, hook up to the Idling event in your IExternalApplication.OnStartup method:

private static Queue<Action<UIApplication>> _tasks;
public Result OnStartup(UIControlledApplication revit)
{
    _tasks = new Queue<Action<UIApplication>>()
    revit.Idling += OnIdling;
}

Great. Now for the implementation of OnIdling:

private void OnIdling(object sender, IdlingEventArgs e)
{
    var app = (UIApplication)sender;
    lock (_tasks)
    {
        if (_tasks.Count > 0)
        {
            Action<UIApplication> task = _tasks.Dequeue();

            // execute the task!
            task(app);
        }
    }
}

This is one half of the equation. Next, we have to get the tasks into queue. Also, note that I'm locking the queue before reading it, since the dialog might access it at the same time as we are reading and I just want to be safe here.

To add a tasks to the queue, we just need a method in the IExternalApplication that will accept it:

public static void EnqueueTask(Action<UIDocument> task)
{
    lock (_tasks)
    {
        _tasks.Enqueue(task);
    }
}

This can then be called from your dialogs code, e.g. when a button is pressed. Assuming your IExternalApplication is called FooPlugin, an invocation could look something like this:

public void YouPushedMyButton(object sender, EventArgs args)
{
     FooPlugin.Enqueue((app) => {
         var doc = app.ActiveUIDocument.Document;
         doc.ProjectInformation.ClientName = "Boss Murphy";
     });
}

If you are not familiar with the (app) => { /* statements */ } this is a statement lambda. You might want to read the chapter Lambda Expressions from the C# Programming Guide on msdn.

A nice property of these lambda expressions is, that they create a closure around the variables that were in scope at the time of creation. Instead of "Boss Murphy" I could also have assigned some instance value from the dialog or a local variable from YouPushedMyButton.

The next time Revit fires an Idling Event, your tasks will be executed!

Oh, one thing: You will have to make sure you open a transaction. I haven't done this in the example, to keep things short and simple, but it's going to go boom on you! There are two possible places for this: Either in the lambda statement directly, or, if you know for sure that you are always going to need a transaction (all your tasks will be writing to the document), you could add it in the OnIdling event handler, wrapping the task(app) call.

Friday, November 12, 2010

Listing Level Elevations with RevitPythonShell

I recently stumbled apon the Revit APP blog and spotted a simple code listing for getting all level elevations. This should be a no-brainer using the RevitPythonShell (RPS), so I fired up a shell and started typing. This is a session transcript in the shell:

>>>import clr

>>>clr.AddReference('RevitAPI')

>>>import Autodesk.Revit.DB

>>>collector = Autodesk.Revit.DB.FilteredElementCollector(__revit__.ActiveUIDocument.Document)

>>>elems = collector.WhereElementIsNotElementType().OfClass(Autodesk.Revit.DB.Level).ToElements()

>>>for lv in elems:
...    print '%s: %.2f' % (lv.Name, lv.Elevation)
...
Level 1: 0.00
Level 2: 9.84
>>>

I am getting a bit fed up of typing import clr and referencing RevitAPI whenever I use RPS so my next step will be to try and add that by default. I'll keep you posted about that.

Wednesday, November 10, 2010

Unit Testing Revit Plugins

Yesterday, Jeremy Tammik brought an interesting thread on augi (Unit testing with Revit API) to my attention, suggesting that I might like to add something on the RevitPythonShell (RPS).

I couldn't register as a member on augi (some mail thing I don't want to digg into), so I thought I'd add my thoughts here.

First a disclaimer: I'm not test infected. I don't floss my teeth every day either - unit testing is just one of those things you should be doing, but nothing really bad happens if you don't. Especially if you use tooth picks etc.

Unit tests (as I understand the term) cannot be dependent on an environment (in this case: Revit). So running a test as a Revit plugin with a Revit document is not a unit test. It is an integration test. I do plenty of these. Here is how:

Using the RPS, I create a "driver" script that loads the assembly to be tested (see the loadplugin module) and executes its public methods, much the same way as the IExternalCommand implementation would. Note here, that I tend to keep my IExternalCommand implementations "dumb", refactoring the logic to other parts of the assembly, so that I don't have to test the IExternalCommand itself (it should just be evident that it works, e.g. instantiate plugin object, call public method on it passing some command data, return).

The "driver" script can be registered as an RPS command and will then show up in the Revit Ribbon. You can even assign a keyboard shortcut to it using the standard Revit procedure for shortcuts (you will have to restart Revit after registering the RPS command first, as it only then gets promoted to an IExternalCommand). Run it often (after every compile)!

I generally don't register plugins that are under development with Revit, so that Revit doesn't lock the assembly on startup - this way, I can keep the source open in Visual Studio and build to my hearts content. The "driver" script will just pick up the newest build and test it.

Anything written to "Debug.WriteLine" will be output in the RPS window. This is handy for testing stuff!

Once you are at the "driver" script level, you can employ pythons unit testing libraries: unittest and also use the doc tests. This also keeps your tests separate from your production code, especially since you are using another language to write the tests!

If you really want to do unit testing, you will have to go the full monty - this is the reason for unit testing anyway:

  • Split up your code into Revit dependent functionality and modules.
  • Make sure your plugin abstracts from the Revit stuff into a logical model that you can then mock and write tests to.

  • Regard the Revit API as an I/O interface or a database connection that is not part of unit testing.

Tuesday, November 9, 2010

Revit Room Boundaries

This came up today at work: For a given room, what are the room bounding objects?

Questions like these can easily be solved with RevitPythonShell. So I fired up an RPS shell and came up with the following script:

import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB.Architecture import Room
from Autodesk.Revit.DB import Wall

sel = __revit__.ActiveUIDocument.Selection.Elements
rooms = [e for e in sel if type(e) is Room]
for room in rooms:
    for bsa in room.Boundary:     # bsa = BoundarySegmentArray
        for bs in bsa:            # bs = BoundarySegment
            print bs.Element

This gets you quite close. I added a featured script to the RPS site that can be used as an RPS command and prints the element id for boundary segments that are model elements.