One of the highlights mentioned in the
overview of the Revit 2014 API is
the FreeForm element API that enables modification of solid geometry imported from DWG or SAT.
Several other new DWG and DXF related features include import of DXF markup, import and link of SAT and SketchUp, and access to the DWG, IFC and DGN layer, linetype, lineweight, font and pattern tables.
One result of these enhancements is the possibility to access 3D coordinates in DWG import for the first time.
Besides looking at that, I'll also point out a few other issues further down:
One DWG import aspect that has often been requested by developers in the past is the ability to explode 3D DWG files and obtain the resulting coordinates in the Revit project.
Revit 2014 families now provide the ability to explode and retain such 3D geometry.
In Revit 2013, one alternative way to achieve this was to implement an AutoCAD add-in that analyses the geometry and exports the 3D coordinate data.
A separate Revit add-in could read the coordinate data and correlate it with the Revit geometry.
Differentiate Label and Text in DWG Export
On a related topic, how can I differentiate between Revit text and label elements after they have been exported to DWG?
Revit Export to DWG results in Labels becoming Text rather than Attributes, as most users might prefer.
It would be interesting to have some flag or embedded information in the exported text that could be used in an AutoCAD application to post-process the information received from Revit and restore the attribute information.
Happily, the exported Labels have Extended Entity Data like this attached:
The differing TypedValue 11 is consistently -2000300 for a text and -2000280 for a label element.
What does this mean?
Well, is actually quite easy.
On seeing these large negative numbers in this specific range, an experienced Revit developer will quickly suspect built-in category or parameter enumeration values.
You can check what they actually represent in the Visual Studio debugger, by jumping to the definition of these enumerations and searching for the specific values.
Looking back at an ancient blog post on the
DWG and DXF export Xdata specification
confirms that these numbers do indeed represent the built-in category of the source element and thus can be used to distinguish the two.
Many thanks to Dale Bartlett, CAD-BIM System Manager at
Atkins (Oman), for suggesting this topic.
Dale adds:
It will be interesting to see if this generates any comments.
As far as the AutoCAD geometry export, the purpose was to build a 3D Space Frame from an engineer’s analysis drawing.
I did in fact write an XML file from AutoCAD of the geometry and imported it into Revit to build the frame.
It took an hour to build 20,000 elements, which was about a year less than doing it manually!
I’ll revise it with 2014 to make it a one-step-inside-Revit process.
Element Ids in Extensible Storage
Question: I need to store ElementId data using schemas within my Revit add-in.
However, the API documentation states that "ids are subject to change during an Autodesk Revit session and as such should not be retained and used across repeated calls to external commands."
My add-in will be used in a worksharing environment.
My testing shows that if I try to save the integer value of an element id, and it changes, my saved id value will be invalid.
However, if I store the actual ElementId in a schema I do not have this problem.
Is saving the ElementId using a Schema safe?
If not, do you have any suggestions or examples of how to safely store an identifier to elements in a schema?
Answer: Yes, it is completely safe to store ElementId data in extensible storage for the purposes you describe.
The statement you quote refers to the fact that the integer value of an element id will change between sessions.
Basically, you can think of it as a pointer.
When stored as an element id in extensible storage, the actual links between the associated elements are stored, and the resulting integer values are recalculated from scratch in every new session, maintaining the links intact.
The integer value of an element id may be changed by worksharing updates as well, and these changes are also automatically handled for element ids stored in extensible storage, so it is perfectly safe to save them there.
All-zero Language Codes in the Revit Product GUID
Watch out for all-zero language code identifiers in the Revit Product GUID due to the new Multiple Language Interface, or MUI, used in Revit 2014.
This version includes RevitLookup and the AddInManager!
Visual Debugging Tools
Rudolf Honke of
Mensch und Maschine acadGraph GmbH discovered
a glitch in the definition of the room solids in one of the standard sample RVT project files.
Rudi says: When testing VRML exporter with Revit 2014, I noticed that the brand new rac_sample_project.rvt has some inconsistencies.
I export the ClosedShells of the rooms into VRML format to display then in my browser.
On the left side you see overlapping room volumes from 'Hall' and 'Entry Hall':
'Kitchen and dining' has an upper edge that needs to be cut off:
'Hall' has two ears protruding into the sky:
I noticed this when debugging the funny results my application was producing.
Since it is not the code, I concluded it must be the input that was corrupted…
Without a visual tool like this, it is quite hard to evaluate what's wrong.
Jeremy adds: You can also observe this using the Export > gbXML command:
'Kitchen and dining' has an upper edge that needs to be cut off:
'Hall' has two ears protruding into the sky:
Furthermore, the Revit 2014 program folder contains a utility named gbXML2dwfx.exe.
If you drag and drop a gbXML file onto this it will create a DWFx file that can be viewed in
Autodesk Design Review,
the free 2D and 3D DWF viewer, which provides more functionality than many VRML viewers.
Response: Interesting.
GbXML2Dwfx.exe appears to be new in Revit 2014.
Regarding the geometry, it is important to note that you cannot rely on geometrical integrity of your Revit Elements.
I faced this problem previously, not only with rooms but also with walls.
Every cut-off and every Boolean operation makes the geometry more complex, and rounding errors may occur.
Separation of symbol and instance data in my add-in and database structure: currently, the furniture loops are placed absolutely, and multiple instances of a symbol duplicate the same loop over and over again at different locations.
I will rewrite this to separate the furniture loop definition, defined by the family symbol, and its placement, defined by the instance.
This needs to be done anyway to enable editing the placement data through the editor interaction on the mobile device.
The
first implementation just
uploaded the room and furniture instance 2D boundary polygon loops, absolutely placed.
Besides converting that to defining and reusing symbols, I also added support for exporting the model and level data to the cloud.
I enhanced the database classes to include a new class DbSymbol.
It defines the family symbol loop, which is reused by the family instances.
The DbFurniture class no longer has its own individual loop data.
Instead, it has a reference to a symbol and placement data defining its 2D translation and rotation.
All the database classes are derived from the DbObj base class:
DbObj
Type
Description
Name
Some of the derived classes have no additional properties of their own at all.
The data structure is this as simple as this:
DbModel
DbLevel
ModelId
DbRoom
LevelId
Loops
ViewBox
DbSymbol
Loop
DbFurniture
RoomId
SymbolId
Transform
The source code does nothing but exactly reproduce this structure.
Here is the complete implementation of these classes:
///<summary>/// Base class for all Jeremy Room Editor classes.///</summary>classDbObj : CouchDocument
{
protected DbObj()
{
Type = "obj";
}
publicstring Type { get; protectedset; }
publicstring Description { get; set; }
publicstring Name { get; set; }
}
///<summary>/// Current model, i.e. Revit project.///</summary>classDbModel : DbObj
{
public DbModel()
{
Type = "model";
}
}
///<summary>/// Level.///</summary>classDbLevel : DbObj
{
public DbLevel()
{
Type = "level";
}
publicstring ModelId { get; set; }
}
///<summary>/// Room///</summary>classDbRoom : DbObj
{
public DbRoom()
{
Type = "room";
}
publicstring LevelId { get; set; }
publicstring Loops { get; set; }
publicstring ViewBox { get; set; }
}
///<summary>/// Family symbol, i.e. element type defining /// the geometry, i.e. the 2D boundary loop.///</summary>classDbSymbol : DbObj
{
public DbSymbol()
{
Type = "symbol";
}
publicstring Loop { get; set; }
}
///<summary>/// Family instance, defining placement, i.e./// transform, i.e. translation and rotation,/// and referring to the symbol geometry.///</summary>classDbFurniture : DbObj
{
public DbFurniture()
{
Type = "furniture";
}
publicstring RoomId { get; set; }
publicstring SymbolId { get; set; }
publicstring Transform { get; set; }
}
Database Upload
The database upload has not changed very much from the
previous version,
except that the model, level and symbol data is now added, and the furniture and equipment instances store their symbol reference and transformation:
I implemented the following simple class to manage the 2D placement.
It is based on the
2D point class,
storing the coordinate data in integers, for various reasons:
Revit precision is no smaller than one sixteenth of an inch, ca. 1.2 mm.
The data is stored in the cloud and rendered on a mobile device: the use of integers
eliminates rounding issues,
lowers the data volume,
enhances performance,
simplifies storage,
improves human reading and understanding.
Since I am already using millimetres for the length measurement, I find it fitting to store the rotation in degrees.
The SVG rendering expects degrees as input, anyway.
Here is the class implementation:
///<summary>/// A 2D integer-based transformation, /// i.e. translation and rotation.///</summary>classJtPlacement2dInt
{
///<summary>/// Translation.///</summary>publicPoint2dInt Translation { get; set; }
///<summary>/// Rotation in degrees.///</summary>publicint Rotation { get; set; }
///<summary>/// The family symbol UniqueId.///</summary>publicstring SymbolId { get; set; }
public JtPlacement2dInt( FamilyInstance fi )
{
LocationPoint lp = fi.Location asLocationPoint;
Debug.Assert( null != lp,
"expected valid family instanace location point" );
Translation = newPoint2dInt( lp.Point );
Rotation = (int) (
( ( 180 * lp.Rotation ) + 0.5 ) / Math.PI );
SymbolId = fi.Symbol.UniqueId;
}
///<summary>/// Return an SVG transform,/// either for native SVG or Raphael.///</summary>publicstring SvgTransform
{
get
{
returnstring.Format(
"R{2}T{0},{1}",
//"translate({0},{1}) rotate({2})",
Translation.X, Translation.Y, Rotation );
}
}
}
The placement is instantiated from a family instance, and returns a suitably formatted SVG transformation string, either for native SVG or the
Raphaël JavaScript SVG library.
Populating Symbols and Instances
To populate the symbol and instance data, I loop over all the instances exactly like I did previously.
Now, instead of exporting the family instance boundary loop in situ, I transform it back to the symbol definition coordinate system instead, and save that in a dictionary mapping the family symbol UniqueId to its boundary loop.
The family instance exports a reference to that symbol and its placement data:
List<Element> furniture
= GetFurniture( room );
// Map symbol UniqueId to symbol loopDictionary<string, JtLoop> furnitureLoops
= newDictionary<string, JtLoop>();
// List of instances referring to symbolsList<JtPlacement2dInt> furnitureInstances
= newList<JtPlacement2dInt>(
furniture.Count );
int nFailures;
foreach( FamilyInstance f in furniture )
{
FamilySymbol s = f.Symbol;
string uid = s.UniqueId;
if( !furnitureLoops.ContainsKey( uid ) )
{
nFailures = 0;
JtLoops loops = GetPlanViewBoundaryLoops(
f, ref nFailures );
if( 0 < nFailures )
{
Debug.Print( "{0}: {1} extrusion analyser failure{2}",
Util.ElementDescription( f ), nFailures,
Util.PluralSuffix( nFailures ) );
}
ListLoops( f, loops );
if( 0 < loops.Count )
{
// Assume first loop is outer one
furnitureLoops.Add( uid, loops[0] );
}
}
furnitureInstances.Add(
newJtPlacement2dInt( f ) );
}
Retrieving the Boundary Loops
The boundary loop retrieval has not changed very much, although the following significant enhancements were made:
Store the symbol loop in the original symbol definition coordinate system.
Store the instance transformation data.
Manage the dictionary of symbols and the instances referencing them.
Save intermediate tessellated curve points, not just start and end point. This functionality can be toggled on and off by setting the Boolean _tessellate_curves switch.
The implementation is separated into two methods:
AddLoops: add the plan view boundary loops from a given solid to the list of loops.
GetPlanViewBoundaryLoops: Retrieve the plan view boundary loops from all solids of given element united together.
If the element is a family instance, transform its loops from the instance placement coordinate system back to the symbol definition one.
Here is the implementation:
///<summary>/// Add all plan view boundary loops from /// given solid to the list of loops./// The creation application argument is used to/// reverse the extrusion analyser output curves/// in case they are badly oriented.///</summary>///<returns>Number of loops added</returns>int AddLoops(
Autodesk.Revit.Creation.Application creapp,
JtLoops loops,
GeometryObject obj,
refint nExtrusionAnalysisFailures )
{
int nAdded = 0;
Solid solid = obj asSolid;
if( null != solid
&& 0 < solid.Faces.Size )
{
Plane plane = newPlane( XYZ.BasisX,
XYZ.BasisY, XYZ.Zero );
ExtrusionAnalyzer extrusionAnalyzer = null;
try
{
extrusionAnalyzer = ExtrusionAnalyzer.Create(
solid, plane, XYZ.BasisZ );
}
catch( Autodesk.Revit.Exceptions
.InvalidOperationException )
{
++nExtrusionAnalysisFailures;
return nAdded;
}
Face face = extrusionAnalyzer
.GetExtrusionBase();
foreach( EdgeArray a in face.EdgeLoops )
{
int nEdges = a.Size;
List<Curve> curves
= newList<Curve>( nEdges );
XYZ p0 = null; // loop start pointXYZ p; // edge start pointXYZ q = null; // edge end pointforeach( Edge e in a )
{
// This returns the curves already// correctly oriented:
curve = e.AsCurveFollowingFace(
face );
if( _debug_output )
{
p = curve.get_EndPoint( 0 );
q = curve.get_EndPoint( 1 );
Debug.Print( "{0} --> {1} following face",
Util.PointString( p ),
Util.PointString( q ) );
}
curves.Add( curve );
}
q = null;
JtLoop loop = newJtLoop( nEdges );
foreach( Curve curve in curves )
{
p = curve.get_EndPoint( 0 );
Debug.Assert( null == q
|| q.IsAlmostEqualTo( p, 1e-05 ),
string.Format(
"expected last endpoint to equal current start point, not distance {0}",
(null == q ? 0 : p.DistanceTo( q )) ) );
q = curve.get_EndPoint( 1 );
if( _debug_output )
{
Debug.Print( "{0} --> {1}",
Util.PointString( p ),
Util.PointString( q ) );
}
if( null == p0 )
{
p0 = p; // save loop start point
}
int n = -1;
if( _tessellate_curves
&& _min_tessellation_curve_length_in_feet
< q.DistanceTo( p ) )
{
IList<XYZ> pts = curve.Tessellate();
n = pts.Count;
Debug.Assert( 1 < n, "expected at least two points" );
Debug.Assert( p.IsAlmostEqualTo( pts[0] ), "expected tessellation start equal curve start point" );
Debug.Assert( q.IsAlmostEqualTo( pts[n-1] ), "expected tessellation end equal curve end point" );
if( 2 == n )
{
n = -1; // this is a straight line
}
else
{
--n; // skip last pointfor( int i = 0; i < n; ++i )
{
loop.Add( newPoint2dInt( pts[i] ) );
}
}
}
// If tessellation is disabled,// or curve is too short to tessellate,// or has only two tessellation points,// just add the start point:if( -1 == n )
{
loop.Add( newPoint2dInt( p ) );
}
}
Debug.Assert( q.IsAlmostEqualTo( p0, 1e-05 ),
string.Format(
"expected last endpoint to equal current start point, not distance {0}",
p0.DistanceTo( q ) ) );
loops.Add( loop );
++nAdded;
}
}
return nAdded;
}
///<summary>/// Retrieve all plan view boundary loops from /// all solids of given element united together./// If the element is a family instance, transform/// its loops from the instance placement /// coordinate system back to the symbol /// definition one.///</summary>JtLoops GetPlanViewBoundaryLoops(
Element e,
refint nFailures )
{
Autodesk.Revit.Creation.Application creapp
= e.Document.Application.Create;
JtLoops loops = newJtLoops( 1 );
Options opt = newOptions();
GeometryElement geo = e.get_Geometry( opt );
if( null != geo )
{
Document doc = e.Document;
if( e isFamilyInstance )
{
// Retrieve family instance geometry // transformed back to symbol definition// coordinate space by inverting the // family instance placement transformationLocationPoint lp = e.Location
asLocationPoint;
Transform t = Transform.get_Translation(
-lp.Point );
Transform r = Transform.get_Rotation(
lp.Point, XYZ.BasisZ, -lp.Rotation );
geo = geo.GetTransformed( t * r );
}
Solid union = null;
Plane plane = newPlane( XYZ.BasisX,
XYZ.BasisY, XYZ.Zero );
foreach( GeometryObject obj in geo )
{
Solid solid = obj asSolid;
if( null != solid
&& 0 < solid.Faces.Size )
{
// Some solids, e.g. in the standard // content 'Furniture Chair - Office' // cause an extrusion analyser failure,// so skip adding those.try
{
ExtrusionAnalyzer extrusionAnalyzer
= ExtrusionAnalyzer.Create(
solid, plane, XYZ.BasisZ );
}
catch( Autodesk.Revit.Exceptions
.InvalidOperationException )
{
solid = null;
++nFailures;
}
if( null != solid )
{
if( null == union )
{
union = solid;
}
else
{
union = BooleanOperationsUtils
.ExecuteBooleanOperation( union, solid,
BooleanOperationsType.Union );
}
}
}
}
AddLoops( creapp, loops, union, ref nFailures );
}
return loops;
}
GeoSnoop Loop Display
I obviously had to update my GeoSnoop display to take the symbol loop dictionary lookup into account.
I invoke it passing in the room loops, symbol loops, and instance placements:
GeoSnoop.DisplayLoops( revit_window,
"Room and furniture", roomLoops,
furnitureLoops, furnitureInstances );
The new implementation displays the instances at their respective location by transforming the referenced symbol geometry accordingly.
I make use of two transformations:
Matrix transform: Transform from native loop coordinate system to target display coordinates.
Matrix placement: Additional transformation from symbol definition to instance location for placing an individual instance.
It also takes the aspect ratio of the room to display into account and adjusts the window height to fit, based on the room bounding box aspect ratio:
Here is the result of displaying a room from the advanced sample project provided with Revit:
Those eight straight lines in the corners are columns.
I should eliminate those.
We don't want to move those around indiscriminately on a mobile device anyway, or the whole house might come crashing down around us.
Caveats
Rudolf Honke wrote in a reaction to my plan described last time:
You say that you want to avoid redundant geometry data in your cloud project:
As you know, it is possible to modify the geometry of an individual family instance by cutting voids off it.
The geometry of such an instance differs from other ones.
As far as I remember, modifying instance geometry this way has been possible since Revit 2012 or so; the API 2013 says:
"FamilyInstance.GetOriginalGeometry: Returns the original geometry of the instance, before the instance is modified by joins, cuts, coping, extensions, or other post-processing."
Thus, there are at least three groups of elements to be handled:
Individual elements, such as walls – have individual geometry, cannot be reused
Family instances which have not been modified – can be instantiated, geometry is reusable
Family instances which are post-processed, resulting in individual geometry – not reusable
In this case, I am only handling furniture and equipment instances that I do not expect to be modified.
After all, the plan is to move them around in the room, so they have to be free of constraints for it to work.
Still, these considerations obviously have to be taken into account for other applications.
Download
To wrap this up for the moment, here is
GeoSnoopSymbols.zip containing
the complete source code, Visual Studio solution and add-in manifest of the current state of this external command.
Next Steps
My next steps will be:
Migrate this add-in from Revit 2013 to 2014.
Implement server-side generated SVG code to display the room and furniture plan in CouchDB using Kanso.
Implement editing of SVG on the mobile device and reflect changes back to CouchDB (I know how now).
Implement Idling event handler and polling of CouchDB in the desktop add-in to reflect the changes back to the BIM in real-time.
Implement an external application wrapper for the add-in providing four commands:
Upload to cloud
Refresh from cloud
Subscribe to cloud
Unsubscribe from cloud
I know exactly how to address all these points now, no exceptions left.
Yay!
I look forward to hearing your comments and suggestions.
I had a chat with Ning Zhou, who was away from the Revit API for a while and is now happily back in the fold.
He explored how to access the material of a ramp element.
Access to Ramp Material
Question: Is there a way to get the ramp material information using API?
I tried lots of paths and could not find anything.
Answer (by Ning himself): I searched again using RevitLookup snoop.
It turns out that basic material info is accessible after all.
I found it us under 'Object type' instead of 'Parameters'.
Apparently only the material name is stored there, in the built-in parameter 'RAMP_ATTR_MATERIAL':
I have not seen anything providing the material volume, so I guess I'll have to use the geometry access and calculate that myself instead.
At least I can now implement a filter selection using the material name!
FilteredElementCollector concreteRamps
= newFilteredElementCollector( doc )
.WhereElementIsNotElementType()
.OfCategory( BuiltInCategory.OST_Ramps )
.Where( e =>
{
ElementId id = e.GetValidTypes().First(
id2 => id2.Equals( e.GetTypeId() ) );
Material m = doc.GetElement( doc.GetElement( id )
.get_Parameter(
BuiltInParameter.RAMP_ATTR_MATERIAL )
.AsElementId() ) asMaterial;
return m.Name.Contains( "Concrete" );
} );
Many thanks to Ning for his research and sharing this helpful result.
Before closing, here is another useful pointer on family instance placement and rotation:
Rotate a Family in Three Different Axes
Here is a pretty neat article on family instance placement strategies from a user point of view, describing how to
rotate a family in three different axes,
which is certainly useful for developers as well.
As always in the Revit API, knowing the best practice from a user and product point of view is of paramount importance before putting any thoughts or efforts at all into API development issues.
Here is a contribution from Mario Guttman of
CASE,
who already made various contributions here in the past.
He says:
I have been purging my 2013 code of deprecated functions in preparation for my 2014 upgrade.
One group of statements I have needed to replace are the view creations.
They were previously using the Document.Create.NewView3D method and needed converting to the View3D.CreateIsometric with a separate ViewOrientation3D object defining the view direction.
After searching the inner reaches of my brain for some ancient math skills I figured out the following:
Assuming that your user interface has produced two angular values (in degrees):
///<summary>/// The angle in the XY plane (azimuth),/// typically 0 to 360.///</summary>double angleHorizD;
///<summary>/// The vertical tilt (altitude),/// typically -90 to 90.///</summary>double angleVertD;
Then this utility function returns a unit vector in the specified direction:
///<summary>/// Return a unit vector in the specified direction.///</summary>///<param name="angleHorizD">Angle in XY plane /// in degrees</param>///<param name="angleVertD">Vertical tilt between /// -90 and +90 degrees</param>///<returns>Unit vector in the specified /// direction.</returns>privateXYZ VectorFromHorizVertAngles(
double angleHorizD,
double angleVertD )
{
// Convert degreess to radians.double degToRadian = Math.PI * 2 / 360;
double angleHorizR = angleHorizD * degToRadian;
double angleVertR = angleVertD * degToRadian;
// Return unit vector in 3Ddouble a = Math.Cos( angleVertR );
double b = Math.Cos( angleHorizR );
double c = Math.Sin( angleHorizR );
double d = Math.Sin( angleVertR );
returnnewXYZ( a * b, a * c, d );
}
From this it is easy to create the ViewOrientation3D object as follows:
The main idea is to have a robust lightweight data container for passing 2D point information back and forth between my Revit add-in, the cloud and mobile devices.
The later development motivated the addition of a couple of convenience methods since the first publication:
///<summary>/// An integer-based 2D point class.///</summary>classPoint2dInt : IComparable<Point2dInt>
{
publicint X { get; set; }
publicint Y { get; set; }
constdouble _feet_to_mm = 25.4 * 12;
staticint ConvertFeetToMillimetres( double d )
{
return (int) ( _feet_to_mm * d + 0.5 );
}
///<summary>/// Convert a 3D Revit XYZ to a 2D millimetre /// integer point by discarding the Z coordinate/// and scaling from feet to mm.///</summary>public Point2dInt( int x, int y )
{
X = x;
Y = y;
}
///<summary>/// Convert a 3D Revit XYZ to a 2D millimetre /// integer point by discarding the Z coordinate/// and scaling from feet to mm.///</summary>public Point2dInt( XYZ p )
{
X = ConvertFeetToMillimetres( p.X );
Y = ConvertFeetToMillimetres( p.Y );
}
///<summary>/// Comparison with another point, important/// for dictionary lookup support.///</summary>publicint CompareTo( Point2dInt a )
{
int d = X - a.X;
if( 0 == d )
{
d = Y - a.Y;
}
return d;
}
///<summary>/// Display as a string.///</summary>publicoverridestring ToString()
{
returnstring.Format( "({0},{1})", X, Y );
}
///<summary>/// Add two points, i.e. treat one of /// them as a translation vector.///</summary>publicstaticPoint2dIntoperator+(
Point2dInt a,
Point2dInt b )
{
returnnewPoint2dInt(
a.X + b.X, a.Y + b.Y );
}
}
JtLoop – a Closed Polygon Boundary Loop
This class consists of a simple list of 2D integer points representing a closed boundary loop.
When a new point is added to the collection, it is compared to the last and ignored if they evaluate equal.
This automatically suppresses too small boundary segment fragments.
///<summary>/// A closed polygon boundary loop.///</summary>classJtLoop : List<Point2dInt>
{
public JtLoop( int capacity )
: base( capacity )
{
}
///<summary>/// Display as a string.///</summary>publicoverridestring ToString()
{
returnstring.Join( ", ", this );
}
///<summary>/// Add another point to the collection./// If the new point is identical to the last,/// ignore it. This will automatically suppress/// really small boundary segment fragments.///</summary>publicnewvoid Add( Point2dInt p )
{
if( 0 == Count
|| 0 != p.CompareTo( this[Count - 1] ) )
{
base.Add( p );
}
}
}
JtLoops – a List of Boundary Loops
Each room produces a collection of loops, since it may include holes.
For the furniture and equipment, I am expecting to manage just one external boundary contour loop each.
On the other hand, for the furniture, this class enables me to easily collect all the individual furniture loops together into one single object.
The addition operator + is used to unite the room and furniture loops into a single container to pass to the visualisation method.
The conversion to a list of Point instances is used to feed the System.Drawing.Drawing2D.GraphicsPath class AddLines method to display the loops in a form.
///<summary>/// A list of boundary loops.///</summary>classJtLoops : List<JtLoop>
{
public JtLoops( int capacity )
: base( capacity )
{
}
///<summary>/// Unite two collections of boundary /// loops into one single one.///</summary>publicstaticJtLoopsoperator+( JtLoops a, JtLoops b )
{
int na = a.Count;
int nb = b.Count;
JtLoops sum = newJtLoops( na + nb );
sum.AddRange( a );
sum.AddRange( b );
return sum;
}
///<summary>/// Return suitable input for the .NET /// GraphicsPath.AddLines method to display the /// loops in a form. Note that a closing segment /// to connect the last point back to the first/// is added.///</summary>publicList<Point[]> GetGraphicsPathLines()
{
int i, n;
List<Point[]> loops
= newList<Point[]>( Count );
foreach( JtLoop jloop inthis )
{
n = jloop.Count;
Point[] loop = newPoint[n + 1];
i = 0;
foreach( Point2dInt p in jloop )
{
loop[i++] = newPoint( p.X, p.Y );
}
loop[i] = loop[0];
loops.Add( loop );
}
return loops;
}
}
JtBoundingBox2dInt – a Bounding Box for 2D Integer Points
As you can see there, it already includes a handy constructor taking a collection of loops to return their entire bounding box.
I now added properties to return the aspect ratio and a System.Drawing.Rectangle to easily define the visualisation target rectangle and coordinate system transformation:
///<summary>/// A bounding box for a collection /// of 2D integer points.///</summary>classJtBoundingBox2dInt
{
///<summary>/// Minimum and maximum X and Y values.///</summary>int xmin, ymin, xmax, ymax;
///<summary>/// Initialise to infinite values.///</summary>public JtBoundingBox2dInt()
{
xmin = ymin = int.MaxValue;
xmax = ymax = int.MinValue;
}
///<summary>/// Return current lower left corner.///</summary>publicPoint2dInt Min
{
get { returnnewPoint2dInt( xmin, ymin ); }
}
///<summary>/// Return current upper right corner.///</summary>publicPoint2dInt Max
{
get { returnnewPoint2dInt( xmax, ymax ); }
}
///<summary>/// Return current center point.///</summary>publicPoint2dInt MidPoint
{
get
{
returnnewPoint2dInt(
(int)(0.5 * ( xmin + xmax )),
(int)(0.5 * ( ymin + ymax )) );
}
}
///<summary>/// Return current width.///</summary>publicint Width
{
get { return xmax - xmin; }
}
///<summary>/// Return current height.///</summary>publicint Height
{
get { return ymax - ymin; }
}
///<summary>/// Return aspect ratio, i.e. Height/Width.///</summary>publicdouble AspectRatio
{
get
{
return (double) Height / (double) Width;
}
}
///<summary>/// Return a System.Drawing.Rectangle for this.///</summary>publicRectangle Rectangle
{
get
{
returnnewRectangle( xmin, ymin,
Width, Height );
}
}
///<summary>/// Expand bounding box to contain /// the given new point.///</summary>publicvoid ExpandToContain( Point2dInt p )
{
if( p.X < xmin ) { xmin = p.X; }
if( p.Y < ymin ) { ymin = p.Y; }
if( p.X > xmax ) { xmax = p.X; }
if( p.Y > ymax ) { ymax = p.Y; }
}
///<summary>/// Instantiate a new bounding box containing/// the given loops.///</summary>public JtBoundingBox2dInt( JtLoops loops )
{
foreach( JtLoop loop in loops )
{
foreach( Point2dInt p in loop )
{
ExpandToContain( p );
}
}
}
}
GeoSnoop – Display a Collection of Curves in a .NET Form
Now comes the exciting part: extracting the loop information from my own data structures, setting up an appropriate .NET form and infrastructure, and passing the information across with a minimum of fuss.
I had some fiddling to do to set this up optimally, I can tell you.
I am very satisfied with the end result, though:
///<summary>/// Display a collection of loops in a .NET form.///</summary>classGeoSnoop
{
///<summary>/// Pen size.///</summary>constint _pen_size = 1;
///<summary>/// Pen colour.///</summary>staticColor _pen_color = Color.Black;
///<summary>/// Margin around graphics.///</summary>constint _margin = 10;
///<summary>/// Draw loops on graphics with the specified/// transform and graphics attributes.///</summary>staticvoid DrawLoopsOnGraphics(
Graphics graphics,
List<Point[]> loops,
Matrix transform )
{
Pen pen = newPen( _pen_color, _pen_size );
graphics.Clear( System.Drawing.Color.White );
foreach( Point[] loop in loops )
{
GraphicsPath path = newGraphicsPath();
transform.TransformPoints( loop );
path.AddLines( loop );
graphics.DrawPath( pen, path );
}
}
///<summary>/// Display loops in a temporary form generated/// on the fly.///</summary>///<param name="owner">Owner window</param>///<param name="caption">Form caption</param>///<param name="loops">Boundary loops</param>publicstaticvoid DisplayLoops(
IWin32Window owner,
string caption,
JtLoops loops )
{
JtBoundingBox2dInt bb
= newJtBoundingBox2dInt( loops );
// Adjust target rectangle height to the // displayee loop height.int width = 400;
int height = (int) (width * bb.AspectRatio + 0.5);
// Specify transformation target rectangle // including a margin.int bottom = height - (_margin + _margin);
Point[] parallelogramPoints = newPoint[] {
newPoint( _margin, bottom ), // upper leftnewPoint( width - _margin, bottom ), // upper rightnewPoint( _margin, _margin ) // lower left
};
// Transform from native loop coordinate system// to target display coordinates.Matrix transform = newMatrix(
bb.Rectangle, parallelogramPoints );
Bitmap bmp = newBitmap( width, height );
Graphics gr = Graphics.FromImage( bmp );
DrawLoopsOnGraphics( gr,
loops.GetGraphicsPathLines(), transform );
Form form = newForm();
form.Text = caption;
form.Size = newSize( width + 7, height + 13 );
form.FormBorderStyle = FormBorderStyle
.FixedToolWindow;
PictureBox pb = newPictureBox();
pb.Location = new System.Drawing.Point( 0, 0 );
pb.Dock = System.Windows.Forms.DockStyle.Fill;
pb.Size = bmp.Size;
pb.Parent = form;
pb.Image = bmp;
form.ShowDialog( owner );
}
}
I bet you expected more than this, didn't you?
To quote
Antoine de Saint-Exupéry:
Il semble que la perfection soit atteinte non quand il n'y a plus rien à ajouter, mais quand il n'y a plus rien à retrancher
(Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away).
Validation of Results
Actually, this is the really exciting part.
I mentioned that I was worried for a moment about the large number of loop vertices in the plan view of the desk.
I was initially hoping for only four vertices, to represent a simple rectangle.
After all, the plan view of a desk and chair looks like this in Revit:
In my visualisation, the same desk and chair loops are displayed like this instead:
The good news is:
We have indeed produced closed loops.
Their shape and location is correct.
Where do all those bumps come from, though?
The answer is easy and completely reassuring: the bumps are the desk drawer handles that stick out a little bit beyond the desktop surface.
Looking at a 3D view in Revit from the top, the desk looks like this:
My results reproduce this exactly.
Looking at the chairs, I mentioned that some of the chair solids cause extrusion analyser failures, and I skip those.
To be precise, I have two failures on each chair.
Comparing the chair 3D view from the top in Revit with my results shows that the armrests are the components causing trouble:
The rest matches up perfectly, once again validifying my approach.
The bumps on the sides of the chairs are the armrest supports.
I also cleaned up the form generation as much as possible.
Resizing, zooming and panning are not supported.
The form aspect ratio is adjusted up front to adapt to the loops to display:
Once again, here is the same view in Revit:
Next Steps
Actually, the next steps are the really, really exciting part.
Now I can turn to the implementation of my data repository and the task of hosting it in the cloud.
I already discussed my tentative plans and high hopes for this.
Let's see if I can live up to them.
Adventure!
Who knows what will come, and where this will lead?
Download
To wrap this up for the moment, here is
GeoSnoopLoops.zip containing
the complete source code, Visual Studio solution and add-in manifest of the current state of this external command.
An alternative and more effective method is to use the Edge AsCurveFollowingFace call instead of AsCurve, as we shall see below.
In any case, we want to check the results, e.g. to ensure that we really have obtained the valid closed boundary loops we expect.
The easiest way to do so is to visualise them graphically.
To visualise something, you mostly need to know how big it is first.
You often need to scale it to fit into your visualisation space, e.g. a fixed-size window, i.e. transform from its given initial size and location to the target coordinate space.
A useful way to determine the size of a boundary loop is to calculate the bounding box of its collection of vertices.
That leads to the following topics of today's post, including one additional unrelated issue that just came in:
I already had one stab at using a built-in Revit API method instead of implementing my own
SortCurvesContiguous solution:
Rudolf Honke suggested testing the
ExporterIFCUtils ValidateCurveLoops method instead, but that did not work.
Now Bettina Zimmermann provided another idea:
I don’t understand why you write your own code for direction sorting of the curves when Revit can provide it for you: Edge.AsCurveFollowingFace.
Scott Conover's
Geometry API document describes
it a bit but not very detailed.
I have written some code using Revit API to find the curves sorted in the right direction for using GeometryCreationUtilities.CreateExtrusionGeometry, so I found out it is possible using this call.
I tested that, simply replacing the call to Edge.AsCurve by AsCurveFollowingFace:
Plane plane = newPlane( XYZ.BasisX,
XYZ.BasisY, XYZ.Zero );
ExtrusionAnalyzer extrusionAnalyzer = null;
try
{
extrusionAnalyzer = ExtrusionAnalyzer.Create(
solid, plane, XYZ.BasisZ );
}
catch( Autodesk.Revit.Exceptions
.InvalidOperationException )
{
++nExtrusionAnalysisFailures;
return nAdded;
}
Face face = extrusionAnalyzer
.GetExtrusionBase();
foreach( EdgeArray a in face.EdgeLoops )
{
int nEdges = a.Size;
List<Curve> curves
= newList<Curve>( nEdges );
XYZ p0 = null; // loop start pointXYZ p; // edge start pointXYZ q = null; // edge end pointforeach( Edge e in a )
{
// This requires post-processing using// SortCurvesContiguous:Curve curve = e.AsCurve();
if( _debug_output )
{
p = curve.get_EndPoint( 0 );
q = curve.get_EndPoint( 1 );
Debug.Print( "{0} --> {1}",
Util.PointString( p ),
Util.PointString( q ) );
}
// This returns the curves already// correctly oriented:
curve = e.AsCurveFollowingFace(
face );
if( _debug_output )
{
p = curve.get_EndPoint( 0 );
q = curve.get_EndPoint( 1 );
Debug.Print( "{0} --> {1} following face",
Util.PointString( p ),
Util.PointString( q ) );
}
curves.Add( curve );
}
CurveUtils.SortCurvesContiguous(
creapp, curves, _debug_output );
q = null;
JtLoop loop = newJtLoop( nEdges );
foreach( Curve curve in curves )
{
// Todo: handle non-linear curve.// Especially: if two long lines have a // short arc in between them, skip the arc// and extend both lines.
p = curve.get_EndPoint( 0 );
loop.Add( newPoint2dInt( p ) );
Debug.Assert( null == q
|| q.IsAlmostEqualTo( p, 1e-05 ),
string.Format(
"expected last endpoint to equal current start point, not distance {0}",
(null == q ? 0 : p.DistanceTo( q )) ) );
q = curve.get_EndPoint( 1 );
if( _debug_output )
{
Debug.Print( "{0} --> {1}",
Util.PointString( p ),
Util.PointString( q ) );
}
if( null == p0 )
{
p0 = p; // save loop start point
}
}
Debug.Assert( q.IsAlmostEqualTo( p0, 1e-05 ),
string.Format(
"expected last endpoint to equal current start point, not distance {0}",
p0.DistanceTo( q ) ) );
loops.Add( loop );
++nAdded;
}
I also added new code to the SortCurvesContiguous method to report correctly sorted and oriented curves.
A typical snippet of output looks like this now:
(2.18,8.4,0) --> (2.18,5.78,0)
(2.18,8.4,0) --> (2.18,5.78,0) following face
(2.18,5.78,0) --> (7.31,5.78,0)
(2.18,5.78,0) --> (7.31,5.78,0) following face
(7.31,5.78,0) --> (7.31,8.4,0)
(7.31,5.78,0) --> (7.31,8.4,0) following face
(7.31,8.4,0) --> (6.75,8.4,0)
(7.31,8.4,0) --> (6.75,8.4,0) following face
(6.75,8.4,0) --> (6.75,8.46,0)
(6.75,8.4,0) --> (6.75,8.46,0) following face
(6.75,8.46,0) --> (6.41,8.46,0)
(6.75,8.46,0) --> (6.41,8.46,0) following face
(6.41,8.46,0) --> (6.41,8.4,0)
(6.41,8.46,0) --> (6.41,8.4,0) following face
(6.41,8.4,0) --> (3.08,8.4,0)
(6.41,8.4,0) --> (3.08,8.4,0) following face
(3.08,8.46,0) --> (3.08,8.4,0)
(3.08,8.4,0) --> (3.08,8.46,0) following face
(2.74,8.46,0) --> (3.08,8.46,0)
(3.08,8.46,0) --> (2.74,8.46,0) following face
(2.74,8.4,0) --> (2.74,8.46,0)
(2.74,8.46,0) --> (2.74,8.4,0) following face
(2.74,8.4,0) --> (2.18,8.4,0)
(2.74,8.4,0) --> (2.18,8.4,0) following face
0 endPoint (2.18,5.78,0)
1 start point match, no need to swap
1 endPoint (7.31,5.78,0)
2 start point match, no need to swap
2 endPoint (7.31,8.4,0)
3 start point match, no need to swap
3 endPoint (6.75,8.4,0)
4 start point match, no need to swap
4 endPoint (6.75,8.46,0)
5 start point match, no need to swap
5 endPoint (6.41,8.46,0)
6 start point match, no need to swap
6 endPoint (6.41,8.4,0)
7 start point match, no need to swap
7 endPoint (3.08,8.4,0)
8 start point match, no need to swap
8 endPoint (3.08,8.46,0)
9 start point match, no need to swap
9 endPoint (2.74,8.46,0)
10 start point match, no need to swap
10 endPoint (2.74,8.4,0)
11 start point match, no need to swap
11 endPoint (2.18,8.4,0)
(2.18,8.4,0) --> (2.18,5.78,0)
(2.18,5.78,0) --> (7.31,5.78,0)
(7.31,5.78,0) --> (7.31,8.4,0)
(7.31,8.4,0) --> (6.75,8.4,0)
(6.75,8.4,0) --> (6.75,8.46,0)
(6.75,8.46,0) --> (6.41,8.46,0)
(6.41,8.46,0) --> (6.41,8.4,0)
(6.41,8.4,0) --> (3.08,8.4,0)
(3.08,8.4,0) --> (3.08,8.46,0)
(3.08,8.46,0) --> (2.74,8.46,0)
(2.74,8.46,0) --> (2.74,8.4,0)
(2.74,8.4,0) --> (2.18,8.4,0)
FamilyInstance Furniture Desk <212646 1525 x 762mm> has 1 loop:
0: (664,2561), (664,1761), (2227,1761), (2227,2561),
(2056,2561), (2056,2580), (1954,2580), (1954,2561),
(937,2561), (937,2580), (836,2580), (836,2561)
As you can see, AsCurveFollowingFace does indeed return all the curves correctly oriented and sorted, and SortCurvesContiguous has nothing at all left to do.
Thank you very much Bettina for this valuable suggestion!
Also thanks to Scott Conover for his valuable Geometry API document from the Autodesk University 2011 class, which I already mentioned numerous times in the past but was not previously published in PDF format and therefore not included in web searches until now.
Bounding Box Determination for XYZ Points
As explained above, I would like to check the validity of the boundary loop listed above.
The easiest way to do so is by displaying it visually.
To do so, I need to transform it from its native coordinate space to my display space, for instance Windows device or HTML5 Canvas coordinates.
This normally requires scaling and translation, and in turn knowledge of the size and location of the native coordinate space.
The latter can be easily determined by calculating a bounding box.
I therefore set out to implement a bounding box determination for XYZ points.
I could use the existing Revit API BoundingBoxXyz class, because it does provide both read and write access to its Min and Max properties.
However, it includes a number of additional features that are of no interest to me at this point, so I prefer to create my own lightweight class for this.
My first idea was to use a generic Tuple class, e.g.
classJtBoundingBoxXyz : Tuple<XYZ, XYZ>
However, the components of such a tuple are read-only and cannot be changed after instantiation, so I cannot easily update the min and max values as I add new points using this approach.
My next idea was to implement my own class and manage Revit XYZ max and min member variables within it.
However, the components of an XYZ are read-only and cannot be changed except by re-instantiation, so extending the bounding box to include new points would have sub-optimal performance in this case as well.
The simplest solution appears to be managing six individual doubles instead, and instantiating XYZ return values when needed, like this:
///<summary>/// A bounding box for a collection of XYZ instances./// The components of a tuple are read-only and cannot /// be changed after instantiation, so I cannot use /// that easily./// The components of an XYZ are read-only and cannot /// be changed except by re-instantiation, so I cannot /// use that easily either.///</summary>classJtBoundingBoxXyz// : Tuple<XYZ, XYZ>
{
///<summary>/// Minimum and maximum X, Y and Z values.///</summary>double xmin, ymin, zmin, xmax, ymax, zmax;
///<summary>/// Initialise to infinite values.///</summary>public JtBoundingBoxXyz()
{
xmin = ymin = zmin = double.MaxValue;
xmax = ymax = zmax = double.MinValue;
}
///<summary>/// Return current lower left corner.///</summary>publicXYZ Min
{
get { returnnewXYZ( xmin, ymin, zmin ); }
}
///<summary>/// Return current upper right corner.///</summary>publicXYZ Max
{
get { returnnewXYZ( xmax, ymax, zmax ); }
}
publicXYZ MidPoint
{
get { return 0.5 * ( Min + Max ); }
}
///<summary>/// Expand bounding box to contain /// the given new point.///</summary>publicvoid ExpandToContain( XYZ p )
{
if( p.X < xmin ) { xmin = p.X; }
if( p.Y < ymin ) { ymin = p.Y; }
if( p.Z < zmin ) { zmin = p.Z; }
if( p.X > xmax ) { xmax = p.X; }
if( p.Y > ymax ) { ymax = p.Y; }
if( p.Z > zmax ) { zmax = p.Z; }
}
}
Bounding Box Determination for 2D Integer Points
Applying the same principles, here is a pretty optimal implemenetation of a bounding box for 2D integer-based points.
It includes a constructor taking a collection of loops, which is the output I am generating from my furniture and equipment plan view calculation:
///<summary>/// A bounding box for a collection /// of 2D integer points.///</summary>classJtBoundingBox2dInt
{
///<summary>/// Minimum and maximum X and Y values.///</summary>int xmin, ymin, xmax, ymax;
///<summary>/// Initialise to infinite values.///</summary>public JtBoundingBox2dInt()
{
xmin = ymin = int.MaxValue;
xmax = ymax = int.MinValue;
}
///<summary>/// Return current lower left corner.///</summary>publicPoint2dInt Min
{
get { returnnewPoint2dInt( xmin, ymin ); }
}
///<summary>/// Return current upper right corner.///</summary>publicPoint2dInt Max
{
get { returnnewPoint2dInt( xmax, ymax ); }
}
///<summary>/// Return current center point.///</summary>publicPoint2dInt MidPoint
{
get
{
returnnewPoint2dInt(
(int)(0.5 * ( xmin + xmax )),
(int)(0.5 * ( ymin + ymax )) );
}
}
///<summary>/// Expand bounding box to contain /// the given new point.///</summary>publicvoid ExpandToContain( Point2dInt p )
{
if( p.X < xmin ) { xmin = p.X; }
if( p.Y < ymin ) { ymin = p.Y; }
if( p.X > xmax ) { xmax = p.X; }
if( p.Y > ymax ) { ymax = p.Y; }
}
///<summary>/// Instantiate a new bounding box containing/// the given loops.///</summary>public JtBoundingBox2dInt( JtLoops loops )
{
foreach( JtLoop loop in loops )
{
foreach( Point2dInt p in loop )
{
ExpandToContain( p );
}
}
}
}
Stay tuned to see how I use this to finally display and verify the extrusion analyser output and my integer-based boundary loop management implementation, coming next.
Overview of Sweep Sample Code and Issue Resolutions
Before closing, here is one last issue that just came up and I would like to mention here as well:
Question: I am trying to create a sweep in a new family using the Revit API, and am having some difficulties.
I found only one single example in the Revit SDK Samples, and can only get it working using your template.
When I use my own, it fails.
Where can I find some more information on this, please?
Answer: I discussed some very advanced sweep issues with Bill Adkison, and we published the essence of his results on
sweep path tolerance criteria.
Normally, sweep issues are nowhere near as hard as the ones he encountered and mastered.
Continuing the research and development for my
cloud-based round-trip 2D Revit model editing project,
I looked at using the ExtrusionAnalyzer to create a plan view boundary profile for the furniture and equipment family instances and implemented a utility method
SortCurvesContiguous to
sort and re-orient the curves it returns into a closed contiguous loop.
I did not yet display the code driving the extrusion analyser, though, or discuss my experiences with that.
///<summary>/// Retrieve all plan view boundary loops from /// all solids of given element.///</summary>JtLoops GetPlanViewBoundaryLoopsMultiple(
Element e,
refint nFailures )
{
Autodesk.Revit.Creation.Application creapp
= e.Document.Application.Create;
JtLoops loops = newJtLoops( 1 );
Options opt = newOptions();
GeometryElement geo = e.get_Geometry( opt );
if( null != geo )
{
Document doc = e.Document;
if( e isFamilyInstance )
{
geo = geo.GetTransformed(
Transform.Identity );
}
foreach( GeometryObject obj in geo )
{
AddLoops( creapp, loops, obj, ref nFailures );
}
}
return loops;
}
2. For each solid, instantiate an ExtrusionAnalyzer, retrieve and process its resulting output curves:
///<summary>/// Add all plan view boundary loops from /// given solid to the list of loops./// The creation application argument is used to/// reverse the extrusion analyser output curves/// in case they are badly oriented.///</summary>///<returns>Number of loops added</returns>int AddLoops(
Autodesk.Revit.Creation.Application creapp,
JtLoops loops,
GeometryObject obj )
{
int nAdded = 0;
Solid solid = obj asSolid;
if( null != solid
&& 0 < solid.Faces.Size )
{
Plane plane = newPlane( XYZ.BasisX,
XYZ.BasisY, XYZ.Zero );
ExtrusionAnalyzer extrusionAnalyzer = null;
extrusionAnalyzer = ExtrusionAnalyzer.Create(
solid, plane, XYZ.BasisZ );
Face face = extrusionAnalyzer
.GetExtrusionBase();
foreach( EdgeArray a in face.EdgeLoops )
{
int nEdges = a.Size;
List<Curve> curves
= newList<Curve>( nEdges );
XYZ p0 = null; // loop start pointXYZ p; // edge start pointXYZ q = null; // edge end pointforeach( Edge e in a )
{
Curve curve = e.AsCurve();
if( _debug_output )
{
p = curve.get_EndPoint( 0 );
q = curve.get_EndPoint( 1 );
Debug.Print( "{0} --> {1}",
Util.PointString( p ),
Util.PointString( q ) );
}
curves.Add( curve );
}
CurveUtils.SortCurvesContiguous(
creapp, curves, _debug_output );
q = null;
JtLoop loop = newJtLoop( nEdges );
foreach( Curve curve in curves )
{
// Todo: handle non-linear curve.// Especially: if two long lines have a // short arc in between them, skip the arc// and extend both lines.
p = curve.get_EndPoint( 0 );
loop.Add( newPoint2dInt( p ) );
Debug.Assert( null == q
|| q.IsAlmostEqualTo( p, 1e-05 ),
string.Format(
"expected last endpoint to equal current start point, not distance {0}",
(null == q ? 0 : p.DistanceTo( q )) ) );
q = curve.get_EndPoint( 1 );
if( _debug_output )
{
Debug.Print( "{0} --> {1}",
Util.PointString( p ),
Util.PointString( q ) );
}
if( null == p0 )
{
p0 = p; // save loop start point
}
}
Debug.Assert( q.IsAlmostEqualTo( p0, 1e-05 ),
string.Format(
"expected last endpoint to equal current start point, not distance {0}",
p0.DistanceTo( q ) ) );
loops.Add( loop );
++nAdded;
}
}
return nAdded;
}
Handling ExtrusionAnalyzer Failures
The desk solids are processed perfectly well by the extrusion analyser, but two of the chair solids produce failures.
This is the standard Revit content 'Furniture Chair - Office' returning invalid solids.
Since I don't (yet?) know how to detect beforehand whether a solid will cause a failure or not, the only option I see is to encapsulate the extrusion analyser implementation in an exception handler.
I also added code to report how many failures occur:
Testing the ExporterIFCUtils ValidateCurveLoops Method
Rudolf Honke added a very valid
suggestion to
my
SortCurvesContiguous implementation: You may have a look at ExporterIFCUtils.ValidateCurveLoops method. RevitApi.chm says:
"Does validity checks on a list of curve loops to ensure that they are all co-planar, closed, and properly oriented."
I have never tested this method, but perhaps it prevents you from inventing the wheel another time...
I like that suggestion a lot and am interested to find out how useful this method might be.
It takes a list of curve loops and returns a new list of curve loops "properly oriented, if possible. If not, the return contains no loops."
I added code to test this method, passing in to it the output produced by the extrusion analyser:
// Test ValidateCurveLoopsCurveLoop loopIfc = newCurveLoop();
foreach( Edge e in a )
{
Curve curve = e.AsCurve();
if( _debug_output )
{
p = curve.get_EndPoint( 0 );
q = curve.get_EndPoint( 1 );
Debug.Print( "{0} --> {1}",
Util.PointString( p ),
Util.PointString( q ) );
}
curves.Add( curve );
// Throws an exception saying "This curve // will make the loop not contiguous. // Parameter name: pCurve"
loopIfc.Append( curve );
}
// We never reach this point:List<CurveLoop> loopsIfc
= newList<CurveLoop>( 1 );
loopsIfc.Add( loopIfc );
IList<CurveLoop> loopsIfcOut = ExporterIFCUtils
.ValidateCurveLoops( loopsIfc, XYZ.BasisZ );
Unfortunately, the CurveLoop Append method throws an exception saying "This curve will make the loop not contiguous. Parameter name: pCurve".
It obviously expects contiguous curves to be passed in and can therefore not be used to re-orient curves if they are oriented the wrong way.
So much for that suggestion.
You got my hopes up there, Rudolf, but no luck this time.
First Conclusion and Capitulation
My first (erroneous) conclusion for the extrusion analyser was simple: the output I receive for a plan view is much too complex for my use.
I was expecting it to return the simplest possible contour to represent the shadow cast by the solid passed in.
The results include lots of extraneous loops that do not contribute to the shadow of the object.
Oops.
Since I am passing in the multiple solids from the desk and the chair to the extrusion analyser one by one, individually, it is obviously returning a plan view boundary outline for each one of the solids, individually, as well.
At first glance I thought that this result is much too complicated for me to handle, since all I want is one single boundary for the whole object.
For a moment, I gave up on the whole idea of using the extrusion analyser and decided to switch to a 2D plan view instead, and ask for the view-specific family instance representation in that view. For the desk, that would simply give me the desired rectangular outline.
Capitulation.
Then I switched on my brain for a second again and realised the obvious fact that individual solids will generate individual outlines.
I decided to give the extrusion analyser another go, unite all the solids into one single one, then pass that in to a single call of the extrusion analyser.
Reimplementation with a Boolean Union
'Gesagt, getan', as the Germans say, 'No sooner said than done', 'a word and a blow'.
I united all the desk solids, passed the resulting union in to the extrusion analyser, and it produces a single closed loop.
I united all the chair solids, passed the resulting union in to the extrusion analyser, and it produces one single failure.
Oh no!
The individual chair solids causing a failure when passed in individually also cause a failure when united with the unproblematic ones.
Next idea:
Extract the family instance solids from the geometry one by one.
Test each solid by passing it in to the extrusion analyser on its own.
If it causes a failure, discard it.
If it does not, unite it with the others.
Pass in the union of all non-failing solids to the extrusion analyser.
Retrieve the resulting curves.
Sort and re-orient the curves to form a contiguous closed loop.
Convert the closed loop data to my integer-based 2D format.
Here is the final code for achieving this:
///<summary>/// Retrieve all plan view boundary loops from /// all solids of given element united together.///</summary>JtLoops GetPlanViewBoundaryLoops(
Element e,
refint nFailures )
{
Autodesk.Revit.Creation.Application creapp
= e.Document.Application.Create;
JtLoops loops = newJtLoops( 1 );
Options opt = newOptions();
GeometryElement geo = e.get_Geometry( opt );
if( null != geo )
{
Document doc = e.Document;
if( e isFamilyInstance )
{
geo = geo.GetTransformed(
Transform.Identity );
}
Solid union = null;
Plane plane = newPlane( XYZ.BasisX,
XYZ.BasisY, XYZ.Zero );
foreach( GeometryObject obj in geo )
{
Solid solid = obj asSolid;
if( null != solid
&& 0 < solid.Faces.Size )
{
// Some solids, e.g. in the standard // content 'Furniture Chair - Office' // cause an extrusion analyser failure,// so skip adding those.try
{
ExtrusionAnalyzer extrusionAnalyzer
= ExtrusionAnalyzer.Create(
solid, plane, XYZ.BasisZ );
}
catch( Autodesk.Revit.Exceptions
.InvalidOperationException )
{
solid = null;
++nFailures;
}
if( null != solid )
{
if( null == union )
{
union = solid;
}
else
{
union = BooleanOperationsUtils
.ExecuteBooleanOperation( union, solid,
BooleanOperationsType.Union );
}
}
}
}
AddLoops( creapp, loops, union, ref nFailures );
}
return loops;
}
This is the current result for my simple sample model (copy the text to see the truncated lines in full):
I have not really checked the validity of these loops yet.
The chairs have arcs in them, so maybe they have to be as complex as they appear.
The desk should actually be just one single simple rectangle, so maybe there is a possibility to clean up its loop, e.g. reduce the 12 vertices to just four, e.g. by identifying collinear segments or something.
I'll look at that in more detail anon.
As you can see, this is all very experimental work in progress.
I hope you can get some use out of it anyway, and am excited to see where this will lead me.
Conclusion, Next Steps and Download
It works so far, and I will still have hope of using this for my final project implementation.
The next step is to test the validity of the loops I retrieve.
As said, the end goal is:
Upload the boundary loop and other data to the cloud.
Alternatively, I could first implement a visualisation tool in .NET for local use and testing.
I have had that on my list for a long time anyway.
Anyway, here is
GetFurnitureLoops.zip containing
the complete source code, Visual Studio solution and add-in manifest of the current state of this external command.
New Revit Add-ins
Before closing, here are some new Revit add-ins pointed out by developers that have been active here on the blog:
Israel Rodriguez of icubY released his
mYbox add-in
providing an easy way to integrate families that are on our portal directly inside Revit, AutoCAD and SketchUp, and includes a WPF client using the Revit API to instantiate the families
(video).
The content is mainly in Brazilian Portuguese.
Fernando Malard of
ofcdesk
points out his
ofctools,
providing advanced maintenance commands to optimise and streamline work, e.g. creating, editing or deleting large numbers of elements.
Continuing the research and development for my
cloud-based round-trip 2D Revit model editing project,
I need to determine the boundary loop polygons to represent the furniture and equipment family instances for manipulation on the mobile device.
To display a polygon using SVG in the mobile device browser, I obviously need a set of contiguous and sorted curve elements forming a closed loop.
I mentioned using the Revit API ExtrusionAnalyzer class to determine the plan view boundary outline for the family instances.
On testing that approach, I discovered that the results it returns are unsorted.
For instance, I analysed the ExtrusionAnalyzer output for the standard Revit furniture content 'Desk 1525 x 762mm'.
In plan view, it can appear like this:
The desk includes lots of internal geometry that is not visible in plan view:
As a result, the information returned by the ExtrusionAnalyzer is much more complex than the simple rectangle one might expect.
In fact, it returns ten separate closed loops, and the first one consists of eight curves.
To check whether they are contiguous and correctly sorted, here is a list of their end points:
You can see quite easily that they are not contiguous.
For instance, the end point at (2.74,8.46) of the first curve equals the start point of the last.
If you look more carefully still, you will notice that some of the curves require reversing to connect to their neighbours.
These observations and considerations led to the implementation of the following curve sorting and orientation method.
Its end point matching comparison relies on the standard Revit precision, which is around one sixteenth of an inch.
Since the built-in Revit database length unit is feet, I define the following fuzz factor for that:
Further, the curve reversal is implemented by creating a completely new curve.
Therefore, the creation application has to be provided for generating these:
///<summary>/// Create a new curve with the same /// geometry in the reverse direction.///</summary>///<param name="orig">The original curve.</param>///<returns>The reversed curve.</returns>///<throws cref="NotImplementedException">If the /// curve type is not supported by this utility.</throws>staticCurve CreateReversedCurve(
Autodesk.Revit.Creation.Application creapp,
Curve orig )
{
if( !IsSupported( orig ) )
{
thrownewNotImplementedException(
"CreateReversedCurve for type "
+ orig.GetType().Name );
}
if( orig isLine )
{
return creapp.NewLineBound(
orig.GetEndPoint( 1 ),
orig.GetEndPoint( 0 ) );
}
elseif( orig isArc )
{
return creapp.NewArc( orig.GetEndPoint( 1 ),
orig.GetEndPoint( 0 ),
orig.Evaluate( 0.5, true ) );
}
else
{
thrownewException(
"CreateReversedCurve - Unreachable" );
}
}
With this support in hand, we can go ahead and implement the SortCurvesContiguous method:
///<summary>/// Sort a list of curves to make them correctly /// ordered and oriented to form a closed loop.///</summary>publicstaticvoid SortCurvesContiguous(
Autodesk.Revit.Creation.Application creapp,
IList<Curve> curves,
bool debug_output )
{
int n = curves.Count;
// Walk through each curve (after the first) // to match up the curves in orderfor( int i = 0; i < n; ++i )
{
Curve curve = curves[i];
XYZ endPoint = curve.GetEndPoint( 1 );
if( debug_output )
{
Debug.Print( "{0} endPoint {1}", i,
Util.PointString( endPoint ) );
}
XYZ p;
// Find curve with start point = end pointbool found = (i + 1 >= n);
for( int j = i + 1; j < n; ++j )
{
p = curves[j].GetEndPoint( 0 );
// If there is a match end->start, // this is the next curveif( _sixteenth > p.DistanceTo( endPoint ) )
{
if( debug_output )
{
Debug.Print(
"{0} start point, swap with {1}",
j, i + 1 );
}
if( i + 1 != j )
{
Curve tmp = curves[i + 1];
curves[i + 1] = curves[j];
curves[j] = tmp;
}
found = true;
break;
}
p = curves[j].GetEndPoint( 1 );
// If there is a match end->end, // reverse the next curveif( _sixteenth > p.DistanceTo( endPoint ) )
{
if( i + 1 == j )
{
if( debug_output )
{
Debug.Print(
"{0} end point, reverse {1}",
j, i + 1 );
}
curves[i + 1] = CreateReversedCurve(
creapp, curves[j] );
}
else
{
if( debug_output )
{
Debug.Print(
"{0} end point, swap with reverse {1}",
j, i + 1 );
}
Curve tmp = curves[i + 1];
curves[i + 1] = CreateReversedCurve(
creapp, curves[j] );
curves[j] = tmp;
}
found = true;
break;
}
}
if( !found )
{
thrownewException( "SortCurvesContiguous:"
+ " non-contiguous input curves" );
}
}
}
This is obviously not the most effective sorting algorithm in the world, but it should do for the hopefully simple cases I expect to encounter.
This method includes an option for verbose logging to the Visual Studio debug output window.
This is what it produces for the unsorted list of input points provided above:
0 endPoint (2.74,8.46,0)
7 start point, swap with 1
1 endPoint (3.08,8.46,0)
6 start point, swap with 2
2 endPoint (3.08,8.38,0)
5 end point, swap with reverse 3
3 endPoint (3.05,8.38,0)
4 end point, reverse 4
4 endPoint (3.05,8.44,0)
5 end point, reverse 5
5 endPoint (2.76,8.44,0)
6 end point, reverse 6
6 endPoint (2.76,8.38,0)
7 end point, reverse 7
7 endPoint (2.74,8.38,0)
The complete output for the entire desk looks like this after sorting and rearranging all ten loops using the SortCurvesContiguous method and converting the results from Revit XYZ coordinates to my
2D integer-based loop representation in millimetres:
At the same time, Revit 2014 has been announced, and I am sure you are eager to hear more about that, especially from the API point of view, so let's have a look at that first.
The Revit 2014 API
It is impossible to cover everything, and I have to start somewhere.
Here are a couple of highlights:
API access to the project browser and selected elements; API commands and macros are enabled.
Copy and paste API supports copy within or between documents including view-specific elements.
Full API support for the new non-rectangular crop regions.
Schedule API now provides formatting control and read-write access to individual data items.
Command API enables programmatic command launch including built-in Revit, external add-in and macro.
Add-in API supports mid-session loading and execution.
Displaced elements API enables exploded views.
Join geometry API to create or remove a Boolean join and control join order.
FreeForm element API enables modification of solid geometry imported from e.g. DWG or SAT.
Site API enables editing of topography surface points and supports sub-regions.
MEP calculations are moved to external services and can be replaced by add-ins.
Structural reinforcement and rebar enhancements.
Enhanced document open, save and worksharing API.
Linked model API supports loading, unloading, path manipulation, link identification and creation.
Linked model interaction enables tag creation for linked rooms, linked element selection, geometric reference conversion, etc.
Import DXF markup, import and link SAT and SketchUp.
Export to NavisWorks via add-in, access DWG, IFC and DGN layer, linetype, lineweight, font and pattern tables.
Direct API access to rendering output pipeline including all geometry and material properties.
Macro API provides support for Python and Ruby development plus List, create, delete and execute modules, macros and security settings.
How does that sound?
Rather a lot of new stuff, isn't there?
Rather a lot of really exciting new stuff, isn't there?
Joe Ye describes a few of these API features in a little more detail on the
AEC DevBlog.
To round this off, here are the complete materials from the Revit 2014 DevDays presentations:
We will have all the time in the world to explore this in more detail anon.
The material provided above should keep you occupied over the Easter weekend, however :-)
Enjoy!
Retrieving Plan View Room Boundary Polygon Loops
Returning to the cloud-based 2D model editing project, one of the first required components is an add-in that determines and uploads the room, furniture and equipment family instance boundary polygons to some globally accessible data repository for a simplified 2D plan view rendering on a mobile device.
The task I want to achieve for the first part of this first step is to retrieve the room boundary and store the 2D loops in a cloud-based data repository.
Since Revit does not support precision below one sixteenth of an inch, I might as well approximate all my data to something in that region.
For performance and efficiency reasons, it is also useful to move my calculations from floating point double numbers to integers.
Since I want to display my model on a mobile device with a limited resolution using SVG, integers also seem pretty appropriate.
Very handily, a millimetre is just a little bit less than a sixteenth of an inch.
That leads me to define the following integer-based 2D point class:
///<summary>/// An integer-based 2D point class.///</summary>classPoint2dInt : IComparable<Point2dInt>
{
publicint X { get; set; }
publicint Y { get; set; }
constdouble _feet_to_mm = 25.4 * 12;
staticint ConvertFeetToMillimetres( double d )
{
return (int) ( _feet_to_mm * d + 0.5 );
}
///<summary>/// Convert a 3D Revit XYZ to a 2D millimetre /// integer point by discarding the Z coordinate/// and scaling from feet to mm.///</summary>public Point2dInt( XYZ p )
{
X = ConvertFeetToMillimetres( p.X );
Y = ConvertFeetToMillimetres( p.Y );
}
publicint CompareTo( Point2dInt a )
{
int d = X - a.X;
if( 0 == d )
{
d = Y - a.Y;
}
return d;
}
publicoverridestring ToString()
{
returnstring.Format( "({0},{1})", X, Y );
}
}
A room boundary may include several loops, for instance if a room surrounds some other space such as an elevator, i.e. its outer boundary loop contains some interior loops representing 'holes'.
Therefore, the room GetBoundarySegments method returns a list of loops, and each loop as a list of boundary segments:
I therefore define my own integer-based 2D loop and list of loops classes like this:
classJtLoop : List<Point2dInt>
{
public JtLoop( int capacity )
: base( capacity )
{
}
publicoverridestring ToString()
{
returnstring.Join( ", ", this );
}
}
classJtLoops : List<JtLoop>
{
public JtLoops( int capacity )
: base( capacity )
{
}
}
The code to retrieve the boundary segments and convert them to my own representation can be implemented as follows:
///<summary>/// Retrieve the room plan view boundary /// polygon loops and convert to 2D integer-based./// For optimisation and consistency reasons, /// convert all coordinates to integer values in/// millimetres. Revit precision is limited to /// 1/16 of an inch, which is abaut 1.2 mm, anyway.///</summary>JtLoops GetRoomLoops( Room room )
{
SpatialElementBoundaryOptions opt
= newSpatialElementBoundaryOptions();
opt.SpatialElementBoundaryLocation =
SpatialElementBoundaryLocation.Center; // loops closed//SpatialElementBoundaryLocation.Finish; // loops not closedIList<IList<BoundarySegment>> loops = room.
GetBoundarySegments( opt );
int nLoops = loops.Count;
JtLoops jtloops = newJtLoops( nLoops );
foreach( IList<BoundarySegment> loop in loops )
{
int nSegments = loop.Count;
JtLoop jtloop = newJtLoop( nSegments );
XYZ p0 = null; // loop start pointXYZ p; // segment start pointXYZ q = null; // segment end pointforeach( BoundarySegment seg in loop )
{
p = seg.Curve.get_EndPoint( 0 );
jtloop.Add( newPoint2dInt( p ) );
Debug.Assert( null == q || q.IsAlmostEqualTo( p ),
"expected last endpoint to equal current start point" );
q = seg.Curve.get_EndPoint( 1 );
Debug.Print( "{0} --> {1}",
Util.PointString( p.ToUv() ),
Util.PointString( q.ToUv() ) );
if( null == p0 )
{
p0 = p; // save loop start point
}
}
Debug.Assert( q.IsAlmostEqualTo( p0 ),
"expected last endpoint to equal loop start point" );
jtloops.Add( jtloop );
}
return jtloops;
}
My external command mainline Execute method driving this method also implements some fancy pre- and post-selection support and reporting code listing the contents of my 2D integer-based loops in the Visual Studio debug output window:
publicResult Execute(
ExternalCommandData commandData,
refstring message,
ElementSet elements )
{
UIApplication uiapp = commandData.Application;
UIDocument uidoc = uiapp.ActiveUIDocument;
Application app = uiapp.Application;
Document doc = uidoc.Document;
if( null == doc )
{
ErrorMsg( "Please run this command in a valid"
+ " Revit project document." );
returnResult.Failed;
}
// Iterate over all pre-selected roomsList<ElementId> ids = null;
Selection sel = uidoc.Selection;
if( 0 < sel.Elements.Size )
{
foreach( Element e in sel.Elements )
{
if( !( e isRoom ) )
{
ErrorMsg( "Please pre-select only room"
+ " elements before running this command." );
returnResult.Failed;
}
if( null == ids )
{
ids = newList<ElementId>( 1 );
}
ids.Add( e.Id );
}
}
// If no rooms were pre-selected, // prompt for post-selectionif( null == ids )
{
IList<Reference> refs = null;
try
{
refs = sel.PickObjects( ObjectType.Element,
newRoomSelectionFilter(),
"Please select rooms." );
}
catch( Autodesk.Revit.Exceptions
.OperationCanceledException )
{
returnResult.Cancelled;
}
ids = newList<ElementId>(
refs.Select<Reference, ElementId>(
r => r.ElementId ) );
}
foreach( ElementId id in ids )
{
Element e = doc.GetElement( id );
Debug.Assert( e isRoom,
"expected parts only" );
JtLoops roomLoops = GetRoomLoops( e asRoom );
int nLoops = roomLoops.Count;
Debug.Print( "{0} has {1} loop{2}{3}",
Util.ElementDescription( e ), nLoops,
Util.PluralSuffix( nLoops ),
Util.DotOrColon( nLoops ) );
int i = 0;
foreach( JtLoop loop in roomLoops )
{
Debug.Print( " {0}: {1}", i++, loop.ToString() );
}
}
returnResult.Succeeded;
}
I tested this on a simple sample room with one hole:
The original start and end points of the boundary segments for this room are reported as follows:
My next step for this add-in is to implement code to determine 2D plan view boundary polygons for the furniture and equipment family instances contained within the selected room.
I am hoping to be able to make use of the ExtrusionAnalyzer class for this.
As I mentioned, it is supplied a solid geometry, a plane, and a direction.
From those, it calculates the outer boundary of the shadow cast by the solid onto the input plane along the extrusion direction.
And I have my day-to-day support tasks to attend to too...
Anyway, here is
GetRoomLoops.zip containing
the complete source code, Visual Studio solution and add-in manifest of the current state of this external command.
As you probably noticed by now, Revit 2014 has been announced.
Here are the
main product features:
Design
Enhanced Autodesk Exchange
Dockable window framework
New stairs and railings
Temporary view templates to change view properties temporarily
Point cloud improvements improve appearance and controls of point clouds
Parameter variance for groups to vary the value of parameters assigned to groups
New air terminal on duct enables placement of air terminal device on duct face
New angle constraints to restrict angles for pipe, duct, and cable tray
Cap open ends of pipe or duct content quickly
CSV file removal project, embedding the data into families
New plumbing template
New rebar placement constraints customization
Reinforcement enhancements with more rebar options
Documentation
Non-rectangular crop regions
Split elevations
Alternate dimensions
Improved rebar tagging to annotate multiple elements with a single tag
Improved positioning of beams and braces
Enhanced schedules providing greater control of schedule formatting
Visualization
Displaced views enable creation of displaced or exploded building design views
Enhanced visualization
New rendering to reduce project costs with cloud-based rendering
Analysis
New building element energy analysis
Enhanced structural analytical model
New duct and pipe calculations to API
Each and every one of these is really exciting in itself, and almost all include or are even based on enhanced API support.
Some of the ones that seem most exciting to me are the non-rectangular crop regions, parameter variance for groups, enhanced schedules, displaced views... well, as said, they are really all very exciting.
And this is not even mentioning some of the new API features, such as the possibility to launch a Revit command and control copy and paste operations.
I will get to all that real soon now.
Still, ignoring all that busy-ness for the moment, I chug along and return to a topic similar to my discussion on
retrieving all touching beams.
In that post, I showed how to recursively traverse a collection of beams and detect the neighbouring ones at each end using an ElementIntersectsSolidFilter with a sphere.
Today I explore the task of determining the columns supporting a selected beam using various different methods.
The task at hand is to pick the indicated beam and report the columns supporting it:
Simple, ain't it?
Solid and Element Intersection
Initially, I made some further attempts and experiments using the ElementIntersectsSolidFilter and ElementIntersectsElementFilter functionality.
The sample provided includes code exercising these tests and also the original bounding box implementation.
It is enclosed in 'if' statements checking the setting of the following Boolean variables:
That enables them to be switched on and off interactively in the debugger, if the need arises.
Unfortunately, as it turns out, they both do not work.
The beam solid is not quite big enough to intersect the columns, because the beam is cut back so that it does not actually intersect them.
The same problem also prevents use the element intersection filter.
It would be great if there was a possibility to grow the solid just slightly, e.g. offset all its faces outwards by an inch or two or define a tolerance before executing the intersection check, like for the bounding box.
Unfortunately, growing or shrinking an arbitrary solid is much harder than a bounding box and therefore not implemented.
Bounding Box Filter Tolerance
The BoundingBoxIsInsideFilter can be instantiated with an optional double tolerance value that allows control over the match criteria by using the given tolerance in the geometry comparison.
By default, the tolerance is set to zero.
If the tolerance is positive, the iterated element outline may extend the tolerance distance outside of the given outline in each coordinate to be a match.
If the tolerance is negative, the iterated element outline must lie at least the tolerance distance inside the given outline in each coordinate to be a match.
This is exactly what I would need here for the solid and element intersection filters as well.
Unfortunately, as said, such a tolerance is not supported by the solid or element intersection filters.
Moving the Beam Downwards in a Temporary Transaction
Lacking the tolerance option, I thought that maybe it would help to move the beam down a bit, and that it would intersect the supporting columns then.
Unfortunately, that does not help either.
The problem is not only that the bottom face of the beam solid may be located above the end of the supporting columns, but also that the ends of the beam are cut back to avoid intersecting the columns, so their solids do not intersect even when the beam is moved slightly downwards.
This temporary movement forces a switch from read-only to manual transaction mode, of course, even though the model is not actually modify in any way in the end.
To make sure that the movement really is executed and the model updated before checking for an intersection, I followed the advice given by Arnošt Löbel on
enhancing the temporary transaction trick and
encapsulated the whole operation in a transaction group.
The group is rolled rolled back at the end, but, before doing so, the encapsulated temporary transaction around the movement of the beam can be committed and the updated geometry evaluated.
By the way, these changes temporarily caused an error saying "Attempted to read or write protected memory. This is often an indication that other memory is corrupt."
This was probably due to accessing the list of elements generated within the temporary transaction after the transaction group is rolled back.
I handled that problem by storing the element ids instead of the live elements themselves inside the transaction, and then opening the elements via their id after rolling back the changes.
As said, the temporary downward translation still did not produce any intersections.
Time for another idea.
GetGeneratingElementIds
A colleague suggested that if the beam is cut back by the columns, you can iterate over the beam geometry faces and call the Element GetGeneratingElementIds method on each one, which might return the column element ids.
In this example, you might also get faces generated by joins with other beams.
I tried this, encapsulating the code in the section if( useGeneratingIds ).
Unfortunately, all the generating ids belong to the structural framing or floors categories, so that does not help to determine the columns either.
No columns at all are returned by this method.
Cylinder Along Location Line Offset Downwards
I then realised that I could create a much simpler independent solid shape to intersect the columns by extruding a cylinder along the beam location line, offset downwards to just below the bottom face of the beam.
It might require extending a little at each end.
All supporting columns should be intersected by it.
A similar approach as the solid intersection with an offset cylinder could obviously also be implemented using the ray casting functionality provided by the FindReferencesByDirection and FindReferencesWithContextByDirection methods and the ReferenceIntersector wrapper class.
I did not implement any sample code demonstrating this, because I wanted to generalise the straight beam cylinder solution to a more general arbitrary curve case.
Sweep Along Location Curve Offset Downwards
Once I had the working solution using a cylinder defined by the straight beam location line offset downwards, I realised that it might be nice to use more generic sweep along curve functionality instead.
After all, the beam location curve might not be straight, and a non-linear location curve can quite simply be used to generate a non-linear solid using the CreateSweptGeometry method instead of CreateExtrusionGeometry.
My first attempt caused the CreateSweptGeometry method to throw an exception saying that "The given attachment point don't lie in the plane of the Curve Loop. Parameter name: pathAttachmentCrvIdx & pathAttachmentParam".
That was my fault, though, because I was providing zero for the parameter value, which is probably the normalised curve start point parameter.
When providing the raw parameter value returned by curve.get_EndParameter( 0 ) instead, all works fine.
Here is a rather unrealistic sample spline beam that I used for testing:
The algorithm reports the following supporting columns for the spline beam:
All is well.
Conclusion and Source Code
Here is the complete source code of this command, including the test branches that are disabled by default.
As said, they can be enabled and tested by modifying the Boolean switches interactively in the debugger.
[Transaction( TransactionMode.ReadOnly )]
publicclassCommand : IExternalCommand
{
constdouble _eps = 0.1e-9;
double SignedDistanceTo( Plane plane, XYZ p )
{
XYZ v = plane.Normal;
return v.DotProduct( p )
- v.DotProduct( plane.Origin );
}
classBeamPickFilter : ISelectionFilter
{
publicbool AllowElement( Element e )
{
returnnull != e.Category
&& e.Category.Id.IntegerValue.Equals(
(int) BuiltInCategory.OST_StructuralFraming );
}
publicbool AllowReference( Reference r, XYZ p )
{
returntrue;
}
}
publicResult Execute(
ExternalCommandData commandData,
refstring message,
ElementSet elements )
{
UIApplication uiapp = commandData.Application;
UIDocument uidoc = uiapp.ActiveUIDocument;
Application app = uiapp.Application;
Document doc = uidoc.Document;
Element beam = null;
try
{
Selection sel = uidoc.Selection;
Reference r = sel.PickObject(
ObjectType.Element,
newBeamPickFilter(),
"Please select a beam" );
beam = doc.GetElement( r );
}
catch( RvtOperationCanceledException )
{
returnResult.Cancelled;
}
List<ElementId> columnIds = null;
// Optionally switch between different tests// by modifying these values in the debuggerbool useBoundingBox = false;
bool useSolid = false;
bool useElement = false;
bool useGeneratingIds = false;
bool useLocation = true;
#region Obsolete previous attempts
if( useBoundingBox )
{
BoundingBoxXYZ box = beam.get_BoundingBox( null );
Outline outline = newOutline( box.Min, box.Max );
ElementFilter bbfilter = newBoundingBoxIntersectsFilter(
outline, 0.1 );
FilteredElementCollector columns
= newFilteredElementCollector( doc )
.WhereElementIsNotElementType()
.OfCategory( BuiltInCategory.OST_StructuralColumns )
.WherePasses( bbfilter );
}
if( useSolid )
{
Options opt = app.Create.NewGeometryOptions();
GeometryElement geo = beam.get_Geometry( opt );
Solid solid = null;
foreach( GeometryObject obj in geo )
{
solid = obj asSolid;
if( null != solid
&& 0 < solid.Faces.Size )
{
break;
}
}
ElementFilter beamIntersectFilter
= newElementIntersectsSolidFilter( solid );
FilteredElementCollector columns
= newFilteredElementCollector( doc )
.WhereElementIsNotElementType()
.OfCategory( BuiltInCategory.OST_StructuralColumns )
.WherePasses( beamIntersectFilter );
}
if( useElement )
{
// Initially, no columns are found to // intersect the beam. Maybe it will help to// move the beam down a bit?using( TransactionGroup txg = newTransactionGroup( doc ) )
{
txg.Start( "Find Columns Intersecting Beam" );
using( Transaction tx = newTransaction( doc ) )
{
tx.Start( "Temporarily Move Beam Down a Little" );
ElementTransformUtils.MoveElement(
doc, beam.Id, -0.1 * XYZ.BasisZ );
tx.Commit();
}
ElementFilter beamIntersectFilter
= newElementIntersectsElementFilter( beam );
FilteredElementCollector columns
= newFilteredElementCollector( doc )
.WhereElementIsNotElementType()
.OfCategory( BuiltInCategory.OST_StructuralColumns )
.WherePasses( beamIntersectFilter );
columnIds = newList<ElementId>(
columns.ToElementIds() );
// We do not commit the transaction group, // because no modifications should be saved.// The transaction group is only created and // started to encapsulate the transactions // required by the IsolateElementTemporary // method. Since the transaction group is not // committed, the changes are automatically // discarded.//txg.Commit();
}
}
if( useGeneratingIds )
{
Options opt = app.Create.NewGeometryOptions();
GeometryElement geo = beam.get_Geometry( opt );
foreach( GeometryObject obj in geo )
{
Solid solid = obj asSolid;
if( null != solid )
{
foreach( Face f in solid.Faces )
{
ICollection<ElementId> ids
= beam.GetGeneratingElementIds( f );
foreach( ElementId id in ids )
{
Element e = doc.GetElement( id );
if( null != e.Category
&& e.Category.Id.IntegerValue.Equals(
(int) BuiltInCategory.OST_StructuralColumns ) )
{
columnIds.Add( id );
}
}
}
}
}
}
#endregion// Obsolete previous attemptsif( useLocation )
{
// Determine beam location curve for // extrusion direction and lengthLocationCurve lc = beam.Location asLocationCurve;
Curve curve = lc.Curve;
Solid solid = null;
// Handle generic curve parameters.// See below for simplified linear case.XYZ p = curve.get_EndPoint( 0 );
double param = curve.get_EndParameter( 0 );
Transform transform = curve.ComputeDerivatives( param, false );
Debug.Assert( p.IsAlmostEqualTo( transform.Origin ),
"expected derivative origin to equal evaluation curve point" );
XYZ tangent = transform.BasisX;
// Use bounding box to determine elevation of// bottom of beam and how far downwards to // offset location line -- one inch below // beam bottom.BoundingBoxXYZ bb = beam.get_BoundingBox( null );
Debug.Assert( .001 > bb.Min.Z - bb.Max.Z,
"expected horizontal beam" );
double inch = 1.0 / 12.0;
double beamBottom = bb.Min.Z;
XYZ arcCenter = newXYZ( p.X, p.Y,
beamBottom - inch );
Plane plane = newPlane( tangent, arcCenter );
CurveLoop profileLoop = newCurveLoop();
Autodesk.Revit.Creation.Application creapp
= app.Create;
Arc arc1 = creapp.NewArc(
plane, inch, 0, Math.PI );
Arc arc2 = creapp.NewArc(
plane, inch, Math.PI, 2 * Math.PI );
profileLoop.Append( arc1 );
profileLoop.Append( arc2 );
List<CurveLoop> loops = newList<CurveLoop>( 1 );
loops.Add( profileLoop );
// Switch this on to handle a straight beam as // a separate simplified case using // CreateExtrusionGeometry instead of the // generic CreateSweptGeometry solution.bool checkForLine = false;
if( checkForLine && curve isLine )
{
XYZ q = curve.get_EndPoint( 1 );
XYZ v = q - p;
Debug.Assert( 0.01 > v.Z,
"expected horizontal beam" );
Debug.Assert( v.IsAlmostEqualTo( tangent ),
"expected straight beam vector to equal start tangent" );
solid = GeometryCreationUtilities
.CreateExtrusionGeometry( loops, v, v.GetLength() );
}
else
{
// Offset location curve downward // one inch below beam bottom faceXYZ offset = arcCenter - p;
transform = Transform.get_Translation(
offset );
CurveLoop sweepPath = newCurveLoop();
sweepPath.Append( curve.get_Transformed(
transform ) );
solid = GeometryCreationUtilities
.CreateSweptGeometry(
sweepPath, 0, param, loops );
}
ElementFilter beamIntersectFilter
= newElementIntersectsSolidFilter( solid );
columnIds = newList<ElementId>(
newFilteredElementCollector( doc )
.WhereElementIsNotElementType()
.OfCategory( BuiltInCategory.OST_StructuralColumns )
.WherePasses( beamIntersectFilter )
.ToElementIds() );
}
int n = (null == columnIds)
? 0
: columnIds.Count<ElementId>();
string s1 = string.Format(
"Selected beam is supported by {0} column{1}{2}",
n,
( 1 == n ? "" : "s" ),
( 0 == n ? "." : ":" ) );
string s2 = "<None>";
if( 0 < n )
{
uidoc.Selection.Elements.Clear();
foreach( ElementId id in columnIds )
{
Element e = doc.GetElement( id );
uidoc.Selection.Elements.Add( e );
}
s2 = string.Join( ", ",
columnIds.ConvertAll<string>(
id => id.IntegerValue.ToString() ) );
}
TaskDialog.Show( s1, s2 );
returnResult.Succeeded;
}
}
For your convenience, here is
GetBeamColumns06.zip containing
the complete source code, Visual Studio solution and add-in manifest of the GetBeamColumns external command.
I hope you find this both interesting and useful as a basis for your own variants.
Today is the last morning meeting with my European DevTech colleagues here in Brittany, and time to travel back to Switzerland.
Before leaving, here is a useful real-world productivity tool by Trevor Taylor of ZGF,
Zimmer Gunsul Frasca Architects LLP,
with his own description of the task and its solution:
The task I want to address is to match interior elevation tags with the rooms they fall inside.
This is used to track back and rename the corresponding views.
Naming interior elevation views is a major time-burner as there can be thousands of views in a project to rename, and they have to be coordinated when the room numbers or names change.
If we don’t tie view names to rooms, we have no hope of ever keeping track of so many views.
I originally attacked this by trying to determine the location of the view tags in the model, but had to give up on that one.
Ben Bishoff of Ideate gave me the idea to approach the problem from the view rather than from the interior elevation tag, so I worked backwards from the view cropbox property using the RevitLookup app and arrived at this simple solution:
1. Collect all views of class ‘ViewSection’, then filter down to those of ‘Interior Elevation’ type:
2. Construct a point at the midpoint of the front bottom edge of each interior elevation view's CropBox.
The CropBox is conveniently relative to the projection plane of the view.
X is to right, Y is up, and Z is the view depth:
// Construct a point at the midpoint of the // front bottom edge of the elev view cropboxdouble xmax = v.CropBox.Max.X;
double xmin = v.CropBox.Min.X;
double zmax = v.CropBox.Max.Z;
XYZ pt = newXYZ(
xmax - 0.5 * ( xmax - xmin ),
1.0,
zmin );
3. Get pt's translation to project's coordinate system:
// Get pt's translation to // project coordinate system
pt = v.CropBox.Transform.OfPoint( pt );
That takes care of the heavy lifting. Pass pt to the GetRoomAtPoint method and you obtain the room, if there is one to get:
It does exactly what I want it to now and will save our teams countless hours on large projects by organizing the names of interior elevation views by room name:
Here is a
complete sample project including
a test model in case you’d like to check it out yourself.
Many thanks to Trevor for this useful tool, his research, implementation, and generous sharing.
Before I sign off, here are two other nice pointers, not related to Revit:
Au Bout du Monde and Super-Sonic Stereo
Brittany is at the end of the earth, or Finisterre, au bout du monde, and I have seen many restaurants, a parking place, and a number of other establishments named after that around here.
That reminded me of one of my favourite humorous YouTube films, a Russian cartoon also named
Au Bout du Monde,
by Konstantin Bronzit:
It was very fitting to share with my colleagues, since two of us are Russian as well.
We enjoyed it a lot, and I hope you like it as much as I do.
While we are at it, yesterday, my son Christopher pointed out another quite nice blog with explanations of what-if questions, the most recent one featuring sound sources, asking
what if my stereo flew around at super-sonic speed?
I enjoyed that as well, and so might you.
Last but not least, today is the first day of spring and
vernal equinox!
Now I am back at work again, enjoying the queries coming in from developers, postponing other important things such as my own long-term class and presentation preparations etc.
Addicted to helping, that's me.
Anyway, here is a piece of doubly good news, in that the developers mentioned below did a good job of helping themselves, in addition to achieving a radical speed improvement from 40 hours down to 3 minutes, i.e. just 0.125% of the original time, to programmatically generate some Revit families.
Question: The problem was initially reported by
Hu Zhao in a recent
comment:
I have 80,000 elements to generate, now I use sweep; do you know which is faster of sweep and extrusion?
Which can offer a better performance for the computer?
It took 40 hours for the sweep operations.
Is it possible to accelerate this?
In Rhino script, only 10 minutes is enough for this operation.
Zhao's scenario is to create about 80,000 sweeps in Revit in one shot.
Per his test, it will take about 20 hours, while his workmate can achieve it in Rhino within 10 minutes.
Zhao expects that Revit could do faster.
As Zhao's test was done in an open family document (with UIDocument), I tried to use in-memory document but it still take about 18 hours to draw all 80K sweeps.
Zhao noticed that although all the drawing work is done in a single transaction, it seems that Revit tries to regenerate the document after there are sweeps created already automatically.
We did set the RegenerationOption to be Manual.
We say it because we saw Revit progress bar in the bottom-left corner indicates it.
Our questions:
Can we prohibit Revit's automatic regeneration?
More straightly, do we have any approach to get the job done faster?
Thanks a lot! And Happy Chinese Snake Year!
Answer: There may be a stray regeneration happening in sweep creation.
If you can demonstrate that (with a sample that creates something less than 80,000 sweeps, please :-), please provide a reproducible case for further analysis.
It is also worth asking what the use case is for 80,000 sweeps in one family.
Are you possibly modelling things so detailed that that a building model with multiple instances of this will have performance problems?
Maybe you could add an image of what you are creating to motivate the development team by helping them understand.
Response: Here is an image showing what we are trying to achieve:
Each sweep corresponds to a single wooden roof in the image.
Here is also
Hu Zhao's source code.
We used an in-memory document and added a StopWatch to measure the time consumed.
The source code implements an external command in Program.cs.
It will pop up a window and let you select a XML file, which contains creation data of all the sweeps.
You can find the XML file along with the .sln file.
To make things simple, I only put 2000+ sweeps creation data in the XML file.
Finally I want to share the good news from Hu Zhao.
He resolved the performance problem by splitting all sweeps into 24 families and then combining them together.
It only takes 3 minutes now.
Of course, the cost is the reduced granularity; the model now no longer has individual roofs with embedded family instances.
But as he is OK with it, we can say we have found the solution.
I am now working in a project plan team for the design of Arch_Tec_Lab in ETH Zürich
(youtube link).
The parametric roof is generated in Rhino by my colleague, and the wooden beams will be assembled by robotic arms.
Before I joined this team, they imported the roof (including roof wooden structure) in DWG format, which did not display correctly in some plan and section views.
In 3D view, it led to Revit crash because of too many triangle meshes in this imported format.
So I wrote a plugin to import the wooden beams (around 80k) of the roof.
We choose XML to exchange the position information, and I choose sweep instead of extrusion to generate each beam.
For now, the roof structure is divided into 7 parts of XML, which generates 7 families.
It took me about 20 hours by using 3 dell precision computers.
Because the roof will be modified several times in the following days, I am now searching for a way to reduce the generation time.
I found that the speed of sweep is slower when the amount increased, and the regeneration that I cannot avoid is especially slow when the amount is above 1500.
Yesterday I found a way to accelerate the generation greatly.
I did like this: divided each 1 part into 24 pieces sections with the mark in XML, then generate each section into single family, and then combine them into a major family.
These actions are all made by the plugin.
This is much more quicker.
Let me introduce our project once again.
The Arch_Tec_Lab is a role model project of department of Architecture, ETH Zürich.
The team includes most important professors in the Institute of Technology in Architecture.
The project aims to realize the BIM control in the life circle.
Now we are using Revit to an extremely detail, and we enjoy the cloud rendering in Autodesk 360, except we wish it would also support animation rendering.
Now we are working on the construction drawings in Revit.
In Switzerland, few architects use Autodesk products, because of the majority of the MAC users.
But we want build a model for the architects and the students in department.
Many thanks to David and Hu for their research and sharing this remarkable performance enhancement!
Addendum: For completeness' sake, here is the
slow source code and
the
complete fast solution.
Hu clarifies: the code we posted yesterday is the slow version.
The 'slow source code' shows how it works.
The 'complete fast solution' is the quicker version.
There are 8 parts to generate.
I finished all the work above in 20 hours using 3 computers, and now I can generate the smallest part in 3 minutes.
Actually, the bigger part took about 6 hours by the slow method, and now takes about 50 minutes.
The time depends on how many parts I separate.
I never test all the 80000 sweeps in one run, maybe it will cost one month ~:D – because the speed runs slower when time goes.
I am winding up my vacation on the
Island of Ischia that
I first discovered three years back.
It is one of my favourite places in the world, where I visited natural wonders such as hot thermal springs of Sorgeto in
Panza,
where you can build your own bathtub to mix the hot spring water with the cold waves from the sea,
and the antique natural roman bath of Cavascura on the Maronti Beach that includes a natural sauna in hewn rock that has been running uninterrupted for 3000 years.
On the way here, I first got off at the wrong ferry stop in Procida.
That also proved very nice, with all the families taking their numerous kids for a Sunday walk, to chat and play in the main square.
From the ferry, I walked up to the Chiesa della Annunziata and on to the Piano Liguori and then explored the peninsula of San Pancrazio with its spectacular old church and caves.
Here is the view from Piano Liguori back towards Procida and Naples.
I visited the Trani family restaurant, olives and vineyard.
They even built their own staircase access to the sea, tunnelling through the almost vertical rock facing the water.
It was a huge pleasure chatting with one of the Trani sons and listening to his joy over the traditional agricultural work amidst the 'canto dei gabbiani', the song of the gulls, and the challenges of making it economically viable through some interaction with tourism.
I later saw this wonderful example of a scalable implementation and thinking outside the box:
Parametric NURB Spline Curve Evaluation
Question: I use the Curve.Evaluate method to obtain equally spaced points along a curve.
This works fine for most curve types, but not on a NURB Spline that I created.
When I evaluate with regularly incremented parameter values, e.g. 0, .2, .4, .6, .8 and 1, the distances of the resulting positions along the curve are not visually equal.
Using the same curve to create a ruled surface shows the lines and points appearing where one would expect.
Here is an example with the green lines displayed where ruled lines on the face occur, and text letters at the locations returned by the evaluate function:
Answer: The
parameterisation of a NURB spline is
such that regions of strong curvature are 'longer' in the parameter space than the actual curve length.
The assumption that a parameter measures uniformly along its length can only be made for lines and arcs.
For ellipses and splines the parameter does not typically follow this pattern, because of the mathematics involved.
"A ‘normalized’ parameter.
The start value of the parameter is 0.0, and the end value is 1.0.
For some curve types, this makes evaluation of the curve along its extents very easy; for example, the midpoint of a line is at parameter 0.5.
(Note that for more complex curve equations like Splines this assumption cannot always be made)."
The Revit Geometry API currently does not offer the ability to measure by segment-length or normalized segment-length.
If it did, a more iterative solution would be easy to describe.
Recent AEC DevBlog Overview
Here is an overview of some of the posts by my colleagues on the AEC DevBlog in the past few weeks:
Bubble end and free end arguments: explores
the detailed meaning of the two XYZ parameters bubbleEnd and freeEnd to the NewReferencePlane method.
Hiding sections in ViewPlan presents
sample code to filter out the plan views, filter the sections created on each one of them, and call HideElements to suppress them in the view.
Using Rebar.CreateFromCurves with curves:
if a Rebar shape includes any straight edges, then its first and last curves must be straight lines, unless it is completely made up of arcs.
Actually, to tell the truth, it is just one day, so far, this week.
Wow, my days are too full.
I am getting nothing else done!
Ribbon Button Identification
Here is a simple question that came up a few times in the past:
Question: I have implemented a ribbon push button.
Now I would like to find out which button was pushed in the event handler, i.e. external command, so that I can attach the same external command to handle multiple different buttons.
How can this be achieved, please?
I would like to identify the button pushed and access its data in the external command Execute method.
Answer: The official Revit API user interface design expects you to implement a separate command class for each push button.
If you do so, then you can identify the push button clicked by the user by the command class whose Execute method was triggered.
If you choose to circumvent this official approach somehow, e.g. by attaching the same external command implementation to several different push buttons, than you will have to identify them by some other means.
When you define a push button, you also specify its Name via the RibbonItemData.Name property. This name is unique across the entire ribbon, or at least within the panel containing the button, so you could use this to identify the button.
Unfortunately, the Revit API does not provide any support for finding out which push button was used to trigger the external command.
While this functionality is not provided by the Revit API, you may be able to make use of the generic .NET Framework libraries to find out.
A concrete example of subscribing to a UI Automation event telling you which button was clicked is given by Rudolf Honke to
pimp my ribbon,
where he shows how to implement an event listener for both Revit and AutoCAD ribbon systems:
ComponentManager.UIElementActivated
+= newEventHandler<Autodesk.Windows
.UIElementActivatedEventArgs>(
ComponentManager_UIElementActivated );
void ComponentManager_UIElementActivated(
object sender,
Autodesk.Windows.UIElementActivatedEventArgs e )
{
// e.UiElement.PersistId says which item has been pressed
}
You will need to test whether this is of any use to you. This event listener may be pretty inefficient and slow.
The use of these APIs is unsupported and at your own risk.
If you wish to use the unsupported UI Automation option, you will have to find out for yourself how to do so using the ample information available on the web and elsewhere.
Probably the easiest way to go is indeed to implement a separate command handler for each button.
If you have many commands requiring similar functionality, e.g. an identical initialisation procedure, you can either derive them all from one common base class, or wrap the common functionality in a method that you can call from each separate external command Execute method.
Opening a URL from a Ribbon Button
Another, simpler, question on hooking up ribbon buttons was also happily and rapidly resolved:
Question: I am attempting to create a URL link like this:
How can I enable the user to click on the button and get the link to open in a web browser?
I know how to create the ribbon panel and push button, but not how to connect to the URL in a web browser.
Answer: Two options:
Official, easy, supported: implement an external command and launch the browser from that.
Unofficial, harder, unsupported: use something else, e.g. the .NET framework UI Automation library discussed above.
Response: Option #1 worked and it was very easy indeed.
All it required was adding just one line of code in the external command, System.Diagnostics.Process.Start, and then simply implementing it in the external application file.
This enables us to populate a custom ribbon panel that includes URLs linking to PDFs and webpages containing office Revit standards.
This provides our staff a quick and easy way to access our standards within the Revit environment.
External command:
External application:
Revit ribbon:
XYZ Comparison and Point and Vector Behaviour
A completely different topic deals with the Revit API XYZ class, its dual use to represent points and vectors, and how to compare them, raised by Graham Cook in the Revit API discussion forum in the thread on a
XYZ.IsAlmostEqualTo problem:
Question: Maybe I'm misinterpreting the IsAlmostEqualTo method.
I want to determine whether two points are within 4 inches of each other, i.e. 0.3333 feet.
I thought the following code would achieve that:
XYZ a = new XYZ(41.7, -76, 0);
XYZ b = new XYZ(4.7, -76, 0);
bool almostEqual = a.IsAlmostEqualTo(b, 0.333);
But even though in this example the two points are 37 feet apart, IsAlmostEqualTo returns true.
Am I misunderstanding the tolerance part of the method?
Answer: The behaviour you observe is correct and intended and has a simple explanation:
IsAlmostEqualTo is implemented for vector comparison, not point comparison, and is mostly designed around small tolerances.
The default for IsAlmostEqualTo with no tolerance input argument given is 1e-09.
At small tolerances, points and vectors are more interchangeable, so I would use the default IsAlmostEqualTo to find "equivalent" points too.
Because this is a directional comparison, not a distance one, 0.333 for the epsilon means to check if the two vectors are within a significant angular range of each other, which the example input points actually pass.
Even if the points are treated as vectors and normalized, they will still pass the comparison because of the wide epsilon permitted.
If you want to compare points with a non-tiny epsilon using the allowed distance between them, use XYZ.DistanceTo method instead.
Autodesk provides a large number of programming platforms and APIs.
An overview is given by the
Platform Technologies overview on the Autodesk Developer Network.
Just to see and be astounded by their sheer number, here is a list of links to the associated developer pages:
Emile Kfouri now pointed out this discussion of the
GBS REST API that
will certainly enable you (yes, you!) to achieve very cool things.
I am looking forward to hearing about your ideas!
I hope you had a wonderful break and a good start into the New Year.
I spent New Year's Eve with my sons and some friends in a Swiss mountain village.
We had a nice hike up and snowboard ride down the neighbouring hill with the Wildhorn in the background:
Now back to work and the Revit API.
Here is a question that came up just before Christmas:
Question: I'm trying to create a wall by face on a slanted conceptual mass face.
Do you have any example code showing how this might work?
Answer: Here is a simple method that can be run in a project containing one single conceptual mass family instance.
It searches the mass for a slanted face whose normal is (-1, 0, 1) and creates a FaceWall instance on that with no problem:
Question: I tried to access the Location property of a FaceWall object.
However, nothing I tried leads to any useful result.
In the following code snippet, both theCurve and thePoint remain null:
Reference r1 = doc.Selection.PickObject(
ObjectType.Element, "Please pick a wall: " );
Element e1 = doc.GetElement( r1 );
FaceWall faceWall = e1 as FaceWall;
LocationCurve theCurve = faceWall.Location
as LocationCurve;
LocationPoint thePoint = faceWall.Location
as LocationPoint;
Answer: Whenever a Revit element location is more complex than a simple point or curve, its Location property will contain more complex internal data (or possibly nothing at all) that currently cannot be represented by the Revit API.
In the case of a FaceWall, it is obvious that no simple point or curve would accurately represent the wall location.
In such cases, another option to determine a location for this kind of object is to analyse its geometry and use the information that provides.
Autodesk OrgOrgChart
For something completely different, here is an interesting graphical representation of the organisational changes Autodesk has been through in the last couple of years.
The OrgOrgChart (Organic Organization Chart) project looks at the evolution of a company structure over time.
A snapshot of the Autodesk organizational hierarchy was taken each day between May 2007 and June 2011:
This representation is reminiscent of the Disk Tree visualization technique developed at Xerox Parc
(Chi E.H., S.K. Card, 1999,
Sensemaking of Evolving Web Sites Using Visualization Spreadsheets,
Proceedings of the Symposium on Information Visualization (InfoVis '99), pp. 18-25),
showing implicit and explicit affinities among information items and revealing relationships that are difficult to discover in traditional representations.
Proximity or connections between items can reveal complex relationships in a comprehensive way, provide visual access to data structures, and enable detection of tendencies or patterns in the process.
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.
I am busy with Autodesk University and developer conference preparations.
The Mac is happily chugging along.
Welcome to Building Performance Analysis
In spite of these busy times, Emile Kfouri and John Kennedy just launched a new blog on the topic of
Building Performance Analysis that
will probably be of great interest to many of us Revit API aficionados.
Meanwhile, here is an API issue that raises a couple of new points:
Triangulation Level of Detail
Question: I am triangulating Revit geometry face objects to get the triangular meshes.
When I first tried this for each face of a standard UB-Universal beam I didn't get the root face (the curved face joining the flange and the web).
I found that setting the Options detail level to Fine sorted that issue.
Now I wonder:
What is the 'LevelOfDetail' argument for on the Triangulate method?
Does it relate to the Options detail level?
Does calling the Triangulate method without an argument correspond to calling the same method with 0.5 as the argument?
Is it possible to triangulate a solid element as a whole so that the triangle vertices match along an edge line, rather than tessellate each face individually, which gives non-matching vertices along the edge?
Answer: The level of detail argument to the triangulation has to do with the level of precision of the triangulation.
Lower levels of detail will result in a coarser tessellation with less triangles but maybe less accuracy.
Higher levels of detail will result in a more accurate but also more populous tessellation (with more triangles as a result).
Of course this applies only to faces with curvature – planes should be well represented at any level.
But this does not answer the question asked – the face comes from Element.Geometry, and there are different sets of faces typically for DetailLevels Coarse, Medium and Fine.
The Fine geometry will likely have more details such as the curved face mentioned, where the Medium geometry won't.
So it has nothing to do with Triangulate(LOD), the inputs are different from the different geometry so you get different output.
To address your other questions:
As usual, a simple question with a not-so-simple answer.
When the level of detail is not used, it uses the Revit defaults for when Revit triangulates faces for graphics (there may actually be a cache of these facets in some cases).
There is probably no universal lod set in these situations.
Yes! The Revit 2013 API introduced the SolidUtils.TessellateSolidOrShell method.
This routine processes the entire solid at once so that there are matched vertices at the edges.
It takes a SolidOrShellTessellationControls argument including an option about LevelOfDetail with the same range of values and meaning as in the Face.Triangulate case.
Revit doesn't use this method itself, so this data will never be cached.
Response: Thank you for finding out.
I should have searched the 2013 Developer's Guide to find the SolidUtils class myself.
I generally use the 2012 PDF guide since it is easier to read/use than the 2013 WikiHelp.
Answer: I understand about the PDF.
However, the Wikihelp has the advantage that Google will return hits in it for you.
I generally use the Revit API help file RevitAPI.chm, and especially I often consult the What's New section.
It would actually be quite useful to have a collection of each release's What's New section.
Maybe I should write a blog post on that.
From then on, all my further search is online.
Obviously, I start with the blog first.
That is what it is there for: my own personal knowledge base :-)
I received and have started installing and learning to use my new laptop, which is a Mac, I'm finishing up my preparations for Autodesk University, and I'm working on preparations for upcoming developer conferences, so you can imagine there is rather a lot to do and 24 hours per day seem very limiting.
I love the Unix shell!
Goodbye, DOS box.
Thank goodness my colleague Joe Ye answered a case that I found interesting and would like to share with you, since Joe is busy preparing his own Chinese developer meetings and conferences:
Question: I am trying to determine the subcategory of each solid element in a family.
Can this be done using the API?
My family that has two different solids and they have different subcategories.
I want to know which solid I am looking at and figured that subcategories would be a good way to go, since they can be applied to solids in a family.
Please note that the family with the subcategories must be nested as a non-shared family and I have to find out what element geometry is what in the supercomponent family.
I know that I can find out what subcategory each solid element belongs to when I open the family, but can I do so when its loaded as well?
Is there any other way of identifying different geometry objects once a family instance has been placed in the project?
Besides looking at the geometrical aspects such as volume, areas etc.
Answer: Yes, you can retrieve the solid category from the family instance in the model.
The category information is stored in the Solid object's GraphicStyle property.
The following code retrieves all the solid category names from a selected family instance:
[Transaction( TransactionMode.ReadOnly )]
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;
Reference r = sel.PickObject( ObjectType.Element,
"Please pick a family instance" );
Element e = doc.GetElement( r );
GeometryElement geoElem = e.get_Geometry(
newOptions() );
int n = 0;
string s = string.Empty;
foreach( GeometryObject obj in geoElem )
{
if( obj isGeometryInstance )
{
GeometryInstance geoInst
= obj asGeometryInstance;
GeometryElement geoElem2
= geoInst.GetSymbolGeometry();
foreach( GeometryObject geoObj2 in geoElem2 )
{
if( geoObj2 isSolid )
{
Solid solid = geoObj2 asSolid;
ElementId id = solid.GraphicsStyleId;
GraphicsStyle gStyle = doc.GetElement(
id ) asGraphicsStyle;
if( gStyle != null )
{
++n;
s += gStyle.GraphicsStyleCategory.Name
+ "\r\n";
}
}
}
}
}
TaskDialog.Show( n.ToString() + " Graphics Styles",
s );
returnResult.Succeeded;
}
}
Here is an example of running this and selecting one of the doors in the basic sample project:
The following materials are listed for this instance:
In this case, the trick is to temporarily delete the sheet and watch which schedules are wiped out by that operation.
Note that in a comment on that post, Phillip Miller points out a way to achieve the same result using a filtered element collector taking a view id argument.
Who is going to create a benchmark comparing the relative speed of these two approaches?