I made it back safe and sound to Switzerland from the developer conferences, and I note that this special pivotal day – the
winter solstice –
seems to be progressing normally and we all survived in spite of the end of an era in the
Maya calendar.
Thus we can continue growing our understanding of the Revit API, as well as attending to other important business here on our lovely planet.
The GeometryInstance GetInstanceGeometry method returns the geometry of a family symbol.
More precisely, according to the Revit API help file RevitAPI.chm documentation, it computes the geometric representation of the instance.
Computes really means computes, and incurs some significant overhead, as emphasised by the help file remarks, which state:
The geometry will be in the coordinate system of the model that owns this instance.
The context of the instance object (such as effective material) will be applied to the symbol.
Note that this method involves extensive parsing or Revit's data structures, so try to minimize calls if performance is critical.
Geometry will be parsed with the same options as those used when this object was retrieved.
This method returns a copy of the Revit geometry.
It is suitable for use in a tool which extracts geometry to another format or carries out a geometric analysis; however, because it returns a copy, the references found in the geometry objects contained in this element are not suitable for creating new Revit elements referencing the original element (for example, dimensioning).
Only the geometry returned by GetSymbolGeometry with no transform can be used for that purpose.
Now a poor hapless (or not so hapless, in fact) developer ran into an example where the invalid references returned by this method cause a significant problem, providing a welcome opportunity to highlight the importance of avoiding use of this particular method for that kind of use:
Question: I am using the analysis visualisation framework AVF to paint some transient graphics a given face.
When I use the following line to apply the AVF to the face, the behaviour is correct:
int idx = sfm.AddSpatialFieldPrimitive(
face, Transform.Identity );
When this call is used instead, however, Revit throws an exception saying that the reference must point to either a Face or a Curve visible in the view:
int idx = sfm.AddSpatialFieldPrimitive(
face.Reference );
This message is incorrect, because the Face is visible in this view.
I really need the face reference call to work so that the AddSpatialFieldPrimitive method overload taking a reference and SpatialFieldPrimitiveHideMode can be used, i.e.
Answer: The behaviour you observe is caused by your use of the GeometryInstance GetInstanceGeometry method, and then asking AVF to pull references from that geometry.
As stated in the description quoted above, these references are invalid.
If you simply change your macro to use GetSymbolGeometry instead, the AVF results will be correctly painted on the faces.
Some other operations besides AVF that also need these references to be correctly obtained from GetSymbolGeometry include dimensioning, face based family creation, and sketch plane creation.
GetInstanceGeometry is a shortcut to get 'in-place' geometry for operations like export to another format, Boolean operations with another solid, or other geometry tools that don't need the connection back to the original element.
I have not posted anything since last Friday, being too caught up in the West European Developer days and travelling.
Today and tomorrow we spend in Gothenburg on the last lap of our journey, after Paris, Milano, Farnborough and Munich.
I had a nap in the taxis to and from the airport yesterday, both in Germany and Sweden, so I was able to burn some midnight oil to share the following exploration with you.
One of the many interesting conversations I had at Autodesk University that caught my special attention dealt with a simple geometrical Revit API question; geometrical questions are among my favourites, and this one is really basic: how to calculate the
centre of mass or
centroid of a solid.
Once again, that led to the exploration of a number of interesting little sub-topics:
Before getting into that, here is a quick heads-up to point out a new free technology preview to simulate airflow around buildings or other objects in a virtual wind tunnel:
Emile Kfouri describes the
use and advantages of Falcon in
some depth on his
Building Performance Analysis blog
that I was previously unaware of.
He includes detailed explanations of the kind of problems this software addresses, getting started, and comparisons with other technologies such as the wind tunnel feature in
Project Vasari and
Simulation CFD 360.
Fascinating stuff.
By the way, this is extremely interesting to look at for any Revit add-in developer, even if you do not care about this specific kind of analysis, because it shows an impressive example of using Revit as a front-end input tool to a powerful analysis component, reporting the results back graphically using the analysis visualisation framework AVF. If you hook up these components intelligently, a lot of functionality can be achieved with little effort.
Centroid and Volume of a Solid
Returning to the centroid and volume calculation, the Revit API provides both of these through the Solid ComputeCentroid method and Volume property.
As said, a conversation prompted me to explore how to implement these calculations on my own as well.
Calculating exact results for an arbitrary solid is not trivial, of course, and could currently only be achieved making use of an external library.
If high precision is not paramount, however, we can simplify the solid to a planar faceted representation by triangulating all its faces to create a
polyhedral approximation of it.
Determining the centroid of a polyhedron is something that can be achieved in a very few lines of code, as I will show below.
Gap Free Triangulation for Polyhedral Approximation
Happily, the Revit 2013 API provides a method for triangulating the entire surface of a solid in one single call, ensuring a closed volume, unlike doing it face by face, which does not.
The Face.Triangulate method returns a triangular mesh approximation to the face.
Revit defines the approximation tolerance internally.
Calling it separately for neighbouring faces, however, will return independent triangulations that do not line up where the faces meet, leaving
gaps in the shell of
the original solid.
This deficiency was eliminated by the SolidUtils class introduced in Revit 2013, which provides the
TessellateSolidOrShell method to facet an entire solid or open shell in one single call.
This enables each boundary component of the solid or shell to be represented by a single properly closed triangulated structure.
Polyhedron Centroid Calculation Algorithm
With these necessary and powerful basics in place, I searched the Internet for a bit of help on implementing an algorithm to calculate the centroid, and found a discussion on determining the
centre of mass of a 3D model from
the
Game Maker Community.
Here is my edited version summarising the results of that thread:
Question: I'm working on a model editor and I have a set centre function to reset the centre of a model. I can do this a few ways which I have implemented.
Method 1: Add vertices, average them and move the model; pseudocode:
for each point
dx += point.x
dy += point.y
dz += point.z
count ++
dx/=count
dy/=count
dz/=count
for each point
point.x-=dx
point.y-=dy
point.z-=dz
The problem: If the model has many points in one area, the deviation favours that area.
Which is not always good because some models have many, many points like at the tip of a gun or the cone of a ship.
Method 2: Find min max of vertices, average the min max into a deviation and move the model:
for each point
mindx = min( point.x, mindx )
maxdx = max( point.x, maxdx )
same for y and z
dx = (mindx + maxdx) / 2
same for dy, dz;
for each point
point.x -= dx
same for y and z
That works OK.
For my third method, I would really like to centre the model on its estimated centre mass.
I have a series of 3 points defining the planes/faces.
I figure I could plug either the plane area or perimeter into the deviation calculation.
That way many points defining a tiny area would not affect the calculation that much.
But I can't figure out the right math for this.
Can you determine the surface areas of each face?
Answer: Well, one would assume that we'd want to count the inside of the model into the mass calculation as well, in which case you want to find the volume of the model.
This actually isn't too difficult to compute assuming that the model is closed (no holes; otherwise it wouldn't really have a well-defined volume) and that it's simple (in the mathematical sense; that means it doesn't intersect with itself).
Each triangular face contributes a tetrahedron's worth of volume to the whole model.
To find this volume given the three vertices of the face v1, v2, and v3, you need only compute v1 · (v2 × v3 ) / 6.
The following is very important.
The vertices of every face must be oriented the same way.
This means that if you look at a face from the outside of a model, its vertices should be oriented in a clockwise fashion (counter-clockwise is valid too, as long as you're consistent).
So, now we essentially have a way to compute a weighting factor for every face in the model. The coordinates we have to weight this by are the centroids of each tetrahedron, given by (v1 + v2 + v3)/4.
This is because one of the vertices of every tetrahedron is the origin so it's 0, so there's no reason to include it here.
So, the centroid of the model is given by the following:
Also, the factor of 6 in the volume is left out because it cancels out because of the division.
So, loop over each face, compute the volume contribution and add that onto a total volume counter and also multiply it by that average position (you can move the division by 4 outside of the sum to make the loop faster; that way only one division needs to be performed instead of one for each face) of the tetrahedron (i.e. multiply it by the sum of the vertices).
Then, outside of the loop, divide by the total volume times 4 (assuming you moved that division by 4 outside of the loop).
You now have the centroid assuming constant density.
The variables are all vectors.
The product of the three vectors is a triple scalar product.
It's the dot product with the cross product (and also the determinant of the matrix of these vectors).
It's equal to this, which can be used as a formula for the volume:
This is because one of the vertices of every tetrahedron is the origin.
Note that this does not require the model to be convex.
All the tetrahedrons are not necessarily entirely contained inside the model.
I only assume that the model is closed and simple.
Any tetrahedrons outside the model will have their area computed to be negative.
They'll subtract off their contributions.
The origin of the system can be anywhere as well.
It doesn't have to be inside the model.
Here is the entire pseudo-code:
var cx, cy, cz, volume, v, i, x1, y1, z1, x2, y2, z2, x3, y3, z3;
volume = 0;
cx = 0; cy = 0; cz = 0;
// Assuming vertices are in vertX[i], vertY[i], and vertZ[i]
// and faces are faces[i, j] where the first index indicates the
// face and the second index indicates the vertex of that face
// The value in the faces array is an index into the vertex array
i = 0;
repeat (numFaces) {
x1 = vertX[faces[i, 0]]; y1 = vertY[faces[i, 0]]; z1 = vertZ[faces[i, 0]];
x2 = vertX[faces[i, 1]]; y2 = vertY[faces[i, 1]]; z2 = vertZ[faces[i, 1]];
x3 = vertX[faces[i, 2]]; y3 = vertY[faces[i, 2]]; z3 = vertZ[faces[i, 2]];
v = x1*(y2*z3 - y3*z2) + y1*(z2*x3 - z3*x2) + z1*(x2*y3 - x3*y2);
volume += v;
cx += (x1 + x2 + x3)*v;
cy += (y1 + y2 + y3)*v;
cz += (z1 + z2 + z3)*v;
i += 1;
}
// Set centroid coordinates to their final value
cx /= 4 * volume;
cy /= 4 * volume;
cz /= 4 * volume;
// And, just in case you want to know the total volume of the model:
volume /= 6;
Remember that the vertices of each face must be oriented in the same way (one way to see if they are is to turn on back face culling; if the model appears normally then they're all oriented properly--the model could also appear inside-out which would also be a valid orientation).
Part of this is actually calculating the area of each triangle without having to figure out the angles, simply using the 3 side lengths.
One way to achieve this is using Heron's Formula:
A = sqrt( s * (s - a) * (s - b) * (s - c) )
Here a, b, c are the lengths of each side and s is the semi-perimeter, s = (a + b + c) / 2.
You can also just take the magnitude of the cross product of two edge vectors of the face and divide it by 2.
This means that you can find the area of a face as a side effect of determining its normal vector, just as I do in my
GetSignedPolygonArea method.
Revit Implementation and CentroidVolume Class
I used the algorithm described above to implement a volume and centre of mass calculation for a Revit solid.
As said, curved solids are approximated by triangulating them.
The result is pretty precise for solids that are planar faceted to start with.
The code includes an assertion to ensure that the volume calculated as a side effect is not too far removed from the value provided by the Solid.Volume property.
Since the algorithm above calculates both the centroid and volume, I implemented the following little helper class to package the two together:
My translation of the algorithm described above to Revit API looks like this, including the validation assertions at the end and an exception handler around the call to TessellateSolidOrShell for reasons explained below:
CentroidVolume GetCentroid( Solid solid )
{
CentroidVolume cv = newCentroidVolume();
double v;
XYZ v0, v1, v2;
SolidOrShellTessellationControls controls
= newSolidOrShellTessellationControls();
controls.LevelOfDetail = 0;
TriangulatedSolidOrShell triangulation = null;
try
{
triangulation
= SolidUtils.TessellateSolidOrShell(
solid, controls );
}
catch( Autodesk.Revit.Exceptions
.InvalidOperationException )
{
returnnull;
}
int n = triangulation.ShellComponentCount;
for( int i = 0; i < n; ++i )
{
TriangulatedShellComponent component
= triangulation.GetShellComponent( i );
int m = component.TriangleCount;
for( int j = 0; j < m; ++j )
{
TriangleInShellComponent t
= component.GetTriangle( j );
v0 = component.GetVertex( t.VertexIndex0 );
v1 = component.GetVertex( t.VertexIndex1 );
v2 = component.GetVertex( t.VertexIndex2 );
v = v0.X*(v1.Y*v2.Z - v2.Y*v1.Z)
+ v0.Y*(v1.Z*v2.X - v2.Z*v1.X)
+ v0.Z*(v1.X*v2.Y - v2.X*v1.Y);
cv.Centroid += v * (v0 + v1 + v2);
cv.Volume += v;
}
}
// Set centroid coordinates to their final value
cv.Centroid /= 4 * cv.Volume;
XYZ diffCentroid = cv.Centroid
- solid.ComputeCentroid();
Debug.Assert( 0.6 > diffCentroid.GetLength(),
"expected centroid approximation to be "
+ "similar to solid ComputeCentroid result" );
// And, just in case you want to know // the total volume of the model:
cv.Volume /= 6;
double diffVolume = cv.Volume - solid.Volume;
Debug.Assert( 0.3 > Math.Abs(
diffVolume / cv.Volume ),
"expected volume approximation to be "
+ "similar to solid Volume property value" );
return cv;
}
As you may have noticed, the assertion limits are pretty arbitrarily chosen.
Support for Multiple Solids
Since the centroid of any given element will be determined by all of its solids, which may be multiple, I implemented the following GetCentroid method to determine all solids belonging to a given element and calculate its total centroid and volume across all its component solids:
///<summary>/// Calculate centroid for all non-empty solids /// found for the given element. Family instances /// may have their own non-empty solids, in which /// case those are used, otherwise the symbol geometry./// The symbol geometry could keep track of the /// instance transform to map it to the actual /// project location. Instead, we ask for /// transformed geometry to be returned, so the /// resulting solids are already in place.///</summary>CentroidVolume GetCentroid(
Element e,
Options opt )
{
CentroidVolume cv = null;
GeometryElement geo = e.get_Geometry( opt );
Solid s;
if( null != geo )
{
// List of pairs of centroid, volume for each solidList<CentroidVolume> a
= newList<CentroidVolume>();
Document doc = e.Document;
if( e isFamilyInstance )
{
geo = geo.GetTransformed(
Transform.Identity );
}
GeometryInstance inst = null;
CentroidVolume cv1;
foreach( GeometryObject obj in geo )
{
s = obj asSolid;
if( null != s
&& 0 < s.Faces.Size
&& SolidUtils.IsValidForTessellation( s )
&& (null != ( cv1 = GetCentroid( s ) ) ) )
{
a.Add( cv1 );
}
inst = obj asGeometryInstance;
}
if( 0 == a.Count && null != inst )
{
geo = inst.GetSymbolGeometry();
foreach( GeometryObject obj in geo )
{
s = obj asSolid;
if( null != s
&& 0 < s.Faces.Size
&& SolidUtils.IsValidForTessellation( s )
&& (null != ( cv1 = GetCentroid( s ) ) ) )
{
a.Add( cv1 );
}
}
}
// Get the total centroid from the partial// contributions. Each contribution is weighted// with its associated volume, which needs to // be factored out again at the end.if( 0 < a.Count )
{
cv = newCentroidVolume();
foreach( CentroidVolume cv2 in a )
{
cv.Centroid += cv2.Volume * cv2.Centroid;
cv.Volume += cv2.Volume;
}
cv.Centroid /= a.Count * cv.Volume;
}
}
return cv;
}
The calculation of the total volume and centroid is similar to the base algorithm described above, with each centroid contribution weighted by the corresponding volume.
Mainline with Picking Twists
Finally, here is the external command Execute method implementation driving this and including some additional useful picking interaction handling twists, such as:
Support for pre-selection or interactive picking.
Checking for a valid active view type before prompting the user to pick an element.
Exception handling to gracefully terminate picking and the entire command on cancels.
The resulting code looks like this:
publicResult Execute(
ExternalCommandData commandData,
refstring message,
ElementSet elements )
{
UIApplication uiapp = commandData.Application;
UIDocument uidoc = uiapp.ActiveUIDocument;
Application app = uiapp.Application;
Document doc = uidoc.Document;
List<ElementId> ids = newList<ElementId>();
Selection sel = uidoc.Selection;
SelElementSet set = sel.Elements;
if( 0 < set.Size )
{
foreach( Element e in set )
{
ids.Add( e.Id );
}
}
else
{
if( ViewType.Internal == doc.ActiveView.ViewType )
{
TaskDialog.Show( _caption,
"Cannot pick elements in this view: "
+ doc.ActiveView.Name );
returnResult.Failed;
}
try
{
IList<Reference> refs = sel.PickObjects(
ObjectType.Element,
"Please select some elements" );
foreach( Reference r in refs )
{
ids.Add( r.ElementId );
}
}
catch( Autodesk.Revit.Exceptions.OperationCanceledException )
{
returnResult.Cancelled;
}
}
Options opt = app.Create.NewGeometryOptions();
foreach( ElementId id in ids )
{
Element e = doc.GetElement( id );
CentroidVolume cv = GetCentroid( e, opt );
Debug.Print( "{0} {1}",
(null == cv ? "<nil>" : cv.ToString()),
ElementDescription( e ) );
}
returnResult.Succeeded;
}
Results
I ran this on the basic sample model delivered with Revit, rac_basic_sample_project.rvt, simply selecting all elements on level 1.
Not surprisingly, this causes a couple of hiccups, notably a few assertions stating that my volume approximation does not match Revit's and an exception thrown by the TessellateSolidOrShell method, in spite of calling IsValidForTessellation beforehand to ensure tht I only feed in acceptable solids to it.
This exception is caused by the teapot in the model, which is in fact a rather abnormal building element, but still...
These hiccups can be ironed out.
Here is the report logged to the debug output window with the hiccups and exception messages manually edited away, informing us that a large number of elements in the model are correctly processed and that for all of them my calculated approximate values correspond more or less to the ones reported by the built-in Revit API methods;
copy and paste to an editor to see the truncated lines in full:
This is more than enough for me and my rather quick and dirty exploration of this.
Improved filtering and error handling is left as an exercise to the inclined reader.
Download
Here is
GetCentroid.zip containing
the complete source code, Visual Studio solution and add-in manifest for this command.
You can close an open Revit document programmatically.
Unfortunately, though, this only works for all except the last one.
People keep running into this issue, of course.
It even motivated René Gerlach to implement a workaround using SendKeys.SendWait to send an Alt-F4 keystroke to Revit to
close the active window,
and Arnošt Löbel to
warn gravely against its use and even publish a
Windows message workaround disclaimer.
Well, happily, such an adventurous workaround is not at all required.
Another workaround making use only of fully supported API calls is obvious and simple, once you think of it: if you really want to close the current document, and it happens to be the last one, all you have to do is open some other document first.
You can use a dummy document for this purpose.
Once it has been opened, the other 'real' document can be closed.
Steven Mycynek of the Revit API development team provided a sample add-in named UiClose implementing this.
Here is the description in his own words:
Enclosed is some sample code I use to close the active document through the API.
I am sure it has some limitations and bugs, but it's a nice start.
To use, simply install the UiClose macro to your macros folder and place the _placeHolder_.rvt dummy model into C:/uiclose or some other location where the main macro can find it.
The demo application closes the active document, opening _placeholder_.rvt when necessary, and will not accidentally close the placeholder document.
It might also be possible to do something with events in an application to make this more automatic, but this wraps it up for now.
Steve implemented this as a SharpDevelop macro.
The macro files need to be placed in the appropriate
macro directory:
Windows XP: C:\Documents and Settings\All Users\Application Data\Autodesk\Revit\Macros\<release>\<product>\VstaMacros
Windows 7: C:\ProgramData\Autodesk\Revit\Macros\<release>\<product>\VstaMacros
In my case, this resolves to
C:\Documents and Settings\All Users\Application Data\Autodesk\Revit\Macros\2013\Revit\VstaMacros
Actually, to see the UiClose macro in the SharpDevelop IDE, I had to place the UiClose folder into the AppHookup subdirectory, so its full path ends up as:
C:\Documents and Settings\All Users\Application Data\Autodesk\Revit\Macros\2013\Revit\VstaMacros\AppHookup\UiClose
Now I see the UiClose module and its CloseActiveDocDemo macro in the Manage > Macros > Macro Manager:
When I run the macro, the current active document is closed.
If it is the last one, the placeholder document is opened in its stead.
If it has unsaved changes, a dialogue box prompting me whether to save or not is displayed.
The macro could obviously be improved to check for that before trying to close the document to avoid such an interruption, or handle it more gracefully.
The macro itself is a trivial one-liner, since it just calls the CloseAndSave method provided by the CloseHelper helper class:
The rest of the ThisApplication class methods deal with setting up and managing the helper class:
publicpartialclassThisApplication
{
privatevoid Module_Startup(
object sender,
EventArgs e )
{
m_CloseHelper = new UiCloseHelper(
this,
@"C:\\uiClose\\_placeholder_.rvt" );
}
privatevoid Module_Shutdown(
object sender,
EventArgs e )
{
}
#region Revit Macros generated code
privatevoid InternalStartup()
{
this.Startup += new System.EventHandler(
Module_Startup );
this.Shutdown += new System.EventHandler(
Module_Shutdown );
}
public UiCloseHelper CloseHelper
{
get
{
return m_CloseHelper;
}
}
private UiCloseHelper m_CloseHelper;
#endregion
}
So finally, let's take a look at the UiCloseHelper implementation in all its glory:
///<summary>/// A helper class to keep a placeholder document /// open in Revit to allow closing the active /// document in Revit.///</summary>publicclassUiCloseHelper
{
///<summary>/// Constructor -- initializes object with /// a UIApplication and location of a /// placeholder document.///</summary>///<param name="uiAppInterface">The Revit UI Application, pass either a UIApplication or an ApplicationEntryPoint</param>///<param name="placeholderPath">Path to a dummy .rvt file.</param>public UiCloseHelper(
dynamic uiAppInterface,
string placeholderPath )
{
m_uiAppInterface = uiAppInterface;
if( m_uiAppInterface == null )
{
thrownewArgumentNullException(
"m_uiAppInterface" );
}
if( ( !( uiAppInterface is Autodesk.Revit.UI.UIApplication ) )
&& ( !( uiAppInterface is Autodesk.Revit.UI.Macros.ApplicationEntryPoint ) ) )
{
thrownewArgumentException(
"Must pass a UIApplication or ApplicationEntryPoint." );
}
SetPlaceHolderPath( placeholderPath );
}
///<summary>/// Closes a UIDocument with the same /// interface as UIDocument.Close///</summary>///<param name="uiDoc">The document to close</param>///<returns>True if successful, false otherwise.</returns>publicbool CloseAndSave(
Autodesk.Revit.UI.UIDocument uiDoc )
{
if( uiDoc == null )
{
thrownewArgumentNullException( "uiDoc" );
}
RejectClosingPlaceHolder( uiDoc );
if( !EnsureActivePlaceHolder( m_uiAppInterface ) )
returnfalse;
return uiDoc.SaveAndClose();
}
///<summary>/// Closes a DB.Document with the same /// interface as DB.Document.Close///</summary>///<param name="doc">The DB.Document to close</param>///<returns>True if successful, false otherwise.</returns>publicbool Close( Autodesk.Revit.DB.Document doc )
{
return Close( doc, true );
}
///<summary>/// Closes a DB.Document with the same /// interface as DB.Document.Close///</summary>///<param name="doc">The DB.Document to close</param>///<param name="saveModified">true to save the document if modified, false to not save.</param>///<returns>True if successful, false otherwise.</returns>publicbool Close(
Autodesk.Revit.DB.Document doc,
bool saveModified )
{
if( doc == null )
{
thrownewArgumentNullException( "doc" );
}
RejectClosingPlaceHolder( doc );
if( !EnsureActivePlaceHolder( m_uiAppInterface ) )
returnfalse;
return doc.Close( saveModified );
}
#region Helper methods
///<summary>/// Throws an exception if the user /// passes the placeholder document.///</summary>///<param name="doc">The document to test to ensure it is not the placeholder document.</param>privatevoid RejectClosingPlaceHolder(
Autodesk.Revit.DB.Document doc )
{
if( doc.Title == System.IO.Path.GetFileName(
this.m_placeHolderPath ) )
{
thrownewArgumentException(
"Cannot close placeholder doc: " + doc.Title );
}
}
///<summary>/// Throws an exception if the user /// passes the placeholder document.///</summary>///<param name="doc">The document to check</param>privatevoid RejectClosingPlaceHolder(
Autodesk.Revit.UI.UIDocument uiDoc )
{
RejectClosingPlaceHolder( uiDoc.Document );
}
///<summary>/// Ensures that the placeholder document is open /// and active to that another open document can be closed.///</summary>privatebool EnsureActivePlaceHolder(
dynamic application )
{
try
{
if( m_placeHolderDoc == null )
{
m_placeHolderDoc = application
.OpenAndActivateDocument( m_placeHolderPath );
}
else
{
m_placeHolderDoc.SaveAndClose();
m_placeHolderDoc = application
.OpenAndActivateDocument( m_placeHolderPath );
}
returntrue;
}
catch( Exception )
{
returnfalse;
}
}
///<summary>/// Sets the path of the placeholder document.///</summary>///<param name="path">Path to placeholder/dummy document.</param>privatevoid SetPlaceHolderPath( string path )
{
m_placeHolderPath = path;
if( !System.IO.File.Exists( m_placeHolderPath ) )
{
thrownewArgumentException(
"File not found: " + path );
}
if( !( System.IO.Path.GetExtension( m_placeHolderPath ) != "rvt" ) )
{
thrownewArgumentException( "Placeholder: "
+ path + " is not a revit file." );
}
}
#endregion #region Data
private Autodesk.Revit.UI.UIDocument m_placeHolderDoc;
privatestring m_placeHolderPath;
privatedynamic m_uiAppInterface;
#endregion
}
It reads a lot harder than it actully is :-)
Here is
UiClose.zip
containing the macro source code and its associated
placeholder
project document.
Many thanks to Steve for providing this useful and interesting workaround!
Conversion of a filtered element collector to an explicit .NET collection of elements or element ids is always costly and mostly avoidable and unnecessary.
Before getting to the details of that issue, here is another quick snapshot from our travels to the West European developer conferences.
As you either know or might guess, one of the main topics of these is cloud and mobile development, e.g. looking at topics such as the
BIM 360 Glue REST API and SDK and
my hands-on exploration of how to
access and use it via Python.
Funnily enough, the lighting in the Paris Charles de Gaulle airport is designed along a cloud scheme, so here is very low quality picture of us almost touching the clouds at the airport gate preparing to board the flight to Milano:
Ora siamo arrivati in Milano per un'altra conferenza di sviluppatori.
One of them is the fact that the conversion from a filtered element collector to a .NET collection is costly and should be avoided if possible.
Here is another observation related to that, brought up by Guy Robinson and answered by Scott Conover of the Revit API development team:
Question: I sometimes use the ToElementIds method, e.g. like this:
var fc = newFilteredElementCollector( doc )
.OfSomething()
.ToElementIds();
foreach( var elemId in fc )
{
var element = doc.GetElement( elemId );
}
I noticed that this is about 25% slower than accessing the elements directly as follows:
var fc = newFilteredElementCollector( doc )
.OfSomething();
foreach (var elem in fc)
{
var element = elem;
}
I am wondering why the ToElementIds approach is so much slower?
Shouldn't the two approaches be equivalent?
Is there any good reason to use ToElementIds at all?
Not surprisingly, this ~25% slower figure has been the same on my tests ever since I started comparing in Revit 2012.
Answer: This is not unexpected.
ToElementIds and ToElements allocate a new concrete .NET collection containing the elements passing the filter.
Then it returns this allocated collection to you, which involves a conversion from a native level collection to a managed object.
This will take extra time as compared to a direct iteration.
Why might you use the ToElementIds variants?
Well, you might need a concrete collection to store in memory.
Or you might need a collection to pass as input to an API method like Delete – which you should definitely not call from the middle of a foreach iteration unless you like unexpected behaviour.
This provides a shortcut to building this allocation.
For any sort of operation of the type "I want to read properties of each passing element" you can skip ToElements and the extra collection allocation.
Thank you Scott for these insights!
For one example of how this can be achieved, look at the aforementioned
FindElement and collector optimisations.
There are lots of other examples in many of The Building Coder discussions, since I consciously and consistently avoid this conversion whenever possible, i.e. almost always.
Here is a quick little post, with a surprisingly short and simple answer to a short and seemingly difficult Revit API question.
Before getting to that, I will just mention that I arrived safe and sound in Paris for a developer conference here.
I had a walk in the sunset on the north bank of the Seine from the Gare de Lyon along the industrial areas on Quai de Bercy and crossed the river over the new-built Pont National to reach the rue de Tolbiac by a rather roundabout route.
So, back to Revit; how can we easily retrieve all the elements listed in a schedule view?
Question: The Revit 2013 API finally enabled the creation of schedule views, which is great.
I would also need to query that schedule and retrieve all the element ids of the elements contained in it.
Currently, I am using the workaround of exporting the schedule and then comparing with elements within the project, which takes a considerable amount of time.
Is there any simpler way to achieve this?
Answer: Yes, there is.
The schedule is a view, and its view id can be passed in to the filtered element collector just like any other.
That will return the relevant elements.
Sorry you had to go to all that unnecessary trouble :-)
Many thanks to
Guy Robinson for
pointing this out!
I provided an overview of the
BIM 360 Glue REST API and SDK last
Friday and hinted at upcoming further exploration.
Well, here it is already.
Due to Autodesk University and the world-wide developer conferences, I had to skip my last education day, but this stuff was too exciting to wait any longer :-)
So, unwilling to go for any length of time without trying out something new, I played a bit with the Glue API anyway.
For fun, I will describe here stepping through the exploration of the Glue authentication process completely manually, making use of the Python programming language and a handy library which probably provides an easier access to the REST API than you imagined possible.
Here are the steps:
Looking for an easy way to manually interact with REST, I immediately turned to Python and found the
requests library,
which describes itself as an 'awesome Python HTTP library that's actually usable'.
I would agree that is a fair assessment.
Get the Google Page
Here is an example showing how simple it is to issue an HTTP request from scratch, including launching the Python interpreter from the command line; basically, it uses one single line of code, calling the method requests.get with the desired URL:
$ python
Python 2.7.2 (default, Jun 20 2012, 16:23:33)
[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> r = requests.get('http://google.com')
>>> print r
<Response [200=""]>
>>> r.headers['content-type']
'text/html; charset=ISO-8859-1'
>>> r.content
'<!doctypehtml>
<htmlitemscope="itemscope" itemtype="http://schema.org/WebPage">
<head>
<metacontent="Search the world\'s information, including webpages, images, videos and more. Google has many special features..."
The API key and secret need to be requested from Autodesk.
Currently, there is no official developer program running for Glue.
You can however buy a normal user account and ask for additional developer access based on that.
As we can see from the documentation, in addition to the API key and secret, plus the normal user account login credentials, the authentication requires a timestamp, more precisely a Unix epoch timestamp using GMT time, the number of seconds since the Unix epoch, January 1 1970 00:00:00 GMT.
The API key and secret are concatenated with the timestamp and encoded using an MD5 cryptographic hash to create a signature, which also has to be sent with the request.
Timestamp and MD5 Digest
Luckily, Python can easily support us in providing the timestamp and signature components.
The timestamp can be generated like this using the time module:
import time
def expires():
'''return a UNIX style timestamp representing 5 minutes from now'''return int(time.time()+300)
The Python Standard Library Cryptographic Services includes the MD5 message digest algorithm 'md5', so that is also easily taken care of.
Following the example given in the Glue API documentation, I created the concatenation and digest of the following items:
The hexadecimal digest exactly matches the signature string listed in the Glue documentation example, so we seem to be on the right track so far.
More Login Credentials
Studying the documentation further, we end up at the nitty-gritty internals of the
Security Service: Login request,
specifying the following full list of required parameters:
format
login_name
password
company_id
api_key
api_secret
timestamp
sig
Actually, I intuitively fixed an error or two when transferring this list; e.g. the secret was not mentioned at this point, and the timestamp has a wrong description associated with it.
So do what every programmer always has to do: ignore the documentation (but only some of it!), trust your own insight, take everything with a grain of salt, and use your brains, intuition and good taste.
By the way, the user name and password required here are the Autodesk id single sign-on credentials, also known as SSO, formerly Autodesk unique login or AUL.
I initially tried to use a GET request and was kindly informed by a suitable error message that I should be using POST instead.
Successful Authentication
I ran into a couple of other not unexpected issues as well, and finally ended up with this method to construct the authentication POST request:
Of special interest to AEC developers, the BIM 360 Glue platform includes support for programming interfaces and a software development kit, the
BIM 360 Glue SDK,
providing a set of tools for developers to interface with it.
The programming support consists of two distinct components:
Display component: embeddable 3D viewer to provide visual access to models within custom third party web applications.
Web services API: REST based service data access to users, projects, models, etc.
Display Component
The Glue Viewer is an embeddable component used to show 3D models from the Glue platform
Viewer parameters allow control of the GUI, i.e. application controls such as navigation, display lists, etc., so the viewer experience can be customised by developers.
The viewer currently supports Windows 32 and 64 bit platforms, just like the Glue web application.
Web Services API
The Glue web services API provides a RESTful data access interface to query and modify data and return JSON or XML.
It enables developers to integrate external applications, e.g., project management systems, accounting systems and custom developed solutions with the BIM 360 Glue platform.
Glue stores information about user interactions within the system.
These operations are referred to as 'Actions'.
Typical Actions can be:
Uploading a model
Creating a view
Adding mark-ups
Creating a clash report
The Glue Web Service API returns many of these actions in API responses.
Actions can be loaded to the viewer to show the user the exact view and state of a model when the action was performed.
Structure of a Client Request
For a typical REST API request, the client sends a HTTP/HTTPS request, POST or GET, depending on the particular service call.
Required parameters are sent to the server for authentication.
An example URL for a login request could be something like:
With the SDK, partners and customers can build a variety of fully interactive web based custom applications and integrations taking advantage of the Glue platform.
A simple call flow for a custom application might be:
Perform user authentication.
Query and list available models.
Use the Glue viewer to display the model within the custom application.
This enables tasks like system integration, project management, accounting, etc.
The web services API provides full data access to system objects:
Users
Projects
Models
Actions
An application can automatically update projects, models, etc. with information from external systems.
Basically, we are looking at the following architectural hierarchy, from bottom to top:
You will need an API Key and an API Secret.
These are unique identifiers assigned for each user, company, or partner.
Furthermore, a Glue developer account is required, a user name and password.
A normal user account can be converted to a developer one on request.
There are even further levels: a default developer account works within the privileges of normal glue user, whereas a privileged developer account can perform requests on behalf of other users as well.
The authentication to the BIM 360 Glue Platform starts with an API key and secret assigned by Autodesk.
Once you have these two pieces of information, API calls can be authenticated to the BIM 360 Glue Platform in one of two ways:
User name and password; using the API key and secret, a signed request including the user name and password is sent to the BIM 360 Glue Service, which returns an authorisation token.
The authorisation token is used for all subsequent calls.
Here is a quick reference to further sources of information and two examples:
We explored and learned a lot of interesting areas during the DevLab at AU on Tuesday last week.
One recurring theme is how to effectively debug an add-in without having to restart Revit and reload the entire model each time a change is made to the source code.
A long, long time back, right in the beginning of the Revit .NET API, it was briefly possible to use the Visual Studio Edit and Continue feature for that.
That was obviously an absolutely perfect solution.
Alas, those times are past, and unlikely to return.
I almost find it hard to believe I am not imagining things.
There are in fact several other known solutions to this problem, which I would classify as follows:
Stupid: set up the Revit project and Visual Studio debug settings to bring you to the exact required debugging position and context in one F5 'start debugging' click.
Every time a code modification is required, simply stop debugging, modify the code, recompile and restart Revit.
That is the approach I use myself, since I can get by working in absolutely minimal test models.
Cool: use an interactive scripting language such as
Python or
Ruby.
In fact, the wish be able to modify the API code interactively without requiring the tedious recompile and reload cycle was one of the main motivating factors in implementing the latter.
The use of this functionality is obviously just a side effect of the much larger main advantage of being able to play interactively with the code in complete freedom, since it is interpreted on the fly and no compilation at all is required.
Effective and laborious: convert the code to be debugged to a macro and use the built-in SharpDevelop IDE.
This means that you convert the part of your add-in that your are debugging and modifying to a macro.
The rest can be left as a compiled DLL.
The two components can communicate with each other.
When finished debugging, you move the code out of the macro environment into the external DLL again.
Efficient: use the
AddInManager and
attach to process to reload repeatedly, with compile and edit cycles.
Melissa Manalac of
S-frame
was making such efficient use of the AddInManager approach at the DevLab that I asked her to document it for you once again.
She points out that James LeVieux already summarized the process in his
comment on
that discussion:
Here are the required steps:
Start Revit 2013.
Before coding, make sure all processes are detached.
After coding, rebuild your solution and attach to the Revit process.
In Revit, go to the Add-Ins tab > Add-In Manager and reload the desired add-in DLL.
Run desired external command.
Repeat steps 2 to 5.
You also need to be aware that the AddInManager requires the external commands it loads to use manual transaction mode.
This is no significant restriction, since
automatic transaction mode is not recommended anyway.
Here are some screen snapshots to further clarify these steps:
1. Start Revit 2013.
2. Before coding, make sure all processes are detached:
3. After coding, rebuild the add-in solution and attach to Revit process.
Rebuild solution:
Attach to process:
Select the Revit.exe process:
4. In Revit, go to the Add-Ins tab > Add-In Manager and reload the desired add-in DLL.
Launch the Add-in Manager:
Load the add-in DLL:
5. Run the desired external command:
6. Repeat steps 2 to 5 as desired.
Many thanks to Melissa for the detailed up-to-date description!
Source Code Coloriser
As I mentioned quite a while back, I use the Visual Studio
CopyToHtml plug-in
to copy and paste colour coded C# and VB source code to HTML for my blog posts.
Harri Mattison of
Boost Your BIM now
pointed out that this utility obviously cannot be used in the built-in Revit SharpDevelop macro environment.
Oh, and I found another one myself,
tohtml.com,
that supports a huge number of different languages besides C#.
Unfortunately, it does not escape all the HTML characters, e.g. '<' remains '<' instead of being converted to '<'.
Advent, advent!
Yet another Revit API focused blog is born!
Harry Mattison, ex Revit API development team, has initiated
Boost Your BIM and
kicked it off with a series on using the built-in
SharpDevelop development
environment Revit macros to resolve duplicate tags:
Thank you, Harry, and the best of luck to you and your new blog!
Dances with Elephants
Also, for a more high-level strategic vision of things, don't forget to take a look now and then at Jim Quanci's
Dances with Elephants.
Jim analyses how small companies can leverage big ones to help build their business, useful tactics eminently applicable to many Revit add-in developers.
He recently posted a number of exciting new essays, so I encourage you to go take a
gander.
Whenever your add-in modifies the model in any way and you wish to query the Revit database, you need to pay careful attention to ensure that you do not retrieve stale or invalid data.
If anything unexpected whatsoever happens, one of the first things to consider is the possible need for a document regeneration or additional separate transactions between steps.
Question: I have a duct system and want to attach my tag to its elements, using the code to
set a tag type.
I tried something like this:
doc.LoadFamilySymbol(
[my_tag_family.rfa],
[type_name],
out tagType);
IndependentTag tag = doc.Create.NewTag(
doc.ActiveView,
element,
true,
TagMode.TM_ADDBY_CATEGORY,
TagOrientation.Horizontal,
XYZ.Zero);
tag.ChangeTypeId(tagType.Id);
This code works fine in some projects, whereas in others Revit throws an InvalidOperationException saying that there is no tag available in the call to the NewTag method.
The behaviour seems to vary between different projects and on different elements.
How can that be?
Answer: Have you tried either regenerating the document or placing the tag symbol loading and the tag creation calls in separate transaction, committed individually?
Response: Problem solved.
As it turns out, the Revit project needs to have some suitable tag type loaded before calling NewTag.
Your transaction hint was very helpful.
My earlier code was not enough as it stands.
It actually requires separate transactions to load the tag type and add a new tag if there was no other tag loaded before.
I did not previously know what Revit meant by 'there is no tag available'.
Furthermore, I was initially using automatic transaction mode for this command, thinking that would free me from worrying about any transaction handling myself at all.
I now learned that the automatic transaction mode does not enclose each action in a separate transaction, but wraps all executed functions in the whole external command into one single one.
In this case, such an approach is not usable at all.
Many thanks to Max for raising this issue and verifying that the solution works!
Extra Regeneration Required to Populate Materials
One of the issues we took a look at during the DevLab at AU last Tuesday was the list of materials attached to a newly duplicated symbol.
At first glance, it appeared that a duplicated family symbol had zero entries in its list of materials, whereas the original symbols material list was correctly populated.
On further exploration, we discovered that this is another case of retrieving stale data after a model modification.
The behaviour is easily rectified by a call to regenerate the model after the symbol duplication.
Here is the final implementation of the external command implementing to test the issue.
It expects a structural steel column to be pre-selected, which is a family instance whose associated symbol has exactly one entry in its materials list.
We duplicate the symbol.
This requires defining a new name.
To simplify repeated testing, we generate a new name by simply appending the clock tick counter to the original one.
Directly after the call to Duplicate, the new symbol's material list has zero entries, which makes no sense for this kind of symbol.
A call to Regenerate fixes that:
[Transaction( TransactionMode.Manual )]
publicclassCommand : IExternalCommand
{
publicResult Execute(
ExternalCommandData commandData,
refstring message,
ElementSet elements )
{
UIApplication uiapp = commandData.Application;
UIDocument uidoc = uiapp.ActiveUIDocument;
Application app = uiapp.Application;
Document doc = uidoc.Document;
Selection sel = uidoc.Selection;
FamilyInstance inst = null;
if( 1 == sel.Elements.Size )
{
foreach( Element e in sel.Elements )
{
inst = e asFamilyInstance;
break;
}
}
if( null == inst )
{
message = "Please select one "
+ "single structural column";
returnResult.Failed;
}
using( Transaction tx
= newTransaction( doc ) )
{
tx.Start( "Duplicate Symbol" );
FamilySymbol symbol = inst.Symbol;
string name = symbol.Name
+ DateTime.Now.Ticks.ToString();
int nMaterialsBefore
= symbol.Materials.Size;
symbol = inst.Symbol.Duplicate( name )
asFamilySymbol;
// The model is in a temporary state that does // not make sense. Regenerate to clean this up.int nMaterialsAfter
= symbol.Materials.Size;
doc.Regenerate();
nMaterialsAfter = symbol.Materials.Size;
Debug.Assert(
nMaterialsAfter.Equals( nMaterialsBefore ),
"why does the material get lost, please?" );
tx.Commit();
}
returnResult.Succeeded;
}
}
For completeness' sake, here is
DuplicatedSymbolLosesMaterial.zip containing
the complete source code, Visual Studio solution and add-in manifest of this little test command.