Wednesday, January 16, 2013

Everyone vs. Just Me for Custom Actions Installer Classes

This post explains how to change the behavior of your Custom Actions Installer class depending on the user choice in the dialog box:

https://dl.dropbox.com/u/8112069/darenatwork/2013.01.16_JustMeEveryone.png

The Problem

When you create a Custom Action project to be called from a Visual Studio 2010 Setup project, you subclass System.Configuration.Install.Installer. This will then be called from the Visual Studio 2010 Setup project, passing in some context information about the installation via the Context.Parameters StringDictionary.

Except it doesn't contain the information about wether the user chose "Just Me" or "Everyone"!

The Solution

I came across this page that explained how to pass in information to Context.Parameters in the custom action: You can use the CustomActionData property to add values to Context.Parameters.

The trick is, to use the format "/varname=value", and variable substitution happens for Windows Installer Properties, if they are enclosed in square brackets...

After googling a bit more, I found a msdn page containing the Windows Installer Property Reference and searching that, I found the property ALLUSERS. This is set to the string "1" if the user chose to install for "Everyone" and set to the empty string "" if the user chose to install for "Just Me".

https://dl.dropbox.com/u/8112069/darenatwork/2013.01.16_CustomActionData.png

Putting two and two together, I added the string "/allusers=[ALLUSERS]" to the CustomActionData property for the custom actions. And then can query this inside the Installer class:

if (Context.Parameters["allusers"] == "1")
{
    // user selected "Everyone"
}
else
{
    // user selected "Just Me"
}

Testing this proved that the ALLUSERS property is retained by the Windows Installer and can also be used for uninstalling.

Context: Installing Revit Addins

When installing addins for the Autodesk Revit product, a manifest file has to be created and placed in a specific directory. There are two addin directories to choose from: One for the current user and one for all users of the system.

The RevitPythonShell installer for Revit 2013 has been updated with the above technique to install to different locations based on the users choice. You can see a working example of what the custom action code looks like in the RevitPythonShell source for the RegisterAddinCustomAction class.

Friday, October 5, 2012

RPS script to print a list of floor types and their functions

Yesterday, I needed to print out all the floor types in a Revit document along with their function (Interior/Exterior). Obviously, I decided to write a quick RevitPythonShell script, but ran into some snags along the way that I would like to share.

This should really be as easy as:

collector = FilteredElementCollector(doc)
for floor_type in collector.OfClass(FloorType):
   print floor_type.Name, floor_type.Function

But, well, nothing is ever easy, is it? What I ended up doing was this:

from System import Enum
collector = FilteredElementCollector(doc)
collector.OfClass(FloorType)
for floor_type in collector:
   name = Element.Name.GetValue(floor_type)
   function_param = Element.get_Parameter(floor_type, BuiltInParameter.FUNCTION_PARAM)
   if function_param:
      function = Enum.ToObject(WallFunction, function_param.AsInteger()).ToString()
   else:
      function = 'None'
   print '%(name)-25s\t%(function)s' % locals()

Let's go through this line by line...

from System import Enum

Uh... I'll explain that later on...

collector = FilteredElementCollector(doc)
collector.OfClass(FloorType)
for floor_type in collector:

Nothing unusual here - this is how to get a list of elements from a Revit document using a FilteredElementCollector and iterating over that list.

name = Element.Name.GetValue(floor_type)

What?! Shouldn't that just have been floor_type.Name? I thought so too. But somehow, whenever I do that, I get an error like this:

>>> # note how ugly one-liners get ;)
>>> ft = list(FilteredElementCollector(doc).OfClass(FloorType))[0].Name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: Name

This AttributeError is somehow related to IronPython not being able to figure out that it needs to use the base method inherited from Autodesk.Revit.DB.Element. So, I tell it to use that explicitly by referencing the property explicitly with Element.Name - and then retrieving its value: .GetValue. It needs to be told for which instance to retrieve the value, which is why I then plug in the floor_type object.

So... now we have the name of the FloorType. What about its function? That isn't a property, but rather a parameter, that can be found using the Revit Lookup tool: BuiltInParameter.FUNCTION_PARAM. Retrieving parameters is easy:

function_param = Element.get_Parameter(floor_type, BuiltInParameter.FUNCTION_PARAM)

Note again, how I use the base method Element.get_Parameter, as it doesn't seem to work on FloorType directly - my guess is that the definition of FloorType somehow isn't what IronPython is used to...

 if function_param:
       function = Enum.ToObject(WallFunction, function_param.AsInteger()).ToString()
else:
   function = 'None'

The parameter may be null or, in python None, which will evaluate to False in a condition. In that case, we just set the text of the variable function to 'None'`. But the other line is a lot more interesting! This is why we did the from System import Enum earlier on: The (integer) value of the FUNCTION_PARAM is actually a member of an enumeration. As far as I can tell, it is equivalent to the WallFunction enumeration. With Enum.ToObject(WallFunction, function_param.AsInteger()), we can an the appropriate instance of this enumeration, which we then convert to a string (ToString()).

The last line:

print '%(name)-25s\t%(function)s' % locals()

Prints out the name and function nicely, using pythons string formatting: Left-alined name with a minimum width of 25 characters, a tab and then the function.

Try the script!

Wednesday, January 18, 2012

Converting line endings on the cheap

You're at a client's PC, installing some scripts and need to edit a text file. Of course your client has no real editor, just that stupid notepad.exe! And then you notice: All the line endings are wrong, that is, they're LF (unix convention) as opposed to CR-LF (dos convention). That is because you do have a real editor, and so does the other guy working on that file and one of you was not working on windows, because it's only an OS, right? A tool, not a religion.

Not religous either: What escapes your mouth when you are confronted with a single line of script and black box characters laughing at you where line breaks should be.

Now, users of cygwin are probably familiar with a myriad of tools (e.g. dos2unix.exe) to convert from dos line endings to unix line endings. You might be able to get tr to work for you too. Or you could just open the file in your trusty real editor and type away.

But your client has none of this installed.

What your client does have installed, is MS Word. Seriously. I'm going to use MS Word now. This is how:

  1. open the script/text file in notepad
  2. select all text, copy it to the clipboard ([CTRL]+[A], [CTRL]+[C])
  3. open a blank document in MS Word
  4. paste from the clipboard ([CTRL]+[V])
  5. select all text in MS Word, copy it to the clipboard ([CTRL]+[A], [CTRL]+[C])
  6. paste over (replacing) the selection in notepad ([CTRL]+[V])
  7. close MS Word and wash your hands to get rid of the dirty hack feeling...

Friday, November 11, 2011

How to install Trac on a Synology DS2011+

In a previous post, I described installing Trac on a Synology DS209+II. I recently had to migrate this installation to a Synology DS2011+ and ran into some snags. This post will help you avoid spending all the hours I did on getting it to work.

Modifying the Synology DiskStation

To be able to do anything to your DS2011+, you first have to install the ipkg bootstrap. I basically followed along with page on the synology forum Overview on modifying the Synology Server, bootstrap, ipkg etc.:

  • You need to enable ssh access to your NAS and then log in with PuTTY
  • The Synology DS2011+ has a "Marvell Kirkwood mv6282" processor, so these steps should do the trick:
cd /volume1/@tmp
wget http://wizjos.endofinternet.net/synology/archief/syno-mvkw-bootstrap_1.2-7_arm-ds111.xsh
chmod +x syno-mvkw-bootstrap_1.2-7_arm-ds111.xsh
sh syno-mvk-bootstrap_1.2-7_arm-ds111.xsh
rm syno-mvk-bootstrap_1.2-7_arm-ds111.xsh

After rebooting the NAS, you can enter these commands:

ipkg update
ipkg upgrade

We are now ready to start installing all the stuff needed for running Trac!

Software to install

Apache

Log on to your DS2011+ and install the following package:

ipkg install apache

Now we have a second httpd listening on port 8000. The configuration file for this instance of apache is /opt/etc/apache2/httpd.conf. You can find the log files in /opt/var/apache2/log. Also note that it is started on system boot by the script /opt/etc/init.d/S80apache. I suggest you execute this script and see if apache starts up nicely.

I ran into some problems with this installation: A syntax error on line 74 of /opt/etc/apache2/httpd.conf. Somehow apache can't load the mod_ext_filter.so. I didn't really care and just commented out that line.

Subversion

Create a new share using the DSM (that web interface on port 5000) - call it "svn". It will show up on your box as /volume1/svn.

I mainly followed the Step-by-step guide to installing Subversion on the Synology wiki. But I didn't bother messing with inetd.conf, since I am running svn under apache with mod_dav_svn. So, that leaves the following steps:

ipkg install svn
su svnowner
cd /volume1/svn
svnadmin create testsvn

You now have a new repository on your NAS called "testsvn". You just can't access it yet - but we'll fix that next! Also note that the Step-by-step guide to installing Subversion includes a few hints on how to get su svnowner to work if you get an error message.

To get Apache to serve your subversion repository, append this to /opt/etc/apache2/httpd.conf:

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

You will have to create /opt/etc/apache2/conf.d/mod_dav_svn.conf of course:

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>

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.

Trac

So far, this guide is much the same as the one it replaces. The new stuff is in the version of python used:

ipkg install python26 py26-trac py26-genshi py26-setuptools sqlite svn-py

The need for upgrading comes from the package svn-py, which is now python 2.6.

Create a share (using the DSM) called "trac-env". This is where we will keep our trac installations.

I am now running Trac version 0.12. 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 -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

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

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

And then, create the file /opt/etc/apache2/conf.d/trac.conf:

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

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. Note how the authentication_url is the same as the Directory in trac.conf.

Backup

Backup is still the same as in the previous post, so I'll just point you there: http://darenatwork.blogspot.com/2011/03/how-to-install-trac-on-synology-ds209ii.html

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