Jeremy Tammik

July 2009

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

July 10, 2009

Harvard Energy Model Survey

Here is an invitation to a survey on the use of building energy models during design, construction and operation. It is being carried out by the Graduate School of Design at Harvard University (GSD) in collaboration with the Harvard Office for Sustainability (OFS) and addressed to building owners, architects and energy consultants to better understand:

  • who on the design team owns and gets access to the energy model of a building,
  • what role the model currently plays during building design and construction, and
  • how the use of the model could be extended to the overall lifetime of a building.

Harvard will be sharing the results of the survey, which will be published by January 2010.

Retrieving Newly Created Elements

The day before yesterday, I demonstrated the use of the Mirror method. Henrik Bengtsson of Lindab immediately came back with a question on this. Funnily enough, the reason I quickly posted the article the day before yesterday was because someone completely different asked the exact same two questions as Henrik in the exact same sequence. There seems to be some kind of morphogenetic resonance going on among Revit developers. Anyway, here is Henrik's question:

Question: The mirror command works all right according to the description. I now face another issue that I had not thought of before. How can I get a reference to the newly created mirrored objects? Those are actually the objects that I want to continue working with.

Answer: Because this question was already asked once this morning, I had some time to meditate deeply on how to retrieve the newly created elements generated by the mirroring operation. My first idea was to grab them by parsing the tail of the journal file, but unfortunately they are not listed there. My second idea works, however:

  • Ask the document for all its elements before the mirroring operation and remember the total number n.
  • Call the Mirror method, generating a number of new elements.
  • Ask the document for all its elements again, and retrieve the ones whose index exceeds n.

I make use of two helper methods for this:

  • GetElementCount determines the total number of document elements before the mirroring operation.
  • GetElementsAfter returns a list of all document elements whose index exceeds a given number.

Here is the implementation of these two methods:

int GetElementCount( Document doc )
{
  int count = 0;
  ElementIterator it = doc.Elements;
  while( it.MoveNext() ) 
  { 
    ++count; 
  }
  return count;
}
 
List<Element> GetElementsAfter( int n, Document doc )
{
  List<Element> a = new List<Element>( n );
  ElementIterator it = doc.Elements;
  int i = 0;
 
  while( it.MoveNext() ) 
  {
    ++i;
 
    if( n < i )
    {
      a.Add( it.Current as Element );
    }
  }
  return a;
}

Here is the code for the new external command implementing this. It is a simple extension of the mirroring command CmdMirror presented on Wednesday:

Application app = commandData.Application;
Document doc = app.ActiveDocument;
 
ElementSet els = doc.Selection.Elements;
 
Line line = app.Create.NewLine( 
  XYZ.Zero, XYZ.BasisX, true );
 
int n = GetElementCount( doc );
 
doc.Mirror( els, line );
 
List<Element> a = GetElementsAfter( n, doc );
 
string s = "The following elements were mirrored:\r\n";
 
foreach( Element e in a )
{
  s += string.Format( "\r\n  {0}",
    Util.ElementDescription( e ) );
}
Util.InfoMsg( s );
 
return CmdResult.Succeeded;

Here is version 1.1.0.41 of the complete Visual Studio solution including the mirroring command CmdMirror and the new command CmdMirrorListAdded.

Disclaimer: Please note that there is nothing in the Revit API documentation stating that the iterator returned by doc.Elements will traverse the elements in the order they were added to the database. That happens to be the case today and has been so in the past. This behaviour may change without notice in the future.

July 09, 2009

View Sketch Plane

Some views provide a sketch plane, others do not. In particular, the section views do not. Here is a little exploration into this subject prompted by the following question from Toste Wallmark of Tecton Limited:

Question: I'm trying to get the SketchPlane property of currently active view, which is a section view. However, it returns null. Do I need to manually create a sketch plane for section views? Other types of views, like floor plans, do correctly return a SketchPlane object.

Answer: I implemented a little dedicated sample application to test your assertion, and I can reproduce what you say in the sample projects that I have looked at so far.

I would recommend to always check whether the sketch plane returned from a view is null or not. In case it is null, you will obviously have to create your own. You can do so using the view origin and direction. These values are always available from the corresponding properties on the view object, even though the view plane is null.

Here is the result of searching for all document elements that are of the View class or one of its five derived classes. Looking at a separate listing of all database elements, I noticed that many of the view related objects occur in pairs, like the elements used for level 1 and 2 below with the element id pairs (13073, 13077) and (15915, 15919). In the other cases, the object type of only one of the two related objects is View, whereas the other is simply a Revit Element and thus does not appear in this list:

View sketch planes

I also print out this list to the Visual Studio debug output window. The overly long lines can be seen by copying this text to an editor:

List of document views' sketch planes: 

View [3651] Project View origin (0,0,0) direction (0,-1,0): 
ViewPlan [13073] Level 1 origin (0,0,0) direction (0,0,1): Level 1 plane origin (0,0,0), plane normal (0,0,1)
ViewPlan [13077] Level 1 origin (0,0,0) direction (0,0,1): Level 1 plane origin (0,0,0), plane normal (0,0,1)
ViewPlan [15915] Level 2 origin (0,0,0) direction (0,0,1): Level 2 plane origin (0,0,13.12), plane normal (0,0,1)
ViewPlan [15919] Level 2 origin (0,0,0) direction (0,0,1): Level 2 plane origin (0,0,13.12), plane normal (0,0,1)
View [29152] North origin (-1.04,84.1,3.94) direction (0,1,0): 
View [29193] East origin (84.12,-2.54,3.94) direction (1,0,0): 
View [29214] West origin (-84.13,-2.42,3.94) direction (-1,0,0): 
View [29233] South origin (-1.08,-84.12,3.94) direction (0,-1,0): 
ViewPlan [29273] Site origin (0,0,0) direction (0,0,1): Level 1 plane origin (0,0,0), plane normal (0,0,1)
View [92030] System Browser origin (0,0,0) direction (0,0,1): 
View [138046] Section 1 origin (-20.55,2.78,13.12) direction (-1,0,0): 

Note that this list includes a view object for the system browser, which obviously has no sketch plane at all. It still returns valid values for the view origin and direction.

Here are the utility methods used by the external command which generates this list:

static string RealString( double a )
{
  return a.ToString( "0.##" );
}
 
static string PointString( XYZ p )
{
  return string.Format( 
    "({0},{1},{2})",
    RealString( p.X ), 
    RealString( p.Y ),
    RealString( p.Z ) );
}
 
static string PlaneString( Plane p )
{
  return string.Format( 
    "plane origin {0}, plane normal {1}",
    PointString( p.Origin ), 
    PointString( p.Normal ) );
}
 
static Filter OrType( 
  Filter f, 
  Type t, 
  Autodesk.Revit.Creation.Filter cf )
{
  Filter f2 = cf.NewTypeFilter( t );
  return cf.NewLogicOrFilter( f, f2 );
}

Here is the code of the external command Execute method:

Application app = commandData.Application;
Document doc = app.ActiveDocument;
View activeView = doc.ActiveView;
 
Autodesk.Revit.Creation.Filter cf 
  = app.Create.Filter;
 
Filter f1 = cf.NewTypeFilter( typeof( View ) );
Filter f2 = OrType( f1, typeof( View3D ), cf );
Filter f3 = OrType( f2, typeof( ViewDrafting ), cf );
Filter f4 = OrType( f3, typeof( ViewPlan ), cf );
Filter f5 = OrType( f4, typeof( ViewSection ), cf );
Filter f6 = OrType( f5, typeof( ViewSheet ), cf );
 
List<RvtElement> views = new List<RvtElement>();
doc.get_Elements( f6, views );
 
Debug.Assert( 0 < views.Count, 
  "expected document to have at leat one view" );
 
string msg 
  = "List of document views' sketch planes: " 
  + Environment.NewLine;
 
foreach( View view in views )
{
  msg += string.Format( 
    "{0}{1} [{2}] {3} origin {4} direction {5}: ", 
    Environment.NewLine, 
    view.GetType().Name, 
    view.Id.Value, 
    view.Name,
    PointString( view.Origin ),
    PointString( view.ViewDirection ) );
 
  SketchPlane sketch = view.SketchPlane;
  msg += ( null == sketch ) 
    ? "<null sketch>" 
    : sketch.Name + " " + PlaneString( sketch.Plane );
}
Debug.Print( msg );
WinForms.MessageBox.Show( msg, "View Sketch Planes" );
return IExternalCommand.Result.Failed;

Note that specifying a type filter when calling the document get_Elements method does not return derived types, only the specific type given. Since I wish to retireve all types derived from the View class as well as base class View elements, I need to create a Boolean expression or'ing together all of them. For this purpose, I invented a nice new little OrType method for succinctly or'ing together a sequence of type filters.

July 08, 2009

Mirror an Element

We already discussed transformation of elements using Move and Rotate. Here is a question regarding the related method Mirror that also deserves attention:

Question: I am trying to mirror a family instance element using the X axis as the mirror line. I am creating a new line using the NewLine method and then supplying a reference to that to the Document.Mirror method, but this is causing Revit to crash. The Reference property of my line is always null. I am doing it like this because I saw an example showing how to mirror a column element through its analytical model curve reference. How can I successfully mirror my family instance element if I have no such reference?

Answer: What you experience is expected. In your code, you are creating an abstract geometrical line at the application level. The resulting object is an abstract geometry entity and not a document element, and therefore no reference is available for this entity. In this case, you should not use the overload of the Mirror method that takes a Reference argument. You can simply use the one taking a Geometry.Line argument instead and use the line itself directly. If you really wanted to use a reference, you could also create a model line in the document and access its reference using the Line.GeometryCurve.Reference property. Normally, one would prefer not to create model elements if it is possible to use only in memory objects. Here is the complete code of a minimal but complete external command Execute method which mirrors the currently selected elements around the X axis:

public CmdResult Execute(
  ExternalCommandData commandData,
  ref string message,
  ElementSet elements )
{
  Application app = commandData.Application;
  Document doc = app.ActiveDocument;
 
  ElementSet els = doc.Selection.Elements;
 
  Line line = app.Create.NewLine( 
    XYZ.Zero, XYZ.BasisX, true );
 
  doc.Mirror( els, line );
 
  return CmdResult.Succeeded;
}

Many thanks to Saikat Bhattacharya for handling this case!

Space Adjacency

We looked at the topic of room and wall adjacency a while back. Martin Schmid now implemented a nice little related utility to check adjacencies between spaces. It demonstrates a beautiful use of the new Space.IsPointInSpace method. Here is an example of the kind of situation he is interested in:

Adjacent spaces

The aim is to obtain a list of all adjacent spaces from this model.

Martin added a new external command CmdSpaceAdjacency to The Building Coder sample application to analyse and report the space adjacency relationships in such a model. Here are some more implementation notes on this from Martin:

Basically, this command starts by collecting all the segments from all spaces, tessellating any curved segments. It then iterates each space's segments over all the other spaces' segments to find the segment 'closest' to it. This is done by FindClosestSegments( segmentPairs, segments );

However, just because a pair of segments from two different spaces are 'close' to one another, doesn't necessarily mean they are to be considered adjacent. There could be an outlying 'space', e.g., a storage shed or garage, and one surface of the space would be considered 'closest' to the main building itself, but in terms of being 'adjacent' for analysis, there is actually a considerable 'air space' between them. Therefore, we need to limit how far apart spaces can be to be considered adjacent.

To achieve this, DetermineAdjacencies( spaceAdjacencies, segmentPairs ) checks whether the midpoints between each pair are closer than a specified tolerance. It also calculates a test point that should be within the adjacent space and uses the new Space.IsPointInSpace method to check whether this is true.

This work s on the test models so far. I have not yet tested on a larger dataset. The implementation is probably not optimal, but is a start.

After speaking with a customer about this, I learned that they needed to limit 'adjacency' to spaces that share a common door between them. Implementing this was actually a little more straightforward: each door knows its 'to room' and 'from room', and this can be queried from the model by inspecting the 'linked' model data. Each space knows what room encloses it, and this info is available in the 'host' model. Thus, I was able to establish a relationship between doors and Spaces to figure out the adjacencies. So that is yet another different method for a different analysis scenario.

It would be nice if there was actually a relationship in Revit between spaces and the walls that enclose it. Also, this is all more cumbersome when working with linked files. There may be another route, for instance if the ray intersection algorithm could be used to intelligently to seek for room or space boundaries and wall surfaces only. However, I found that in linked models, it seems that there is no way to determine exactly what object is being hit by the ray. Apparently, it only tells you that it hit a 'linked model', but not what particular object or type within that linked model. There are probably some API requirements to eke out of these example scenarios, but I'll leave that for later.

So much for Martin's explanation of this algorithm and other related work of his. Here are some more comments of mine on this command implementation based on reverse engineering:

The CmdSpaceAdjacency command works with the following data items:

  • Segment: a helper class to manage a space boundary segment, managing start and end point and the associated space and including some methods to obtain and compare slope and distance to other segments.
  • List<Segment> segments: a list of all spaces' boundary segments.
  • Dictionary<Segment, Segment> segmentPairs: a dictionary mapping each segment to the closest other segment in the set.
  • Dictionary<Space, List<Space>> spaceAdjacencies: a dictionary mapping each space to a list of all other spaces directly adjacent to it.

This data is generated and processed step by step by the following methods:

    GetBoundaries: determine all boundary segments for a given space. This method is applied in a loop to all selected spaces, or all spaces in the project if none were manually preselected.
  • FindClosestSegments: iterate over the list of segments and determine the closest other segment for each one. This is not fool-proof, since it simply calculates the distance between the midpoints of each segment.
  • DetermineAdjacencies: determine the space adjacencies from the pairs of closest boundary segments using the Space.IsPointInSpace method.
  • ReportAdjacencies: print the results to the Visual Studio debug output console.

Here is the Segment class implementation:

class Segment
{
  XYZ _sp;
  XYZ _ep;
  Space _space;
 
  public XYZ StartPoint
  {
    get { return _sp; }
  }
 
  public XYZ EndPoint
  {
    get { return _ep; }
  }
 
  public Space Space
  {
    get { return _space; }
    set { _space = value; }
  }
 
  public Segment( XYZ sp, XYZ ep, Space space )
  {
    _sp = sp;
    _ep = ep;
    _space = space;
  }
 
  public double Slope
  {
    get
    {
      double deltaX = _sp.X - _ep.X;
      double deltaY = _sp.Y - _ep.Y;
      if( deltaX != 0 )
      {
        return deltaY / deltaX;
      }
      return 0;
    }
  }
 
  public bool IsHorizontal
  {
    get
    {
      return _sp.Y == _ep.Y;
    }
  }
 
  public bool IsVertical
  {
    get
    {
      return _sp.X == _ep.X;
    }
  }
 
  public new string ToString()
  {
    return string.Format( "{0} {1}",
      Util.PointString( _sp ),
      Util.PointString( _ep ) );
  }
 
  public XYZ MidPoint
  {
    get
    {
      return _sp + 0.5 * ( _ep - _sp );
    }
  }
 
  public XYZ DirectionTo( Segment a )
  {
    XYZ v = a.MidPoint - MidPoint;
    return v.IsZero ? v : v.Normalized;
  }
 
  public double Distance( Segment a )
  {
    return MidPoint.Distance( a.MidPoint );
  }
 
  public bool Parallel( Segment a )
  {
    return ( IsVertical && a.IsVertical )
      || ( IsHorizontal && a.IsHorizontal )
      || Util.IsEqual( Slope, a.Slope );
  }
}

GetBoundaries simply asks a space for its boundary curves, tessellates them, and generates Segment instances for the result:

private void GetBoundaries(
  List<Segment> segments,
  Space space )
{
  BoundarySegmentArrayArray boundaries
    = space.Boundary;
 
  foreach( BoundarySegmentArray b in boundaries )
  {
    foreach( BoundarySegment s in b )
    {
      Curve curve = s.Curve;
      XYZArray a = curve.Tessellate();
      for( int i = 1; i < a.Size; i++ )
      {
        Segment segment = new Segment(
          a.get_Item( i - 1 ),
          a.get_Item( i ), space );
 
        segments.Add( segment );
      }
    }
  }
}

FindClosestSegments iterates over the list of segments and determines the closest other segment for each one by comparing the distance between their midpoints, returning The resulting closest pairs:

private void FindClosestSegments(
  Dictionary<Segment, Segment> segmentPairs,
  List<Segment> segments )
{
  foreach( Segment segOuter in segments )
  {
    bool first = true;
    double dist = 0;
    Segment closest = null;
 
    foreach( Segment segInner in segments )
    {
      if( segOuter == segInner )
        continue;
 
      if( segInner.Space == segOuter.Space )
        continue;
 
      double d = segOuter.Distance(
        segInner );
 
      if( first || d < dist )
      {
        dist = d;
        first = false;
        closest = segInner;
      }
    }
 
    segmentPairs.Add( segOuter, closest );
  }
}

DetermineAdjacencies determines the space adjacencies from the pairs of closest boundary segments using the Space.IsPointInSpace method. It analyses the relationship between the two closest segments s and t. If their distance exceeds the maximum wall thickness, the spaces are not considered adjacent. Otherwise, a test point two millimetres away from s in the direction of t is calculated and the Space.IsPointInSpace method applied to it to test whether it really lies within the candidate neighbouring space:

private void DetermineAdjacencies(
  Dictionary<Space, List<Space>> a,
  Dictionary<Segment, Segment> segmentPairs )
{
  foreach( Segment s in segmentPairs.Keys )
  {
    Segment t = segmentPairs[s];
    double d = s.Distance( t );
    if( d < MaxWallThickness )
    {
      XYZ direction = s.DirectionTo( t );
      XYZ startPt = t.MidPoint;
      XYZ testPoint = startPt + direction * D2mm;
      if( t.Space.IsPointInSpace( testPoint ) )
      {
        if( !a.ContainsKey( s.Space ) )
        {
          a.Add( s.Space, new List<Space>() );
        }
        if( !a[s.Space].Contains( t.Space ) )
        {
          a[s.Space].Add( t.Space );
        }
      }
    }
  }
}

Finally, ReportAdjacencies prints the results to the Visual Studio debug output console:

private void PrintSpaceInfo(
  string indent,
  Space space )
{
  Debug.Print( "{0}{1} {2}", indent,
    space.Name, space.Number );
}
 
private void ReportAdjacencies(
  Dictionary<Space, List<Space>> spaceAdjacencies )
{
  Debug.WriteLine( "\nReport Space Adjacencies:" );
  foreach( Space space in spaceAdjacencies.Keys )
  {
    PrintSpaceInfo( "", space );
    foreach( Space adj in spaceAdjacencies[space] )
    {
      PrintSpaceInfo( "  ", adj );
    }
  }
}

Here is the code of the Execute method which performs these steps in sequence:

Application app = commandData.Application;
Document doc = app.ActiveDocument;
 
List<Element> spaces = new List<Element>();
if( !Util.GetSelectedElementsOrAll(
  spaces, doc, typeof( Space ) ) )
{
  Selection sel = doc.Selection;
  message = (0 < sel.Elements.Size)
    ? "Please select some space elements."
    : "No space elements found.";
  return CmdResult.Failed;
}
 
List<Segment> segments = new List<Segment>();
 
foreach( Space space in spaces )
{
  GetBoundaries( segments, space );
}
 
Dictionary<Segment, Segment> segmentPairs
  = new Dictionary<Segment, Segment>();
 
FindClosestSegments( segmentPairs, segments );
 
Dictionary<Space, List<Space>> spaceAdjacencies
  = new Dictionary<Space, List<Space>>();
 
DetermineAdjacencies(
  spaceAdjacencies, segmentPairs );
 
ReportAdjacencies( spaceAdjacencies );
 
return CmdResult.Failed;

Here is the result of running the new command CmdSpaceAdjacency on the sample shown above:

Report Space Adjacencies:

Space 1 1
  Space 2 2
Space 2 2
  Space 4 4
  Space 3 3
  Space 1 1
Space 3 3
  Space 2 2
  Space 4 4
Space 4 4
  Space 2 2
  Space 5 5
  Space 3 3
Space 5 5
  Space 4 4

Here is version 1.1.0.38 of the complete Visual Studio solution with the new command.

As Martin pointed out above, this sample provides a solution for one specific case. It may not be totally reliable under all circumstances, and as Martin already discovered, many other approaches and different requirements for space adjacency analysis may occur. It does however provide a wonderful example of what can be achieved with relatively little effort. And as said, it also shows a really nice use of the new Space.IsPointInSpace method.

Very many thanks to Martin for providing this interesting sample!

July 07, 2009

Revit 2010 Web Update 1

The Revit 2010 Web Update 1 is now available from the usual places:

Click on the Product Download link on the left hand side.

Here are the detailed Revit API enhancements included in this update, available in all three flavours:

  • Suppressed Revit inaccuracies warnings during transaction when generating geometries using massing API.
  • Associated LinearArray's NumMembers property with Family parameter.
  • Dissociated FamilyParameter from Label property of BaseArray and Dimension.
  • Provided a tool to update reference path of RevitAPI.dll for samples.
  • Provided an update to DistanceToPanels sample.
  • Improved stability when creating NewReferencePoint with PointOnEdge that references a ModelCurve.
  • Floating point values are no longer reset when crossing from managed code to unmanaged code.
  • In the Print API, ViewSheetSetting.Save() no longer raises an InvalidOperationException when the user removes all views.
  • A meaningful exception will be raised when an incorrect sketch plane is used in NewSweep(,,SketchPlane,,,).
  • FamilyManager.Set() can now assign an ElementId value to a parameter.
  • Improved stability when using the DocumentOpened event and linked files.
  • Improved stability of VSTA.

July 06, 2009

DWG and DWF Family Creation

Here is another question on family creation, this time not regarding parameters, but the geometrical content and reuse of existing geometry.

Revit is a strongly parametric system, and the Revit family management obviously also. However, if one happens to have important predefined static geometry for certain components, that can be reused as well. It is of course a shame to be using static, non-parametric geometry in an environment with such powerful support for parameterisation. In some cases, however, reusing pre-existing content is the most efficient solution. The following question deals with this kind of geometry reuse.

Question: How can I use existing DWG and DWF files to define new Revit types and create families? In particular:

  • Do I need to create a separate family or a family type for each DWG or DWF file?
  • Do I need to create a physical RFA file before inserting it into the project, or can I use the API to insert the in-memory Document object directly? The RFA file is not required later, and the inserted family is embedded in the project.

Answer: The first place to look for an idea of how to automate importing external CAD files into a family is the Revit SDK sample DWGFamilyCreation. It shows how to import a DWG file into a family document, create a new type, and add parameters to it. In this sample, one single DWG file is imported into the family document. The new type is defined, and two parameters are added to it for the DWG file name and creation time.

Let's reiterate some basics. In Revit, a family is a collection of the types or symbols it defines. Normally, the types contained in one single family are closely related.

The family file defines the geometry, which can be parameterised. There can be any number of types in a family file. Each type can have parameters, and these parameters can be used to modify the geometry by driving pre-defined dimensions and constraints. Several types can thus share the same geometry.

If the geometry is defined by an imported DWG or DWF file, it will obviously be static and not have any dimensions or constraints that can be driven by parameters. If you import two DWG or DWF files into the same family document, they would by default both be visible. However, portions of the geometry can also be selectively hidden for different types.

If you have a one-to-one correspondence between the types and the DWG or DWF files defining the geometry, you will want to display only the portion of geometry defined by DWG or DWF file X for the corresponding type X. This can be achieved in two ways: either by creating a separate family for each DWG or DWF file, or by adding several DWG or DWF files and hiding all the geometry from the other ones for type X.

Here is an example of binding geometry with family types using DWF files:

In this example, three separate groups of static geometry are loaded into one single family file from three separate DWF files. Three types are created. Each type refers to one group of static geometry, and the other two are hidden for that type.

The three DWF files represent desks of different sizes: 60x30, 60x40, and 72x36.

  • Load them into a new family, and create three family types: 60x30, 60x40, and 72x36.
  • Add three family type Yes/No parameters: 60x30Visible, 60x40Visible, 72x36Visible.
  • Bind the visibility parameter to the respective imported instance visibility property:
Visibility parameters
  • Set 60x30Visible to true and the other two parameters to false for type 60x30, and analogously for the other two types:
Visibility parameter settings

Now you can control the geometry visibility depending on the selected type.

One can use this approach to load all DWG or DWF files into one single family, create a separate type for each one, and hide all of the other geometry for each, so each type displays exactly one DWG or DWF file.

Alternatively, of course, you can create a separate family file for each DWG or DWF file. The advantage of that would be less complexity and smaller files, and also the possibility to define different parameters for different source files; the disadvantage is the loss of the grouping into a single coherent family, the larger number of files, and ensuing management overhead.

In order to actually load the newly defined family type into a Revit project, one can use one of the two methods for loading families and types provided by the Document class:

  • LoadFamily, which loads an entire family and all its types or symbols.
  • LoadFamilySymbol, which loads only one specified family type or symbol from a family file.

Both of these are provided in two flavours each, one taking a string argument specifying the file name of the RFA file to load, and the other taking a Document argument. You can use the Document argument overload to load a family document that has not yet been saved in an external file. Therefore, you can create a new family document in memory and load it without saving it first.

July 04, 2009

Get and Set Family Category and Parameters

I am receiving a lot of questions about parameters in family documents, so I will be posting several solutions in this area in the next few days. The first one is short and simple and suited for a rapid Saturday post. There is so much news coming that I do want to cram it in right away. This is a question from Jose Fandos of Andekan LLC:

Question: How can we get and set the family category and the family parameters seen in the "Family Category and Parameters" Revit dialogue box? This dialogue can be displayed by selecting Create > Category and Parameters in the family editor:

Family Category and Parameters dialogue

The parameters include the OmniClass number, for instance. We are not looking for the type or symbol parameters, and we need this access from the family editor, not from the project editor.

Answer: I used the RvtMgdDbg tool to explore a family file to answer your query. Using that, I can step into RvtMgdDbg > Snoop Application... > Application > Active Document > Owner Family > Parameters to see the family parameters, including the OmniClass number with the built-in parameter enumeration value OMNICLASS_CODE:

Snoop OmniClass number

The family category is also available as a property on this owner family object, but it is read-only.

Here is the external command Execute method that I implemented to demonstrate read access to both of these properties and write access to the OmniClass number:

BuiltInParameter _bip = BuiltInParameter.OMNICLASS_CODE;
 
public IExternalCommand.Result Execute( 
  ExternalCommandData commandData, 
  ref string message, 
  ElementSet elements )
{
  Application app = commandData.Application;
  Document doc = app.ActiveDocument;
 
  if( !doc.IsFamilyDocument )
  {
    message 
      = "Please run this command in a family document.";
  }
  else
  {
    Family f = doc.OwnerFamily;
    Category c = f.FamilyCategory;
    Parameter p = f.get_Parameter( _bip );
 
    Debug.Print( 
      "Category '{0}', OmniClassNumber {1}", 
      c.Name, p.AsString() );
 
    p.Set( "Jeremy" );
 
    Debug.Print( "Modified OmniClassNumber {0}", 
      f.get_Parameter( _bip ).AsString() );
  }
  return IExternalCommand.Result.Failed;
}

Note that I am returning Failed from the Execute method, so the transaction that Revit is managing for my command will be aborted, so the changes I made will not actually be committed.

Next week I plan to discuss work-arounds for the challenges surrounding shared parameters in family files, so stay tuned. Happy weekend to all!

July 03, 2009

Prompt the User for Pinning and IFC Export

We recently touched on the topic of IFC export and the lack of API access to this functionality. Another property that the Revit API also does not provide write access to is the Element.Pinned property. In both of these cases, however, write access is provided through the user interface, and it is possible to programmatically read the current state. Therefore, an application can determine the current state and prompt the user to execute the required action if necessary. While it would be nicer to be able to implement a fully automated process, this approach is fool-proof and safe.

Prompt the User to Pin a Link

Jim Bish of Inlet Technology recently suggested an effective, simple and reliable work-around for the lack of write access to the Element.Pinned property. His requirement is especially to pin unpinned Revit link instances. One solution is to simply prompt the user to pin them:

Public Function Execute( _
  ByVal commandData As ExternalCommandData, _
  ByRef message As String, _
  ByVal elements As ElementSet) _
As IExternalCommand.Result _
Implements IExternalCommand.Execute

  Dim app As Application = commandData.Application
  Dim doc As Document = app.ActiveDocument
  Dim element As Element = Nothing
  Dim f As Filter

  f = app.Create.Filter.NewCategoryFilter( _
    BuiltInCategory.OST_RvtLinks)

  Dim result As New List(Of Element)
  Dim n As Integer = doc.Elements(f, result)

  For Each element In result
    If TypeOf (element) Is Instance Then
      If Not element.Pinned Then
        MsgBox("Hello! " _
          & element.ObjectType.Name _
          & " is not pinned! Please pin it.")
      End If
    End If
  Next
End Function

Some features that might be worth adding to Jim's quick solution: Some kind of logging functionality, so that you are not forced to acknowledge the message box individually for each non-pinned link. It might also be helpful to add some more information to the message, such as the link's element id, in a widget allowing you to copy it to the clipboard. This will enable you to select it in the user interface, for instance in order to zoom in to it.

Prompt the User to Export to IFC

Another solution prompting the user to execute a command due to the lack of programmatic access was proposed by Richard Brand of Itannex, to handle the lack of an API to drive the IFC export functionality:

My tool needs to generate a ZIP file containing the Revit file and the IFC export. This ZIP file should be created when the user selects a menu item in my application. That is why I cannot produce the IFC files through a nightly batch process driving Revit through a journal file. I 'solved' it by checking if there is an IFC file present. If not, or if it is not recent, I ask the user to export the model to IFC by hand into a particular directory. As long as the Revit API does not provide access to the IFC export I guess we have to solve this issue this way. It should be much more user friendly if the IFC export would be generated automatically.

July 02, 2009

Three Coding and Performance Hints

We received three interesting and important coding and performance hints from Piotr and Guy Robinson on the post on Linq yesterday. I find them too important to leave them dozing away in the comment section, so here is their promotion to post status:

  1. Use auto-implemented properties to simplify the helper class code.
  2. Use ParametersMap instead of looping over all element parameters.
  3. The .NET Framework 3.5 SP1 is significantly improved over .NET 3.5.

As Piotr pointed out, auto-implemented properties are a nice new feature that has been available since .NET 3.0. It saves some typing and significantly simplifies and shortens the helper class definition.

Guy underlined that Joel's speed increases probably are a result of using proxy objects rather than Linq. Proxies are useful for data binding as well, though, so they get used more often than not, and Linq is pretty cool. Also, using .NET3.5SP1 is much better than .NET3.5. It includes a number of important fixes, and WPF is faster with SP1.

Guy also points out that looping over all the element parameters of each element is very costly, so you will get a nice additional speed improvement and a simpler class again for large datasets by using the ParametersMap in the constructor, rather than looping through all parameters.

Jeremy adds that you should please be aware that using the parameter name strings as keys in the parameter lookup has the disadvantage of making the code language dependent. If possible, that should be avoided, and built-in parameter enumeration values or GUIDs should be used instead. I should think that is faster still, though maybe only marginally.

This is what the helper class definition looks like after applying the first two recommendations:

class InstanceData
{
#region Properties
public Element Instance { get; set; }
public String Param1 { get; set; }
public bool Param2 { get; set; }
public int Param3 { get; set; }
#endregion
 
#region Constructors
public InstanceData( Element instance )
{
  Instance = instance;
 
  ParameterMap m = Instance.ParametersMap;
 
  Parameter p = m.get_Item( "Param1" );
  Param1 = ( p == null ) ? string.Empty : p.AsString();
 
  p = m.get_Item( "Param2" );
  Param2 = ( p == null ) ? false : ( 0 != p.AsInteger() );
 
  p = m.get_Item( "Param3" );
  Param3 = ( p == null ) ? 0 : p.AsInteger();
}
#endregion
}

Many thanks again to Joel for the original article and to Piotr and Guy for the valuable hints!

I still wonder whether using ParametersMap is faster than simply using Parameters. Another interesting thing to look at some day would be a benchmark comparing various different parameter access methods. One of these days, when we have lots of time ...

RSS Feed

Search

Search