Here are some pointers from a conversation I had with Daren Thomas on how to display a reference point in the model for debugging purposes that may be on general interest.
Question: I am playing around with the FindReferencesWithContextByDirection method and would like to display the references found as coloured blobs or something in the model.
How can I achieve this, please?
This question could also be simplified into: how to create an easily visible dot at a 3D point in the model?
Can such a "dot" exist on its own or must it be drawn onto a face? How?
I never really got into actually creating geometry in Revit.
Answer by Daren: I figured it out on my own now:
I can create a SketchPlane and draw a ModelCurve on it.
Given two points a and b, this
RevitPythonShell
code will create a green line between them in the 3D view:
transaction = Transaction(doc)
transaction.Start('draw a line')
c = XYZ(0, 0, 0)
v0 = XYZ(a.X - b.X, a.Y - b.Y, a.Z - b.Z)
v1 = XYZ(a.X - c.X, a.Y - c.Y, a.Z - c.Z)
plane = Plane(v0.CrossProduct(v1), a)
sketchPlane = doc.Create.NewSketchPlane(plane)
line = doc.Application.Create.NewLineBound(a, b)
doc.Create.NewModelCurve(line, sketchPlane)
transaction.Commit()
Answer by Jeremy: Yes, model lines is probably the simplest way to go.
You need model curves in 3D, and can use detail curves in a 2D view.
The Creator class included in The Building Code samples includes several usage examples implemented in C#.
It was used to graphically highlight geometry in numerous previous posts, and last updated to
display the boundaries of a slab.
All it does is package what you describe above nicely, though.
For more complex geometry and transient display, you can also use the analysis visualisation framework AVF.
It could be used to draw a full 3D sphere as a marker, if you prefer, as I showed when displaying
Apollonian spheres using AVF.
I have a situation where my slabs are cut by openings, however, and I need to retrieve the original uncut gross slab boundary.
Here is an example with two slabs A and B and an opening cutting out part of both of them:
The solution provided retrieves the cut out net slab boundary:
How can I obtain the following original gross uncut slab boundary?
Answer: One way to obtain the unmodified geometry of the slab is to use the temporary transaction trick to temporarily eliminate all openings and other elements affecting its shape.
It shows how the transaction handling for the temporary operation can be simply and cleanly encapsulated.
It analyses the material quantities of walls, floors, and roofs, and outputs the result to Excel by implementing the following steps:
Collect all roof elements
Iterate through each material found in each roof element
Find the net volume and area of the material
Store the material quantities
Write the results to the output file
Find the gross material quantities
Start a new transaction
Delete the elements that cut the host (doors, windows, openings)
Find the volume and area for each material
Store the material quantities
Write the results to the output file
Rollback the transaction
Repeat the above steps for walls and floors
Open the output file in Microsoft Excel
To determine the boundary lines of the floor slab with all opening removed, you could proceed in the same fashion as described in step 6 for obtaining the gross material quantities.
Temporarily delete then openings in a separate transaction, retrieve the original slab geometry, and abort the transaction to restore the original state.
This technique is easy to implement.
We reused this method for many different purposes a number of times, e.g.
Addendum: Arnošt Löbel pointed out that
this will not always work.
Some modifications require the transaction to be committed before they take full effect.
The
workaround for
that is to enclose it within a transaction group and roll back the group at the end.
Apphack @ AU
Do you have an interesting AutoCAD app you would like to publish?
Will you attend Autodesk University 2012 and would you like to win an attractive prize?
Answer:
Yes, this is indeed a rather simple question.
Model lines are model curves.
Model curves are not family instances.
Your collector includes a family instance filter.
The family instance filter will eliminate all model curves, preventing any model lines from being found.
Simply remove the family instance class filter, or replace it by a model curve class filter.
By the way, the most common quick filters are all available using shortcut methods on the collector class itself.
Using the shortcut methods saves you from having to instantiate a separate filter instance, and ensures that all the filters you apply are quick filers.
All the following three variants should work, and presumably produce identical results:
Saikat Bhattacharya demonstrates the use of the View class HideElements and UnhideElements methods to
control the visibility of linked files,
and points out that the SetVisibility method cannot be used for this purpose.
Visual Studio 2012 Model Editor Supports OBJ and FBX
Visual Studio 2012 includes a
Model Editor that
enables you to inspect the 3-D model formats OBJ, Autodesk FBX and COLLADA.
You can also use built-in 3-D primitive generation and materials to create placeholder art for 3-D games and applications.
If you wanted to work in depth with FBX yourself, you would presumably use the
FBX SDK,
currently at version 2013.3, supporting C++ development in Visual Studio 2005, 2008, and 2010.
Sie Mögen Sich
For something completely unrelated to computers and technology, here is a seven minute video, Sie mögen sich, a quite deep philosophical relationship analysis by Shaban & Käptn Peng that my son Christopher pointed out and that I enjoyed a lot:
It is in German, however, and probably makes no sense at all unless you understand the language.
From Revit 2013 onwards, you have full API access to create and read data from stairs and railings, as well as their separate subcomponents, as you can see from this excerpt of the What's New section in the Revit API help file RevitAPI.chm:
Stairs and Railings API – Stairs and Stair Components
The new Stairs and StairsType classes in the Autodesk.Revit.DB.Architecture namespace provide access to new stairs elements in the Revit database. Note that these API classes only provide access to the Stairs created 'by component' introduced in this release.
Stair elements created by sketch cannot be accessed as a Stairs object in the API.
It is possible to identify both types of stairs using the BuiltInCategory OST_Stairs. The static predicate method Stairs.IsByComponent
indicates whether a stair alement is created by components (runs, landings and supports) or not.
The classes StairsLanding, StairsLandingType, StairsRun and StairsRunType provide access to the subcomponents and subcomponent types of the new Stairs elements.
The pre-2013 stairs are still supported by Revit in old models, and their geometry is still not accessible.
Several useful hints about pre-2013 stair geometry access can be gleaned from the Autodesk Revit API forum discussion on
accessing stair geometry,
so I edited and summarised it a bit for reproduction here:
Question: I have a multiple floor building with stairs and need to access the stair geometry using the Revit API.
Unfortunately, only post-2013 stairs provide access to full location and geometry information.
For pre-2013 stairs, you will have to use other means.
Question: My stair models were created in Revit 2011 and 2012.
I am trying to retrieve their information using the Revit 2013 API.
I can access information like Name, TypeName, FamilyName, TopLevel, BaseLevel, ActualNumRiser, ActualRiserHeight, TreadWidth, TreadDepth, TravelDistance, etc.
What I actually is geometry information like location points.
Is there any method to get the location information and the rooms associated with stairs?
Answer: You could calculate location points for pre-2013 stairs in a variety of different ways.
First, you could get the BoundingBoxXYZ of your stair.
Using this, you could calculate its lower-most centre point as your desired location point.
Second, you could browse the stair geometry, also searching for the bottom-most point.
For this, you need to iterate over all its solids and faces.
Having calculated a proper point, you can use the Document GetRoomAtPoint method to find the proper room.
You could compare the elevations of your levels to find the one fitting your location point, but actually both sketch and family based stairs already maintain a built-in parameter STAIRS_BASE_LEVEL_PARAM which provides the level directly, avoiding the need to iterate and search through all level elevations.
Question: By using BoundingBoxXYZ I can only get the Max and Min points of the stair bounding box.
I still do not know location point of the landing or floor at the top and bottom of each stairway.
How can I get these points from the bounding box?
Also, GetRoomAtPoint returns null when I try to use it to retrieve the room name.
What should be causing that?
Answer: GetRoomAtPoint needs a point that is inside the room volume.
It may be useful to ensure that the point you test is not located directly on the volume border.
Because of tolerance issues, it could easily end up being falsely calculated as very slightly outside.
LocationPoint from BoundingBoxXYZ:
What I meant was that you could calculate the point that is in the lower centre of the bounding box, meaning on the bottom.
Add one meter to its Z value, e.g., to ensure it to be inside the 3D room volume.
Use that to calculate a point to be passed to GetRoomAtPoint.
To calculate the landing and floor location points, you could analyze the stair geometry, meaning that you need to interpret it.
Question: Is it possible to analyze the stair's geometry for the Revit file created prior to 2013?
And is it possible to access the geometry information of stairs created by sketch and not by component?
"But even if we cannot *modify* a stair this way, we can get *more* information than we would get if we just read the stair's parameters or properties.
For example, the longest CurveArray in the Sketch.Profile of a stair can be combined with the number of steps to calculate the 3D data of each step's front side.
The curves in the profile won't give us the correct Z data because the sketch is just a projection, but using the stair height and number of steps, we can calculate the Z distance for each step."
Of course you only can obtain 'stupid' geometry such as lines, faces and solids.
By combining that with the parameter values you mention above, you could *calculate* the information you want.
I must admit that there is no common approach for each type of stairs, but the point is how to obtain any information *at all*.
Many thanks to Rudolf the Revitalizer for this deep and inspiring explanation!
Revit 2013 Roombook Extension
The Revit Roombook extension helps calculate the surface area of walls, floors and ceiling elements, room circumferences and the total number of furnishing elements.
It enables automated detection of room areas and surfaces and supports users in configuring this data to local requirements, achieving more accurate model take-offs, and exporting quantified results to Excel and QTO, Autodesk Quantity Takeoff.
I happened to notice this sweet
15 minute video explaining
it in full detail:
A special highlight that I particularly enjoyed in this case is the effective and pleasant narration in a soft, very nicely and slightly accented female voice :-)
Here is another longish post that is not directly related to the Revit API, but rather Microsoft Office programming, more specifically PowerPoint.
However, the sample material I present at the end is very directly related to Revit programming, so rest assured that all is well and hopefully interesting.
The
Autodesk University
class materials will be due one of these days.
The
deadline is
not exactly looming yet, but definitely and inexorably creeping up.
I still need to finish off the handout for my class
CP4107 on
the new Revit 2013 UI API functionality, probably the most exciting one of my
three AU sessions.
Normally, I think the handout should be finished before I start creating the slide deck.
I think the recommended steps for preparing a class should look like this:
Gather experience.
Know what you have to share.
Create compelling illustrative samples.
Summarise the presentation content in a handout document.
Create a slide deck to illustrate.
For some reason, however, I sometimes find myself with an existing slide deck in hand, and no handout document yet created.
In that case, I often use the slide deck to create a skeleton handout document by exporting the pure text content of the slide deck and converting that to a document and outline.
In this case, of course, I am aided and abetted in doing this dirty deed by the fact that I already have the finished slide deck from the precursor session at DevCamp.
Slide Deck Comparison and Text Extraction
Another important reason for extracting pure text content from a slide deck is for comparison purposes.
Working on presentations, especially as a group project, I regularly want to find out exactly what changed between one version and another of a given set of PowerPoint slides.
Differences in graphics are often easy to spot, but small changes in the text may also be really important and harder to identify.
Given the pure text content, it is trivial to find and pinpoint them using text comparison tools such as Unix
diff and
Visual Studio
Windiff, of course.
Enter ppt2txt, my very own PowerPoint slide deck text extractor.
I implemented it back in 2007 and have been using it happily ever since.
The thought of sharing it with you guys crossed my mind, so here it is.
Alternative Comparison Method
My colleague Marat Mirgaleev mentioned another way to compare similar slide decks without extracting the text.
One of the subfolders contains the slides.
Within that folder, each individual slide is represented by a separate XML file.
A simple slide by slide comparison of the deck can be performed by comparing the date associated with each one.
Anyway, back to my Ppt2txt text extractor.
Ppt2txt Usage
Usage is simple: you feed it a PPT file, it extracts all the text on all the slides and spits it out in a pure ASCII text file.
By default, the output file path is determined from the input PPT, just replacing the extension.
You can use the '-f' option to specify a different output filename or '-' to redirect the output to stdout.
By default, a prefix string 'Title: ' is added in front of each slide title, making it easy to identify and e.g. search and replace the paragraph style of all titles in one fell swoop.
If you prefer no such prefix, use the '-t' option to suppress it.
Here is the usage message explaining this a bit more succinctly (copy to a text editor to see the truncated lines in full):
ppt2txt 2.0 * PowerPoint Slide Deck Text Extractor
(C) 2007-2012 Jeremy Tammik Autodesk Inc.
usage: ppt2txt [-f textfilename] [-t] pptfilename
-t: skip adding prefix 'Title: ' to each slide title
-f: write output to textfilename.txt
default: pptfilename.txt
-f-: stdout
Ppt2txt Implementation
Here is the code implementing this:
#region Namespaces
using System;
using System.IO;
using Microsoft.Office.Core;
using Ppt = Microsoft.Office.Interop.PowerPoint;
usingPptType = Microsoft.Office.Interop.PowerPoint.PpPlaceholderType;
#endregion// Namespacesnamespace ppt2txt
{
classProgram
{
///<summary>/// Usage prompt.///</summary>staticstring[] _usage = newstring[] {
"ppt2txt 2.0 * Powerpoint Slide Deck Text Extractor",
" (C) 2007-2012 Jeremy Tammik Autodesk Inc.",
"usage: ppt2txt [-f textfilename] [-t] pptfilename",
" -t: skip adding prefix 'Title: ' to each slide title",
" -f: write output to textfilename.txt",
" default: pptfilename.txt",
" -f-: stdout"
};
///<summary>/// Constant representing 'True' in Powerpoint API.///</summary>constMsoTriState _t = MsoTriState.msoTrue;
///<summary>/// Windows carriage return + linefeed combination.///</summary>conststring _crlf = "\r\n";
///<summary>/// Prepend a linefeed to every /// non-empty input string.///</summary>staticstring PrependLinefeed( string s )
{
return 0 == s.Length ? string.Empty : _crlf + s;
}
///<summary>/// Append a new paragraph p to the given text s/// with a linefeed separator in between if s is/// not empty.///</summary>staticstring AppendParagraph( string s, string p )
{
if( 0 < s.Length )
{
s += _crlf;
}
return s + p;
}
#region Escape unpleasant Powerpoint characters
//static Encoding _ascii = Encoding.ASCII;///<summary>/// Remove unpleasant non-ascii characters /// that crop up in powerpoint.///</summary>staticstring normalise_string( string s )
{
s = s.Replace( "…", "..." );
s = s.Replace( '–', '-' );
s = s.Replace( '‘', '\'' ); // backward apostrophe from powerpoint
s = s.Replace( '’', '\'' ); // forward apostrophe from powerpoint
s = s.Replace( ' ', ' ' ); // strange space char from powerpoint//// quote://
s = s.Replace( '\u0060', '\'' ); // grave accent
s = s.Replace( '\u00b4', '\'' ); // acute accent
s = s.Replace( '\u2018', '\'' ); // left single quotation mark
s = s.Replace( '\u2019', '\'' ); // right single quotation mark
s = s.Replace( '\u201C', '"' ); // left double quotation mark
s = s.Replace( '\u201D', '"' ); // right double quotation mark //// space://
s = s.Replace( '\x0b', '\n' ); // vertical tab
s = s.Replace( '\u0020', ' ' ); // space basic latin
s = s.Replace( '\u00a0', ' ' ); // no-break space latin-1 supplement
s = s.Replace( '\u1680', ' ' ); // ogham space mark ogham
s = s.Replace( '\u180e', ' ' ); // mongolian vowel separator mongolian
s = s.Replace( '\u2000', ' ' ); // en quad general punctuation
s = s.Replace( '\u2001', ' ' ); // em quad
s = s.Replace( '\u2002', ' ' ); // en space
s = s.Replace( '\u2003', ' ' ); // em space
s = s.Replace( '\u2004', ' ' ); // three-per-em space
s = s.Replace( '\u2005', ' ' ); // four-per-em space
s = s.Replace( '\u2006', ' ' ); // six-per-em space
s = s.Replace( '\u2007', ' ' ); // figure space
s = s.Replace( '\u2008', ' ' ); // punctuation space
s = s.Replace( '\u2009', ' ' ); // thin space
s = s.Replace( '\u200a', ' ' ); // hair space
s = s.Replace( '\u202f', ' ' ); // narrow no-break space
s = s.Replace( '\u205f', ' ' ); // medium mathematical space
s = s.Replace( '\u3000', ' ' ); // ideographic spacereturn s;
//using System.Globalization;//using System.Text;//System.Globalization.UnicodeCategory.//s = s.Replace( "", "-->" ); // right arrow//s = s.Replace( '', '\'' ); // backquote Unicode characters//return Encoding.ASCII.GetBytes( s ).ToString();//return _ascii.GetString( _ascii.GetBytes( s ) );
}
#endregion// Escape unpleasant Powerpoint characters///<summary>/// Get all the text from a given Ppt /// shape, delimited by _crlf.///</summary>staticstring GetShapeText( Ppt.Shape shape )
{
string s = null;
if( _t == shape.HasTextFrame
&& _t == shape.TextFrame.HasText )
{
s = shape.TextFrame.TextRange.Text.Trim();
string[] a = s.Split(
newchar[] { '\r', '\n' } );
s = string.Empty;
foreach( string s2 in a )
{
s += PrependLinefeed( s2.Trim() );
}
s = s.Trim();
if( 0 == s.Length )
{
s = null;
}
}
return s;
}
staticint Main( string[] args )
{
// Command line argument values.string filename_in = null;
string filename_out = string.Empty;
bool title_prefix = true;
// Process command line arguments.int n = args.Length;
int i = 0;
while( i < n )
{
string a = args[i];
if( '-' == a[0] )
{
if( 't' == a[1] )
{
title_prefix = !title_prefix;
}
elseif( 'f' == a[1] )
{
filename_out = a.Substring( 2 );
if( 0 == filename_out.Length )
{
++i;
if( i >= n )
{
Console.Error.WriteLine(
"-f option lacks output filename" );
break;
}
filename_out = args[i];
}
}
else
{
Console.Error.WriteLine(
string.Format( "invalid option '{0}'",
a ) );
break;
}
}
elseif( null == filename_in )
{
filename_in = a;
if( !File.Exists( filename_in ) )
{
if( File.Exists( filename_in + ".ppt" ) )
{
filename_in = filename_in + ".ppt";
}
elseif( File.Exists(
filename_in + ".pptx" ) )
{
filename_in = filename_in + ".pptx";
}
}
if( !File.Exists( filename_in ) )
{
Console.Error.WriteLine(
string.Format(
"unable to open input file '{0}'",
filename_in ) );
break;
}
}
else
{
Console.Error.WriteLine(
string.Format( "invalid argument '{0}'",
a ) );
break;
}
++i;
}
if( null == filename_in
|| !File.Exists( filename_in ) )
{
foreach( string s in _usage )
{
Console.Error.WriteLine( s );
}
return 1;
}
// Determine full input and output filenames.
filename_in = Path.GetFullPath( filename_in );
string ext = "txt";
if( 0 == filename_out.Length )
{
filename_out = Path.ChangeExtension(
filename_in, ext );
}
elseif( !filename_out.Equals( "-" ) )
{
filename_out = Path.Combine(
Path.GetDirectoryName( filename_in ),
filename_out + "." + ext );
}
// Open output file and process ppt input.using( StreamWriter sw
= filename_out.Equals( "-" )
? newStreamWriter( Console.OpenStandardOutput() )
: newStreamWriter( filename_out ) )
{
if( null == sw )
{
Console.Error.WriteLine(
string.Format(
"unable to write to output file '{0}'",
filename_out ) );
}
else
{
string s, title, subtitle, body, notes;
Ppt.Application app = new Ppt.Application();
app.Visible = _t;
Ppt._Presentation p = app.Presentations.Open(
filename_in, _t, _t, _t );
foreach( Ppt._Slide slide in p.Slides )
{
title = subtitle = body = string.Empty;
foreach( Ppt.Shape shape in slide.Shapes )
{
s = GetShapeText( shape );
if( null != s )
{
if( MsoShapeType.msoPlaceholder
== shape.Type )
{
switch( shape.PlaceholderFormat.Type )
{
casePptType.ppPlaceholderTitle:
casePptType.ppPlaceholderCenterTitle:
title = AppendParagraph(
title, s );
break;
casePptType.ppPlaceholderSubtitle:
subtitle = AppendParagraph(
subtitle, s );
break;
casePptType.ppPlaceholderBody:
default: // e.g., ppPlaceholderObject
body = AppendParagraph(
body, s );
break;
}
}
else
{
body = AppendParagraph(
body, s );
}
}
}
// Retrieve notes text.
notes = string.Empty;
foreach( Ppt.Shape shape
in slide.NotesPage.Shapes )
{
s = GetShapeText( shape );
if( null != s )
{
if( slide.SlideIndex.ToString() != s )
{
notes = AppendParagraph(
notes, s );
}
}
}
// Write output for current slide.if( 0 < ( title.Length + subtitle.Length
+ body.Length + notes.Length ) )
{
s = ( ( 0 == title.Length )
? ( _crlf + "Slide " + slide.SlideIndex.ToString() )
: ( _crlf + ( title_prefix ? "Title: " : "" ) + title ) )
+ PrependLinefeed( subtitle )
+ PrependLinefeed( body )
+ PrependLinefeed( notes );
sw.WriteLine(
normalise_string(
s.Replace( "\n", _crlf ) ) );
}
}
p.Close();
sw.Close();
}
}
return 0;
}
}
}
I reformatted it to minimise the number of truncated overlong lines.
Look at the source download below or copy to a text editor to see all the lines in full.
Ppt2txt Download
Here is
ppt2txt_2.zip containing
the full source code and Visual Studio solution for this utility.
I hope you find it as useful as I do.
Comments and suggestions for improvement are appreciated, as always.
Formatting in Word
One of the ppt2txt options I mentioned above was to turn off the 'Title: ' prefix.
The question is why to implement and turn it on in the first place.
Well, it prefixes each slide heading with a string saying 'Title: '.
Assuming none of my existing text already contains that exact string, I can then search and replace it globally.
In Word, I can event use search and replace to set a specific paragraph style by selecting Home > Replace > Find what: Title: > Replace with: > More>> > Format > Style... > Heading 3 > Replace All:
Next, I replace the same search string by an empty string with no formatting to remove all the prefixes.
I also replace duplicate paragraph end markers by single ones using the search string "^p^p" and the replacement "^p".
Sample Run
Here is the draft material that I am working on for my AU class
CP4107 on
the Revit 2013 UI API, including source pptx, intermediate txt, target docx and pdf renderings:
PDF rendering of the handout, which is what I submit as the final handout for AU participants.
The handout document already contained some boilerplate text at the beginning and end before inserting the slide deck text.
I replaced the 'Title: ' prefixes first by the Heading 3 paragraph style and then by empty strings to remove them.
I removed all duplicate paragraph markers.
That's it.
The PDF renderings of the slide deck and handout document were generated using
eDocPrintPro.
My next step is obvious: add some artificial intelligence with domain knowledge, linguistic skills and good taste to create a completely finished handout fully automatically.
A much easier way to reach the exact download I am after is to enter the appropriate string in Google.
That takes me directly to the download page with no need to manually traverse any convoluted search paths.
Here is a list of the Google search strings I entered and the resulting URLs:
Here are the enhancements listed for this update.
API enhancements are not included this time, unfortunately, but there are some of those in there as well:
Revit Architecture
Improves stability when upgrading 2012 project which contains analytical walls.
Improves stability when adjusting multiple sketch lines of a floor at the same moment.
Improves stability when editing the gutters on a roof object with the Add/Remove Segment tool.
Improves stability when renaming a Stair run type or landing to ‘none’.
Improves stability when using the ribbon Cancel Edit Mode button to exit Stair Edit Mode.
Improves stability when using an open profile for a wall sweep.
Improves stability when splitting a wall.
Revit Structure
Enables the Area, Area Boundary and Tag Area ribbon commands.
Improves stability when using Beam Systems.
Improves the respect of the Top In-Plane parameter for Analytical Models Walls after wall is modified.
Improves the joining of Rebar to the host surfaces.
Improves retention of Rebar Shapes upgraded within Revit 2012 projects.
Improves stability when editing Rebar Shapes in the Family Editor.
Improves display of underlay items in non-wireframe Structural Views.
Revit MEP
Improves stability when sizing Duct.
Corrects the use of tees when specified in duct routing preferences within projects upgraded from Revit 2012.
Improves stability when upgrading a pipe fitting connected to multiple systems.
Improves stability when editing duct fitting size in the active view and then editing the duct fitting type in the Property Palette.
Revit Platform
Improves stability when Save to Central or Sync with Central.
Improves stability when utilizing Create Assembly Views from the project browser.
Improves stability when editing a label within the family editor.
Improves stability when saving file to a location with low disc space.
Improves stability when launching Export gbXML Settings dialog.
Improves stability and memory usage within graphics display.
Improves stability during IFC data import which contains openings with no usable geometry.
Improves stability when importing IFC data which imported into Revit 2012 with warnings.
Improves import of clipped solids during IFC import.
Improves IFC Export to support buildingSMART International IFC certification.
Improves stability when linking or attached DWG files.
Improves stability of printing after Export to DWG or DXF format.
Improves stability when creating, duplicating or deleting a material in the Material Editor.
Improves the retention of part parameters within a part upgraded from Revit 2012.
Corrects ribbon tab population after installing .NET 4.5
Improves stability while editing calculated values within a schedule after deleting a parameter used in a formula.
Improves stability with schedules which contains a filter based upon a user defined parameter and elements in linked files do not contain the user defined parameter.
Improves stability when opening Sun Settings dialog.
Improves stability when editing Text Notes.
Improves stability when upgrading Revit 2012 project to Revit 2013.
Improves stability when moving a Crop Region which utilizes the disjoin option.
The build identifier is 20121003_2115 Update Release 2.
I documented a number of personal and possibly naive ideas and experiments regarding
mobile device room location as
a result of my previous so-called education day four weeks back.
Well, last Friday I had another one :-)
This time I took a deeper and hopefully very slightly wiser look at ideas for enabling the following kind of workflow:
Extract relevant data from the BIM.
Populate a cloud database to provide ubiquitous access to relevant data.
Serve up relevant data in an easily consumable format, e.g. for mobile devices.
Enable interaction with the data, e.g. query and update.
I spent some time thinking about how to implement a suitable viewer with support for picking.
My colleagues
Philippe and
Adam have
developed and documented a powerful and impressive
cloud and mobile based workflow including
a 3D viewer implementation supporting object selection for a number of CAD authoring environments, obviously including AutoCAD, Inventor and Revit, posting data to the cloud, e.g. Azure, for consumption on various Android and iOS mobile devices, etc.
I would love to be able to achieve the same effect with significantly less effort.
Lazy...
I already suggested that it might be possible to use
scalable vector graphics SVG
for this kind of thing last time I looked at the cloud and mobile topic.
I still think it really does provide everything I need, since its
base functionality includes
support for interactivity, linking and metadata, among many other things.
My goal here today is to prove that SVG is a good choice for
providing, displaying, and interacting with 2D graphical data.
It is supported by all devices and requires only minimal JavaScript programming to achieve useful results, so an easily tested and extremely portable cross-platform solution seems very feasible.
Today my goal is to supply
the proof of the pudding.
Specifically, I wish to prove that I can use SVG to achieve the following:
Display a room boundary and contained items as coloured polygons using SVG.
The items may be furniture, or any kind of family instance, e.g. simply represented by their bounding boxes.
Embed the SVG into a standard HTML page displayable on any browser, so that it can be viewed and manipulated on any mobile device.
Identify and interact with the displayed objects, e.g. identify the room or furniture and display information about a selected item.
I'll discuss the following steps towards this goal:
I would very much like to show you the results of my SVG experiments right here and now on this page.
An easy way to almost achieve this would be to simply provide normal hypertext links to the different SVG web pages I describe.
A much nicer, more elegant and compelling approach is obviously to embed the SVG directly into this page, which presumably ultimately consists of HTML.
There are a number of different possibilities to
embed SVG into HTML,
which as always are somewhat browser dependent.
After some experimenting in the Typepad blog editor, I decided to use the embed tag, which is consequently the technique you will be enjoying further down if you read on.
All my following examples are accessible in two ways, both using a hypertext link to open up a new page presenting the SVG, and an embedded inline version that you can interact with in situ. You can of course view this page's source in your browser to examine both methods.
Colour Picker
I started out searching the web for the terms "svg pick".
One hit was a pretty impressive
SVG colour picker implemented
and published by Kevin Hughes in 2004 as a way to learn SVG:
It includes documentation, links to learn SVG, and additional SVG notes.
A colour picker seems like a good starting point, although I found this particular example a bit overwhelming for a project that I was hoping to finish in just a couple of hours.
I searched for something simpler instead, and turned up a presentation on
using canvas in SVG by
Klaus Förster of the University of Innsbruck including a much simpler
CSS3 colour picker which
helped get me started implementing my very own
colour picker:
Do as the man says: click on one of the little coloured rectangles at the top, and the selected colour is displayed in the bottom.
That provides pretty conclusive evidence that we really can display simple geometric shapes, interactively pick them, and react on the selection, all within a standard web page in a browser with no additional software installed, doesn't it?
How is this achieved?
This implementation requires three files: an image displaying the coloured boxes to select at the top, a JavaScript helper file, and the SVG itself.
The main component is the following SVG code, embedded into the HTML page using the embed tag (copy to a text editor to see the truncated lines in full):
<?xmlversion="1.0"encoding="us-ascii" ?><svgxmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink"><title>Jeremy's SVG color picker</title><scripttype="text/javascript"xlink:href="SVGCanvasElement.js" /><scripttype="text/javascript"><![CDATA[window.onload = function() { var canvas = new SVGCanvasElement(10,10,240,240); var context = canvas.getContext('2d'); var image = document.getElementById('svgImage'); var hint = document.getElementById('svgHint').lastChild; var active = document.getElementById('activeColor'); var marked = document.getElementById('markedColor'); context.drawImage( canvas.importImage(image),0,0,canvas.width,canvas.height ); context.canvas.parentNode.style.display = 'none'; image.onclick = function(evt) { var sx = evt.clientX-image.x.baseVal.value; var sy = evt.clientY-image.y.baseVal.value; var pxArr = context.getImageData(sx,sy,1,1).data; var fill = 'rgb('+pxArr[0]+','+pxArr[1]+','+pxArr[2]+')'; var opac = 1.0/255.0*pxArr[3]; active.setAttributeNS(null,"fill",fill); active.setAttributeNS(null,"fill-opacity",opac); marked.x.baseVal.value = evt.clientX - (sx % 20); marked.y.baseVal.value = evt.clientY - (sy % 20); hint.firstChild.nodeValue = "fill='"+fill+"' fill-opacity='"+opac+"'"; };};]]></script><imageid="svgImage"x="10"y="10"width="240"height="240"xlink:href="img/svg_css3_colors.png" /><rectid="activeColor"x="10"y="260"width="240"height="30"stroke="#000"fill="none"shape-rendering="crispEdges"/><rectid="markedColor"x="-20"y="-20"width="19"height="19"stroke="#000"fill="none"shape-rendering="crispEdges"/><textid="svgTipp"x="10"y="310"font-size="16px">click CSS3 color palette to pick a color</text><textid="svgHint"x="10"y="330"font-size="16px"><tspan></tspan><tspan>picked</tspan></text></svg>
I must admit that I did have to fiddle around considerably in the blog hosting file manager to figure out where best to place the three inter-dependant files.
The best solution, as always, was the simplest.
This colour picker is completely based on Klaus' work, with only minimal cleanup and perfectionistic improvements, such as painting a 19 by 19 rectangle around the picked colour instead of a 20 by 20 one.
Again, how does it work?
The SVG code defines five elements:
The image element to display an image presenting all the selectable colours.
A large rectangle at the bottom to be filled with the currently selected colour.
A small 19 by 19 pixel rectangle to be moved around to mark the current colour selection inside the palette image.
A static text element prompting you to pick a colour.
A dynamic text element displaying the large rectangle's current fill and fill-opacity attribute values.
These elements are retrieved using a snippet of JavaScript code executed on loading the page.
The code also subscribes to the JavaScript onclick event.
The event handler manipulates the elements as shown.
Specifically, the getImageData function retrieves the colour and opacity of the picked point, which is used to calculate the settings and populate the various elements appropriately.
I would say that this provides a credible proof of concept.
Polygon and Groups
Next step: as said, I want to display a room retrieved from the building model and various items contained in it using polygons.
How do I display an arbitrary polygon in SVG?
Searching the web turned up this succinct and complete
polygon with a hole sample
SVG file which basically says it all with no need for any further explanation at all:
It displays a quadrilateral polygon with a triangular hole, which requires just one single SVG element, and adds a group around it for identification purposes:
<gid="polygon"transform="translate(0.5,50.5)"> <pathd="M 35 -10 L 10 -20 15 -40 45 -45 Z M 20 -30 L 35 -35 30 -20 Z"style="fill:blue;fill-opacity:0.2;stroke:blue;stroke-width:1"fill-rule="evenodd" /> </g>
I must admit that I went no further to read any documentation;
I just based all my further experiments on the following assumptions and guesswork.
Will I regret this?
It was quick and fun, anyway.
And it worked :-)
Just guessing, I assume that 'M' stands for 'move to', and 'L' stands for 'draw a line to', and 'Z' closes the polygon.
These keywords are followed by pairs of X and Y coordinates to define the polygon loop start and vertex points.
Note the 'evenodd' fill-rule attribute, which I assume leaves holes in odd-levelled interior loops.
I further assume that an interior loop within a hole is considered filled again with this setting.
Cool.
As you can try out and see for yourself in the room picker provided below, the hole is indeed not considered to be part of the polygon, and picking inside the hole will return nil.
In addition to the polygon, which is what it is all about, this neat little example also adds X and Y gridlines to simplify orientation and analysis, plus square markers for the polygon vertices, filled for the start and empty for the subsequent points.
Here is the complete SVG file implementing all this.
Note the three groups defined around the grid lines, polygon, and marker points:
<?xmlversion="1.0"encoding="UTF-8"standalone="no"?><!-- Created on 15 Mar 2011 by Mike Toews in Vim --><svgxmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="51"
height="51"> <gid="background"> <rectwidth="51"height="51"fill="white" /> </g> <gid="grid"transform="translate(0.5,0.5)"> <linex1="0"y1="0"x2="50"y2="0"style="stroke:#ddd;stroke-width:1"/> <linex1="0"y1="10"x2="50"y2="10"style="stroke:#ddd;stroke-width:1"/> <linex1="0"y1="20"x2="50"y2="20"style="stroke:#ddd;stroke-width:1"/> <linex1="0"y1="30"x2="50"y2="30"style="stroke:#ddd;stroke-width:1"/> <linex1="0"y1="40"x2="50"y2="40"style="stroke:#ddd;stroke-width:1"/> <linex1="0"y1="50"x2="50"y2="50"style="stroke:#ddd;stroke-width:1"/> <linex1="0"y1="0"x2="0"y2="50"style="stroke:#ddd;stroke-width:1"/> <linex1="10"y1="0"x2="10"y2="50"style="stroke:#ddd;stroke-width:1"/> <linex1="20"y1="0"x2="20"y2="50"style="stroke:#ddd;stroke-width:1"/> <linex1="30"y1="0"x2="30"y2="50"style="stroke:#ddd;stroke-width:1"/> <linex1="40"y1="0"x2="40"y2="50"style="stroke:#ddd;stroke-width:1"/> <linex1="50"y1="0"x2="50"y2="50"style="stroke:#ddd;stroke-width:1"/> </g> <gid="polygon"transform="translate(0.5,50.5)"> <pathd="M 35 -10 L 10 -20 15 -40 45 -45 Z M 20 -30 L 35 -35 30 -20 Z"style="fill:blue;fill-opacity:0.2;stroke:blue;stroke-width:1"fill-rule="evenodd" /> </g> <gid="points"transform="translate(-1.5,48.5)"> <rectid="outer point 1/5"width="4"height="4"transform="translate(35,-10)"style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="outer point 2"width="4"height="4"transform="translate(10,-20)"style="fill:white;stroke:blue;stroke-width:1" /> <rectid="outer point 3"width="4"height="4"transform="translate(15,-40)"style="fill:white;stroke:blue;stroke-width:1" /> <rectid="outer point 4"width="4"height="4"transform="translate(45,-45)"style="fill:white;stroke:blue;stroke-width:1" /> <rectid="inner point 1/4"width="4"height="4"transform="translate(20,-30)"style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="inner point 2"width="4"height="4"transform="translate(35,-35)"style="fill:white;stroke:blue;stroke-width:1" /> <rectid="inner point 3"width="4"height="4"transform="translate(30,-20)"style="fill:white;stroke:blue;stroke-width:1" /> </g></svg>
Room and Furniture Picker
With all of this in hand, let's hand-craft a sample SVG file displaying a room and a couple of bits of furniture, allowing us to pick individual elements
, reacting to and displaying the pick results.
After the preparation above, it is surprisingly easy, actually, and impressively self-contained.
Here is
Jeremy's room and furniture picker:
Here is the entire implementation, omitting most of the trivial grid line definition (copy and paste to an editor to see the full truncated lines):
<?xmlversion="1.0"encoding="us-ascii" ?><svgxmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="241"
height="201"> <title>Jeremy's SVG Room and Furniture Picker</title> <scripttype="text/javascript"> <![CDATA[window.onload = function() { var result = document.getElementById('result'); window.onclick = function(e) { var id = e.target.id; if(0==id.length){id="<nil>";} var x = e.clientX; var y = e.clientY; var s = "id='" + id + "' @ " + x + "," + y; result.firstChild.nodeValue = s; };};]]> </script> <gid="background"> <rectwidth="300"height="200"fill="white" /> </g> <gid="grid"transform="translate(0.5,0.5)"> <linex1="0"y1="0"x2="240"y2="0"style="stroke:#ddd;stroke-width:1"/>
...
</g> <gtransform="translate(0.5,0.5)"> <pathid="room"
d="M 20 20 L 220 20 220 150 150 150 150 100 20 100 Z M 160 120 L 210 120 210 140 160 140 Z"
style="fill:blue;fill-opacity:0.2;stroke:blue;stroke-width:1"
fill-rule="evenodd" /> </g> <gid="furniture"transform="translate(0.5,0.5)"> <rectid="table"width="100"height="20"
transform="translate(50,50)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair1"width="8"height="10"
transform="translate(37,55)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair2"width="10"height="8"
transform="translate(60,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair3"width="10"height="8"
transform="translate(80,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair4"width="10"height="8"
transform="translate(100,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair5"width="10"height="8"
transform="translate(120,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> </g> <textid="result"x="20"y="180"font-size="16px">Welcome. Please click around.</text></svg>
Well, that was interesting for me at least.
A logical next step would be the implementation of a web service as described above to be populated with room and furniture data and serve it up on request in SVG and other formats.
No need for an own viewer implementation; I can use the functionality built into the browser on my mobile device.
Struggle with Safari
Oh no!
There I was, proudly bragging my head off about the cross-browser capabilities of SVG, and thinking I had solved all problems in the world.
I just tested my furniture picker on both an iOS iPad and an Android tablet.
Thank God it works on one of them.
Unfortunately the Safari browser does not react to the picks.
I may or may not have made some error in my SVG or JavaScript coding that Safari does not like and other browsers overlook.
It may or may not work with some other browser on iOS.
I may or may not find a solution that is better still.
Ouch, painful.
Sorry.
Still, there we are for the moment.
A happy update: I found a solution by
Phil Archer of
W3C of
that does work on Safari as well as my PC Firefox and Android tablet browser, a
simple SVG in HTML test page.
Phil says that the preferred method is to use the object element and define the SVG file
using the src attribute.
He further presents samples using embed and iframe.
All three work fine on all browsers I tested, including Safari on the iPad.
I reimplemented my room and furniture picker JavaScript code based on Phil's sample SVG.
Here is a normal hypertext link to the updated cross-country implementation, as well as inline versions using both embed and object tags.
They all work on all browsers I tested, which include Firefox on the PC, Safari on the iPad, and the Android browser:
For completness' sake, here is the updated source code:
<svgxmlns='http://www.w3.org/2000/svg'xmlns:xlink='http://www.w3.org/1999/xlink'
onload='startup(evt)'><script><![CDATA[ var SVGDocument = null; var SVGRoot = null; var count=0; var svgns = 'http://www.w3.org/2000/svg'; var xlinkns = 'http://www.w3.org/1999/xlink'; var result = null;function startup(evt){ O=evt.target SVGDoc = O.ownerDocument; SVGRoot = SVGDoc.documentElement; result = SVGDoc.getElementById('result'); O.setAttribute("onclick","report(evt)");}function report(evt){ id = evt.target.id; if(0==id.length){id="<nil>";} x=evt.clientX; y=evt.clientY; s = "id='" + id + "' @ " + x + "," + y; result.firstChild.nodeValue = s;}]]></script> <gid="background"> <rectwidth="300"height="200"fill="white" /> </g> <gid="grid"transform="translate(0.5,0.5)"> <linex1="0"y1="0"x2="240"y2="0"style="stroke:#ddd;stroke-width:1"/>
...
<linex1="240"y1="0"x2="240"y2="200"style="stroke:#ddd;stroke-width:1"/> </g> <gtransform="translate(0.5,0.5)"> <pathid="room"
d="M 20 20 L 220 20 220 150 150 150 150 100 20 100 Z M 160 120 L 210 120 210 140 160 140 Z"
style="fill:blue;fill-opacity:0.2;stroke:blue;stroke-width:1"
fill-rule="evenodd" /> </g> <gid="furniture"transform="translate(0.5,0.5)"> <rectid="table"width="100"height="20"
transform="translate(50,50)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair1"width="8"height="10"
transform="translate(37,55)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair2"width="10"height="8"
transform="translate(60,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair3"width="10"height="8"
transform="translate(80,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair4"width="10"height="8"
transform="translate(100,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> <rectid="chair5"width="10"height="8"
transform="translate(120,75)"
style="fill:blue;stroke:blue;stroke-width:1" /> </g> <textid="result"x="20"y="180"font-size="16px">Welcome. Please click around.</text></svg>
Of course, if you are a real programmer, you will not trust me that the code presented above is up to date, follow the links above to the real live SVG file, and view its source instead.
Wow, that was quite a bit of extra unexpected effort.
I'm afraid my education day stretched on a little bit beyond Friday...
I hope you find it useful.
I provided some explanations for this change back then in 2010, and would now like to add the point provided by Albert Szilvasy via Kean Walmsley to explain why AutoCAD.NET
Point2d and Point3d objects are immutable:
The same reason why System.String is immutable.
The problem is that a mutable type is confusing to use when it appears as a property return value. For example:
class A
{
public XYZ prop {get;}
};
A a;
a.prop.X = 1.0;
Would this syntax mean that I can set the X value of a read-only property? Or would this simply mean that I set the X value of a temporary point object returned by prop?
Just as Kean says: there you have it. :-)
Unfortunately, there are some other areas in the Revit API where this very confusion still arises from time to time.
Here is a nice little example illustrating different approaches to find the end point of the bottom centre line of an inclined beam.
Question: I am trying to find the coordinates of the end points of the bottom centre line of an inclined beam.
Depending on the beam angle, these coordinates will change.
Here is a 3D view of the kind of situation I am looking at:
The front view looks like this with the beam end points marked in orange.
The desired bottom line end points are marked by red points within the circled areas:
I am currently using the following code to retrieve the beam location line start and end points:
One way to approach this is to use the beam solid faces and projection methods provided by the Revit geometry API to calculate the desired points.
You can determine the bottom face of the beam by querying the beam for its solid, iterating over its faces, and searching for the face whose normal vector has a minimal Z value, i.e. points downward the most.
I provided source code samples implementing this for the
bottom and
top faces of a wall.
You can then project the beam location line end points onto that face using the Face.Project method to obtain the desired bottom line end points.
Parameter Values and Trigonometry
A second solution is based on querying some of the beam parameter values and using a little bit of trigonometry.
It calculates the sides of the right angle triangle using the beam direction vector, vertical delta and insertion value and returns the beam bottom coordinate as follows in C#:
void GetBeamInsertionAndVertDelta(
Element beam,
double sideLength,
outdouble beamInsertion,
outdouble beamVertDelta )
{
// sideLength is beam height. // If there is any other element like plate to // be attached to beam bottom then // sideLength = beam height + plate thickness
beamInsertion = 0.0;
beamVertDelta = 0.0;
if( null != beam && _eps < Math.Abs( sideLength ) )
{
Parameter cutLength = beam.get_Parameter(
BuiltInParameter.STRUCTURAL_FRAME_CUT_LENGTH );
Parameter stOffset = beam.get_Parameter(
BuiltInParameter.STRUCTURAL_BEAM_END0_ELEVATION );
Parameter endOffset = beam.get_Parameter(
BuiltInParameter.STRUCTURAL_BEAM_END1_ELEVATION );
double hypotenuse = cutLength.AsDouble();
double side = Math.Abs( stOffset.AsDouble()
- endOffset.AsDouble() );
double angle = Math.Acos( side / hypotenuse );
beamInsertion = ( sideLength / Math.Tan( angle ) );
beamVertDelta = ( sideLength / Math.Sin( angle ) );
}
}
Perpendicular Vector
The simplest solution may possibly be to avoid using the beam geometry as well as the tan and sin functions and just calculate straight away based on the beam location line.
You can determine the vector perpendicular to the beam location line and pointing downward as much as possible, i.e. lying in the plane P defined by the (non-vertical!) location line and the global Z axis.
Let's say the beam location line top two endpoints are stored in 'p' and 'q'.
To determine the bottom points, we just need to know the height 'h' of the beam cross section and calculate a vector 'v' of length 'h' pointing downward and perpendicular to the beam location line.
The desired points are given by p + v and q + v.
I used a similar calculation to determine the wall width or thickness vector 'w' to display the
wall compound layers,
except that it is horizontal, i.e. has a zero Z component, instead of lying in the plane P, with a minimal (negative) Z component.
Something like this untested snippet should do the job, given the beam location curve start point 'p', end point 'q' and height 'h' as defined above:
XYZ u = q - p;
XYZ w = XYZ.BasisZ.Cross( u );
XYZ v = h * w.Cross( u ).Normalized;
As said, the desired points are then given by p + v and q + v.
Determine Hosted Family Host Type
On a different topic, in his latest AEC DevBlog post, Saikat Bhattacharya explains how you can
determine the base host type of a hosted family via
the Host parameter on the family document owner family property, which stores the following integer values corresponding to the FamilyHostingBehavior enumeration:
None
Wall
Floor
Ceiling
Roof
Face
The parameter value is zero, i.e. None, for families created using line and pattern based templates, so their host type cannot be determined using this method.
Note that if you have a face-based family instance, you can query its Host and HostFace properties to retrieve the host element and a reference to the hosting face, respectively.
Two more interesting methods to explore in this context are the FamilyCanConvertToFaceHostBased and ConvertFamilyToFaceHostBased methods provided by the FamilyUtils class.
Here is a short and sweet deletion question handled by Joe Ye:
Question: I am trying to delete a GraphicStyle object in a RFA family document.
I tried to achieve this using the following code, but it was not successful:
Dim elem As Element = m_rvtDoc.Element(
"5ad9f0cf-6eff-4c63-8a44-9f3a87881dd7-00000b22")
m_rvtDoc.Delete(elem)
How can I delete this object?
Answer: Some internal line style objects cannot be deleted, because the model requires their presence.
As you can see in the following image, the Delete and Rename buttons are disabled when Hidden Lines are selected:
Some of the other line types, i.e. GraphicStyle objects, can very well be deleted.
I manually created an own line type in the dialogue shown above.
Since it is my own, the model does not depend on it.
I can then delete the line type I created using the following external command:
[TransactionAttribute( TransactionMode.Manual )]
publicclassCommand : IExternalCommand
{
publicResult Execute(
ExternalCommandData commandData,
refstring messages,
ElementSet elements )
{
UIApplication app = commandData.Application;
Document doc = app.ActiveUIDocument.Document;
Transaction trans = newTransaction( doc );
trans.Start( "Delete Line Style" );
//ElementId id = new ElementId( 4889 );//Element elem = doc.get_Element( id );string guid = "6387d73b-1e94-456a-8804"
+ "-aaaf48a905f0-0000131a";
Element elem = doc.get_Element( guid );
doc.Delete( elem );
trans.Commit();
returnResult.Succeeded;
}
}
By the way, deleting elements obviously requires an open transaction, which needs to be committed after the deletion.
Here is a rather specialised and hitherto undocumented topic of great importance to anyone trying to model precise complex swept geometry.
The issues below arose exploring the tolerance requirements of the 'Pick Path' options in the Revit UI, which map to the ReferenceArray and Reference input to the two FamilyItemFactory API calls for generating this kind of geometry:
It required quite a bit of research to resolve, both by the Revit development team and most of all by
Bill Adkison of
MarinCAD Custom Software Engineering, who is using this to create detailed furniture families like this:
Bill ran into a number of issues creating these very precise and detailed sweeps.
Here is an example of some of the ensuing discussions and results:
Question: I have problems with sweeps created via PickPath.
I am creating many successful pickpath sweeps.
However, in some cases, pickpath sweeps are failing with a MemoryAccessViolation.
One example has two model lines which can each be selected to create a PickPath sweep, but when both are selected together, the sweep fails.
I have confirmed that their endpoints meet up.
One curve is a Hermite spline, the other a line.
When I convert the spline to a set of line segments connecting the fit points, it works.
What is the problem with connecting these two curves into a common sweep pattern?
They fail interactively as well as via API.
This is still basically a programming problem, because I need to know the criteria for when attaching splines and arcs will work.
In another case of failure, a model line and a model arc of short radius: 0.34 inches, and included arc of .61 radians, so the ultimate arc length is .224 inches, well above the 1/32" Revit precision threshold.
You can sweep these curves independently, but they do not sweep when picked together.
In the debugger, their endpoints line up.
Answer: There is a small gap between the line and arc in the model that causes the problems for that sweep path.
Such gaps should be avoided.
Other conditions to watch out for are curves that are nearly tangent, but not quite tangent, or arcs with very large radii that are nearly indistinguishable from lines.
Here are some internal observations which may help give an idea of what is going on:
1. CollectPath3dCurveGRefs complains about the gap between the line and arc in the model. The gap has size about 9.8e-6, but that function uses DOUBLE_EPS (= 1.0e-9) as a point-to-point tolerance. It issues a DBG_WARN but returns ERR_SUCCESS when it finds a gap.
The unit tangents of the line and arc at the point where they (nearly) meet differ by about 0.002 in the z-coordinate (and somewhat less than that in the y-coordinate).
Apparently, Revit can bridge huge angles in lines, while being rather intolerant of even small angles in other areas.
CreateSweptSolid calls fixGapsInSweepPath, which calls trimCurvesToNbrs, but the latter function uses sqrt(0.1)*DOUBLE_EPS as a point-to-point tolerance, so it considers the gap to need "repair". It attempts to intersect the line and the arc, but that ultimately calls circXLine2d, which uses DOUBLE_EPS as a tolerance, so that function does not find an intersection. TrimCurvesToNbrs therefore does not modify the path (i.e., it does not "fix" the gap). It does check that the result is continuous to within a certain tolerance, but it uses the vertex tolerance for that check, and this gap is smaller than the vertex tolerance (which is about 0.0005).
CreateSweptSolid ultimately fails because getCornerTurningTrf fails when finding the corner-turning transform from the line to the arc, because vecAlmostEqual(cornerPnt1, cornerPnt2) is false. This essentially uses DOUBLE_EPS as a point-to-point tolerance. I confirmed that avoiding that rejection in the debugger allows the swept solid to be constructed successfully. Nevertheless, such gap and lack of tangency should be avoided whenever possible.
2. In the model, the line and the Hermite spline are not accurately tangent. Their tangent vectors at the point where the curves meet differ by about 0.01 in one of the coordinates; they meet at an angle of about 0.017 radians (= 1.02 degrees). While computing the mitre surface between the line and the spline, getMiterCurve then calls pathSegsAreNearlyTangent, but that uses a tolerance of one degree, so this case doesn't even meet the criterion for being nearly tangent. getMiterCurve then fails at the check for parallel normVec = (0.49, -0.85, 0.13) and planeNorm = (-0.86, -0.5, 0.0) (approximately).
PlaneNorm is the normal vector of the plane of the sweep path, as computed by getMiterCurve, while normVec was passed down from createSweepAlongCurve, where it was computed by a call to getRadii, which gets it directly from the sweep frame passed to createSweptSolid.
3. Please make the line accurately tangent to the spline in the example.
They meet at an angle slightly greater than one degree.
Of course, making the line accurately tangent to the spline is a difficult numerical problem, and will involve modifying both curves, which are in different planes, which will also have to be modified.
Alternatively, one could recognize the situation and either break the sweep, or go back to all line segments.
4. Please make analytic computations (i.e., those involving lines, arcs, planes, and other simple curves or surfaces as accurate as possible for double-precision floating-point computations.
In such cases, values that would ideally be equal should be considerably less than 1.0e-9.
In general, Revit expects points that are ideally coincident to have a distance considerably less than about 1/160".
In some cases, though, Revit code demands a tighter tolerance.
Curves that are ideally tangent should have an angle at most 0.1 degrees.
The failure to create a sweep for the line/spline path in the sample with a circular profile of radius about 0.4329 might be fixed by making the line tangent to the spline.
5. There seem to be a number of hardcoded almost vertical and almost unit vectors in your code.
Revit definitely does not like slightly off-axis lines and planes.
It will certainly help stability if you can replace all vectors like the following one by the almost identical ones aligned with the cardinal axes:
XYZ normalizedMajorAxis
= new XYZ( 1.0,
-0.00000000000024924835886401896,
0.0000000000000012680828609390458 );
Specifying very slightly off-axis data is just asking for trouble.
Do you really need to model anything that absolutely has to be so slightly off-axis?
Is rounding an option for the input data you provide?
This is not hard, once you are aware of the issue.
Response: 1. Details such as the point-to-point tolerance is exactly the kind of information I am looking for.
The gap in my data is a somewhat bigger than I expected, so I will write code to re-close them after my Revit computations are finished.
Does the fact that the tangents differ by this amount matter, i.e. cause the failure?
Sweep handles 90-degree corners for line segments.
Do non-lines have to have matching tangents?
If so, that is great to know, as well as utterly unobvious.
I can take evasive action if the tangents must match.
What is 'the vertex tolerance' exactly, in contrast to 'point to point'?
For use by sweeps or path validation after the basic point to point?
Is it used for Angles?
Should I just be concerned with the DOUBLE_EPS distance for this problem?
2. So you require tangency for connecting PickPath segments that aren't lines?
That is good to know.
As noted earlier, because PickPath works with corners between lines, I assumed there was no tangent requirement for connecting elements, nor did I see any in the documentation.
So to clarify the rules: must connecting PickPath elements have shared tangents at their connection point, UNLESS they are bound line segments?
Here is the current list of possibilities:
Line – Line: no tangency requirements.
Line – Spline: tangency required.
Line – Arc: tangent required or no?
Spline – Arc: ?
Spline – Spline: ?
Spline – Arc: ?
If tangency requirements are a problem, I will correct them if possible, or revert to line segments.
I updated my logic to detect gaps and tangential inequality at connection points of
multi-curve PickPaths, leading to the key logic summarised in the following code:
///<summary>/// Revit angle tolerance: 1 degree/// has been shown to be stable.///</summary>constdouble kRevitAngleTolerance = Math.PI / 180.0;
XYZ curCurveStartPt = null; // from current curveXYZ prevCurveEndPt = null; // from previous curve or nullXYZ shiftVector = null;
// Use DOUBLE_EPS tolerance in IsAlmostEqualTo // for legitimate endpoint proximity.bool shiftCurve = ( prevCurveEndPt != null )
&& !curCurveStartPt.IsAlmostEqualTo(
prevCurveEndPt );
if( shiftCurve )
{
// This case very rarely triggers.
shiftVector = curCurveStartPt.Subtract(
prevCurveEndPt );
}
XYZ nextCurveStartTan = null; // from next curve or nullbool nextCurveIsLine = null; // from next curve, if anyXYZ curCurveEndTan = null; // from current curvebool curCurveIsLine = null; // from current curvedouble epsTanAngle = nextCurveStartTan.AngleTo(
curCurveEndTan );
// Use cited tolerance of 1 degree between // connecting endpoint tangent vectors. // I actually experimented with up to 5 degrees, // and although I observed cases of tangent // differences of at least 2.6 degrees to// successfully sweep, sometimes it works and // sometimes it excepts.// 1 degree has held up well so far; // I have no known failures with it.// But the result is annoying notches in Revit // in places where AutoCAD sweeps can handle // the same curves automatically.bool writeSweepNow = false;
if( ( nextCurveStartTan != null )
// connecting lines have unequal // tangents but work in Revit.
&& ( !curCurveIsLine || !nextCurveIsLine )
&& ( epsTanAngle > kRevitAngleTolerance ) )
{
// Break off sweep and start a new one;// when tangent angles exceed this, // the sweep can except.// Final code to tweak current curve tangents // would go here; this is a big project in itself.
writeSweepNow = true;
}
// if (shiftCurve)// Current curve is re-read, with shift in // location set in shiftVector.// Append current curve to model and record its // references in current curve reference array.// if (writeSweepNow)// Sweep is written with current curve, and new // Sweep begins with the next curve.// Loop until all curves are accumulated, // then write final sweep.
This eliminates all the failures due to connecting multiple curves in PickPath sweep creation.
MEP Calculations
A second question was raised on another rather specialised calculation topic:
Question: Can you point to any documents describing the Revit MEP calculation methodology?
Saikat Bhattacharya published an article taking a look at the Revit API Hub class, never before mentioned here.
It lives in the Autodesk.Revit.DB.Structure namespace and is derived from the base Element class.
It represents a connection between two or more Revit Elements, avoiding the need to connect them with each other directly.
They each refer to the hub, which manages all the connectivity information for them.
Its interface is pretty minimal; the only members not inherited from Element appear to be:
GetHubConnectorManager to retrieve the hub ConnectorManager.
HasOrigin to indicate whether the hub has a specific location in 3D space.
GetOrigin to retrieve the hub 3D position, if it has one.
I shaved again, after a couple of weeks of abstinence.
Here is a last parting shot before leaving the unshaved masses, and afterwards:
Note the prow of a boat sticking out of the wall in the left-hand picture... special bonus points for anyone who can tell where this photo was taken :-)
AutoCAD and Revit are
very different animals,
as we already pointed out a couple of times in the past.
Exaggerating just a little, you might say that Revit manages a BIM and ensures its consistency at all times, so there are no exceptions to anything ever, meaning nothing can be overridden.
Moreover, almost all Revit geometry is parametrically driven, so there is no way to tweak the model geometry either, except of course by changing the official parameter values.
This is obviously a challenge to developers with existing AutoCAD applications.
Here is another example of some typical considerations of an application developer with an existing full-blown AutoCAD application, wondering whether there is any way to start supporting Revit as well:
Question: We have customers using our software and asking if we could port our AutoCAD.NET application to Revit.
In our current implementation, we override line functionality to display special walls.
We need to modify subcomponents individually, manipulate grip points, split them, add more components, etc.
Intersections between walls affect neighbouring walls and their subcomponents.
We have therefore currently overridden grip points, snap points, graphics, properties, a custom ribbon is regenerated then an object is selected and so on.
Is it possible to implement something similar with Revit?
I guess we probably need to customize the functionality of some wall type?
What is best place to start if this is possible?
Answer: I am afraid that the answer is definitely not that you can simply port your existing application.
The Revit API is completely different from the AutoCAD one.
More than this, the entire underlying principles, workflows and paradigms are utterly different and almost beyond comparison.
In fact, one of the biggest stumbling blocks to learning the Revit API is being caught up in preconceptions coming from
AutoCAD or other non-BIM APIs.
In Revit, almost all the functionality you list above cannot be overridden, because Revit manages a building information model, BIM, and ensures that it remains consistent at all times.
The proper approach in Revit would probably be to define an appropriate family and family types.
I am not at all a product expert, though, so I cannot advise you at all about how best to implement your specific model in Revit.
You need to consult with application engineers and product experts about that, and above all understand the existing Revit functionality in depth before you even start considering creating an add-in application, which may easily end up reproducing or even fighting against the recommended Revit paradigms and workflows.
What I can tell you with a hundred percent certainty is that your existing AutoCAD application cannot simply be ported to Revit, and that your best approach will almost certainly require reconsidering the whole implementation from scratch.
What I can also say with conviction is that it will probably be well worth your while in the long run to start learning and understanding the Revit BIM concepts and researching how a useful add-in for that domain might look, to enable you to start work on a BIM-compatible implementation, in which you will certainly be able to reuse a lot of your existing know-how and continue supporting your customers as they gradually migrate to BIM usage.
If you have customers knowledgeable in Revit who are interested in cooperating with you to drive such a project, I would take that opportunity with great gratitude if I were you.
Best place to start? Learn the product functionality, and do your best to forget AutoCAD for a moment.
Response: Thank you for a great answer!
I think at this point we have to stick with AutoCAD due the limitations or the concept of Revit.
The main interest of our customer in using Revit was using IFC for integration with thermal calculation applications.
Of course Revit would also save us having to implement a number of additional objects like openings, roofs etc. that are not available in AutoCAD.
I believe we will continue implementing the functionality on top of AutoCAD but still continue to research Revit at the same time.
Perhaps at some point we could implement some kind of separate editor for our model, maybe using a WPF dialogue user interface that plays with normal walls in Revit.
If I read the API documentation correctly, we can still store additional info to wall objects.
Answer: Your approach sounds very feasible and sensible to me.
Implementing additional functionality in a separate WPF dialogue interacting with Revit would work fine.
Please be aware that you can also maintain your existing AutoCAD functionality and access that in the background, invisibly to the user, from your Revit add-in.
For example, you can read the relevant data from the Revit model, process it in the background using your existing application, and display useful results in your own WPF dialogue in a way that supports an efficient Revit workflow. That might enable you to achieve a near perfect solution with little effort.
And yes, absolutely, you can store any additional data you like on walls and any other elements using the
extensible storage functionality.
Importing SAT into Revit
People now and then ask about programmatically importing a SAT file into Revit.
You can export a SAT file using the API and import an SAT file from the user interface, simply by going through Insert > Import > Import CAD > ACIS SAT Files.
However, the Revit API Import methods only take options for DGN, DWG/DXF, gbXML, and images.
One obvious workaround is to implement an intermediate step to generate a DWG file from the SAT one.
This can be achieved either using RealDwg or AutoCAD itself, if it happens to be installed on the same machine.
In either case, the description of implementing
ACISIN and ACISOUT in .NET might
come in handy to achieve this.
ElementTransformUtils and Family Instance Creation May Force Regeneration
If you are facing performance problems generating or manipulation largish models, please be aware that you need to watch out a bit with the ElementTransformUtils class.
Some of its methods contain built-in regeneration at the end, which is executed regardless of your other regeneration actions.
Family instance creation can also force regeneration at times.
As always, some research and lots of benchmarking is advisable.
AU Registration Savings End October 14
The end is near!
Oct 14 is the last chance to save $500 on the Autodesk University 2012 registration.
Here is some important information for point cloud engine developers, who may be confused by the SDK samples provided.
Question: We have taken the Revit point cloud engine example provided and modified it to create three cells, each loading its points from an ASCII file.
The cell boundary is then drawn as blue random points.
We noticed that the maximum number of points ever requested is 512 * 512 and decreases as you zoom out.
This number is applied to all cells, so the third and second cells disappear rather that fewer points being drawn in each cell.
This of course prompts some questions:
What sets the size of the buffer that is to be filled with points?
How does the point engine pass the buffer to each cell such that a closer cell would be requesting more points than one in the distance?
Answer: An instance of the IPointCloudAccess class will be requested by Revit when drawing the point cloud in the view.
For performance reasons,
when rendering every frame, Revit asks the engine to fetch the necessary points split into multiple batches.
The number of batches requested depends on the view: the smaller the projection of the cloud bounding box on the screen
the fewer batches Revit requests.
Revit assumes that each batch contains points uniformly distributed over the visible part of
the cloud ('visible' as defined by the filter).
Thus, the points supplied by the engine should not be geometrically distinct, e.g.
divided into multiple independent volumes, because otherwise, at distant zoom levels, Revit will only request a few batches and only part
of the cloud will be displayed.
In other words, on every frame, Revit asks the engine to fetch some number of points in a few batches.
Each batch is 256K points.
The number of batches requested depends on the frame: the smaller the projection of the cloud bounding box on the screen, e.g. when you zoom out or orbit, the fewer batches Revit requests.
Revit assumes that each batch contains points uniformly distributed over the visible part of the cloud, where 'visible' is defined by the filter.
In the sample code, the plug-in does the complete opposite: it groups points in a few cubes, 256K each, and returns one cube per requested batch.
So if Revit requests 3*256K points, it gets three cubes, but if you zoom out and Revit requests only 2*256K points, it receives only 2 cubes.
In this example Revit expects to get 2*256K points uniformly distributed over the 3 cubes.
This assumption is currently not reflected adequately in the documentation.
Sorry!
It should say something like:
The main purpose of the engine is to quickly fetch points from one point cloud file satisfying certain conditions described in the client query.
Typical request would be to get specified number of representative points within specified volume, e.g. a view frustum.
By representative we mean that it is responsibility of the engine to make sure that the points being returned give a good idea of the distribution of all points satisfying the query rather than, e.g. concentrating all in one area.
This simply shows how a contrived SDK engine sample may not provide the best example for real-world usage, not having a database of points which can be queried representatively.
Programmatically Flip Work Plane
Saikat Bhattacharya provided a very clear and succinct explanation on flipping the the work plane for a family instance, and a code sample
using the IsWorkPlaneFlipped property to
achieve this programmatically.
Here is my well-meant contribution to making this the most exciting Wednesday of your entire week.
For the first time in history, the Revit 2013 UIView class provides a possibility to convert back and forth between Revit model coordinates and Windows device screen points.
I am still a bit surprised that I have not yet received any questions about these exciting possibilities.
Well, anyway, that gives me a chance to finally dive into this exploration free of preconceptions.
Another Revit 2013 topic that I wanted to talk about for a long time is the new ReferenceIntersector class.
My previous attempts at publishing anything on it were thwarted by running into some unintended features which have meanwhile been attended to.
Now I can combine both of these topics into a single nice example, presenting my very own tooltips displaying any information I like based on the cursor location in the Revit BIM.
Does that sound cool, or what?
To give you an idea of what I am talking about, here is a
two-minute video depicting
a sample run demonstrating the final result, with the normal Revit tooltips and my own specialised ones displayed side by side.
To run the demo, I open up the basic architectural sample project, switch away from the perspective view, since add-ins are disabled there, select Level 2, turn on my own tooltips, hover around different elements to compare the Revit tooltip with my own one, and turn off my tooltips again:
Table of Contents
To display a useful tooltip, the user needs to be free to play around in the model.
Simultaneously, my add-in needs to be able to access the Windows cursor location, determine the corresponding Revit model coordinates from it, and query the Revit BIM to determine the information I would like to display.
This calls for an Idling event handler, adding a third topic to the two already mentioned above.
We thus end up with the following list, including hints on the respective approaches, solutions, and helper methods:
As you hopefully know by now, the implementation to handle the Idling event needs to be really clean and clear and is a little bit tricky.
I strongly suggest basing anything you do on the ModelessForm_IdlingEvent SDK sample.
If you implemented your Idling event handler in any other way in the past, it might be a good idea revisiting it now and comparing notes with that sample.
I based my WinTooltip external application on it, anyway.
It handles all the modeless form interaction in the external application implementation.
In my case, the modeless form is either a Visual Studio designer generated JtTooltipForm or a hand-built JtTooltipForm2.
The external application is instantiated as a singleton instance, and provides public access to that instance so that external commands can access it and use its functionality to show and hide the modeless form.
It implements an internal CloseForm method, public methods ShowForm and HideForm, called by the external commands, and the standard interface methods OnStartup and OnShutdown, like this:
///<summary>/// Singleton external application class instance.///</summary>internalstaticApp _app = null;
///<summary>/// Provide access to singleton class instance.///</summary>publicstaticApp Instance
{
get { return _app; }
}
///<summary>/// The tooltip form to display.///</summary>internalstaticJtTooltipForm2 _form = null;
///<summary>/// Dispose and null out form./// Return true if it was previously not disposed.///</summary>staticbool CloseForm()
{
bool rc = _form != null;
if( rc )
{
if( !_form.IsDisposed )
{
_form.Dispose();
}
_form = null;
}
return rc;
}
///<summary>/// Create and show the form, /// unless it already exists.///</summary>///<remarks>/// The external command invokes /// this on end-user request.///</remarks>publicvoid ShowForm( UIApplication uiapp )
{
// If we do not have a form yet, create and show itif( _form == null || _form.IsDisposed )
{
// Instantiate JtTooltipForm to use // the designer generated form.
_form = newJtTooltipForm2();
_form.Show();
// If we have a form, we need Idling too
uiapp.Idling += IdlingHandler;
}
}
///<summary>/// Hide the form.///</summary>///<remarks>/// The external command invokes /// this on end-user request.///</remarks>publicvoid HideForm( UIApplication uiapp )
{
if( CloseForm() )
{
// If the form was showing, we had subscribed
uiapp.Idling -= IdlingHandler;
}
}
publicResult OnStartup( UIControlledApplication a )
{
_app = this;
_form = null;
returnResult.Succeeded;
}
publicResult OnShutdown( UIControlledApplication a )
{
if( CloseForm() )
{
a.Idling -= IdlingHandler;
}
returnResult.Succeeded;
}
Helper Methods
Apart from the top-level management, the Idling event handler does all the rest of the work.
Before we get to the event handler itself, I'll present the three helper methods it uses:
GetActiveUiView: retrieve the active UIView.
GetView3d: retrieve the 3D view named "{3D}".
ElementDescription: return a descriptive text for a given Revit element.
We can get the active document view directly, but need some additional coding to determine the associated UIView, for example like this:
The ReferenceIntersector constructor requires a 3D view argument in which the ray tracing takes place.
In this sample, I assume that a suitable 3D view named "{3D}" is available and retrieve that using one single line of filtered element and LINQ driven code:
///<summary>/// Return the 3D view named "{3D}".///</summary>View3D GetView3d( Document doc )
{
returnnewFilteredElementCollector( doc )
.OfClass( typeof( View3D ) )
.Cast<View3D>()
.FirstOrDefault<View3D>(
v => v.Name.Equals( "{3D}" ) );
}
You can obviously modify this in any way you like and set up a suitable 3D view to suit your specific view setting, sectioning and filtering needs.
The location dependent BIM information displayed in the tooltip can be determined completely freely.
In my case, I simply detect what element the cursor is hovering over and use the ElementDescription method to put together a descriptive text for it:
///<summary>/// Return a string describing the given element:/// .NET type name,/// category name,/// family and symbol name for a family instance,/// element id and element name.///</summary>staticstring ElementDescription(
Element e )
{
if( null == e )
{
return"<null>";
}
// For a wall, the element name equals the// wall type name, which is equivalent to the// family name ...FamilyInstance fi = e asFamilyInstance;
string typeName = e.GetType().Name;
string categoryName = ( null == e.Category )
? string.Empty
: e.Category.Name + " ";
string familyName = ( null == fi )
? string.Empty
: fi.Symbol.Family.Name + " ";
string symbolName = ( null == fi
|| e.Name.Equals( fi.Symbol.Name ) )
? string.Empty
: fi.Symbol.Name + " ";
returnstring.Format( "{0} {1}{2}{3}<{4} {5}>",
typeName, categoryName, familyName,
symbolName, e.Id.IntegerValue, e.Name );
}
This is somewhat similar to and yet does not exactly match the information displayed by the standard Revit tooltip and other parts of the user interface, as you might have noticed in the
video recording.
Idling Event Handler
The Idling event handler implements the following steps, which we first discuss one by one before presenting the entire code in context:
Converting from the cursor position retrieved in Windows device coordinates to Revit model coordinates is possible by calculating the relative position in the UIView.
Here is an illustration assuming a screen resolution of 1600 x 1200 with a Revit view located at the rectangle 800,150 to 1500,650, displaying a plan view of part of the Revit model, with model coordinates ranging from 10,40,0 to 220,120,0.
The current cursor location is represented by the green spot:
The Windows device coordinate origin 0,0 is at the upper left, and Y values increase downwards, whereas the Revit model coordinate Y values increase upwards.
You can easily query the cursor position in device coordinates on the Windows screen from .NET framework at any time using the System.Windows.Forms.Cursor.Position property:
Point p = System.Windows.Forms.Cursor.Position;
Determine Revit Model Coordinates
The cursor location is obviously returned in Windows device coordinates, stored in the point (or vector) variable 'p'.
The aim is to determine the location indicated by this point in Revit model coordinates, for example by determining the vector v from the lower left corner of the Revit view to that location.
We can easily determine the relative position of p between the two corners of the UIView in Windows device coordinates by subtracting the lower left hand corner and dividing by the total width and height to obtain the relative width and height location values dx and dy like this:
From the UIView, we can also determine model coordinates of the view corners.
They are stored in a and b, respectively.
Now it is easy to calculate the cursor point 'q' in model coordinates from the two relative values like this:
IList<XYZ> corners = uiview.GetZoomCorners();
XYZ a = corners[0];
XYZ b = corners[1];
XYZ v = b - a;
XYZ q = a
+ dx * v.X * XYZ.BasisX
+ dy * v.Y * XYZ.BasisY;
Query Revit BIM for Information using ReferenceIntersector Ray Casting
Once we have the model space cursor location 'q', we can calculate a point beyond the model extents (hopefully) from which one can picture the user looking into the model.
The first element intersected by a ray cast from that point in the view direction should be the one we are interested in displaying information about.
Initially, the sample run was a bit boring, because the only element encountered when looking from straight above was always the roof.
I tweaked that to make the example more interesting, not by modifying the model itself – I use the standard architectural basic sample project rac_basic_sample_project.rvt – but by adding a filter to remove all roof category elements from the reference intersector results instead.
The exact elements of interest and information to display are entirely up to you and your needs to determine, of course.
The ray casting requires a 3D view to operate in.
In this case, I simply pick the one named "{3D}" returned by the GetView3d helper method.
You can set up your own view with specific graphics properties, section cuts, and only certain elements visible, if that better suits your requirements.
Here is the code determining the view, view direction and ray origin, setting up the intersector, determining the target element, and defining the tooltip text to display in the variable 's':
View3D view3d = GetView3d( doc );
XYZ viewdir = view.ViewDirection;
XYZ origin = q + 1000 * viewdir;
// Find all elements://ReferenceIntersector ri// = new ReferenceIntersector( view3d );// Find all elements except roofs:ElementFilter f = newElementCategoryFilter(
BuiltInCategory.OST_Roofs, true );
ReferenceIntersector ri
= newReferenceIntersector( f,
FindReferenceTarget.Element, view3d );
ReferenceWithContext rc
= ri.FindNearest( origin, -viewdir );
string s = "Element not found";
if( null != rc )
{
Reference r = rc.GetReference();
Element e = doc.GetElement( r );
s = ElementDescription( e );
}
Display Tooltip and the JtTooltipForm class
I originally used the Visual Studio designer to create a simple tooltip form named JtTooltipForm for me, then tweaked that auto-generated code to create one named JtTooltipForm2 myself programmatically from scratch.
Besides the constructor setting up the form and the Label object to display the tooltip text, it also manages an offset from the cursor position at which to display itself, provides a method to set the tooltip text, and overrides the OnShown and OnVisibleChanged methods to react appropriately to position changes:
///<summary>/// A tooltip window designed to move /// around with the cursor position.///</summary>classJtTooltipForm2 : Form
{
///<summary>/// The offset from the mouse pointer /// at which to show the form.///</summary>publicPoint Offset { get; set; }
///<summary>/// Tooltip text.///</summary>Label _label;
///<summary>/// Set the tooltip text.///</summary>publicvoid SetText( string s )
{
_label.Text = s;
}
///<summary>/// Move the window to an offset of mouse pointer.///</summary>protectedoverridevoid OnShown( EventArgs e )
{
base.OnShown( e );
Location = newPoint(
MousePosition.X + Offset.X,
MousePosition.Y + Offset.Y );
}
///<summary>/// Move the window to an offset of mouse pointer.///</summary>protectedoverridevoid OnVisibleChanged(
EventArgs e )
{
base.OnVisibleChanged( e );
if( Visible )
{
Location = newPoint(
MousePosition.X + Offset.X,
MousePosition.Y + Offset.Y );
}
}
public JtTooltipForm2()
{
Size = newSize( 200, 20 );
_label = newLabel();
SuspendLayout();
_label.AutoSize = false; // the label will not change its height automatically, only width, so switch off AutoSize to wrap text
_label.CausesValidation = false;
_label.Dock = DockStyle.Fill;
_label.Location = newPoint( 0, 0 );
_label.Size = newSize( 35, 13 );
_label.Parent = this;
AutoScaleDimensions = newSizeF( 6F, 13F );
AutoScaleMode = AutoScaleMode.Font;
BackColor = SystemColors.Info;
ClientSize = newSize( 200, 12 );
Controls.Add( _label );
FormBorderStyle = FormBorderStyle.None;
Name = "JtTooltipForm";
Opacity = 0.8D;
ShowInTaskbar = false;
TopMost = true;
TransparencyKey = Color.White;
ResumeLayout( false );
PerformLayout();
Offset = newPoint( 10, 0 );
}
}
You may note that I switched off the AutoSize property.
Setting it to true enables the label to automatically expand, and that just means getting wider to fit the text length.
The label will not change its height automatically, only width.
A neat trick to wrap text before putting it into the label that I spotted and did not make use of employs a regular expression to do the job:
Controlling the text and position from the Idling event handler is a trivial two-liner:
// Move tooltip to current cursor // location and set tooltip text.
_form.Location = p + newSize( _form.Offset );
_form.SetText( s );
Entire Idling Event Handler Implementation
For better readability, here is the entire Idling event handler implementation in all its glory.
It even includes one slight improvement that is not demonstrated by the ModelessForm_IdlingEvent SDK sample implementation:
If the Revit project is closed while the Idling event is still subscribed to, the UI document may be null.
One needs to add a check for that situation as well in the Idling handler, in case the hander relies on an open document.
///<summary>/// Idling event handler.///</summary>///<remarks>/// We keep the handler very simple. First check/// if we still have the form. If not, unsubscribe /// from Idling, for we no longer need it and it /// makes Revit speedier. If the form is around, /// check if it has a request ready and process /// it accordingly.///</remarks>publicvoid IdlingHandler(
object sender,
IdlingEventArgs args )
{
UIApplication uiapp = sender asUIApplication;
UIDocument uidoc = uiapp.ActiveUIDocument;
if( null == uidoc || _form.IsDisposed )
{
uiapp.Idling -= IdlingHandler;
}
else// form still exists
{
Document doc = uidoc.Document;
View view = doc.ActiveView;
UIView uiview = GetActiveUiView( uidoc );
Rectangle rect = uiview.GetWindowRectangle();
Point p = System.Windows.Forms.Cursor.Position;
double dx = (double) ( p.X - rect.Left )
/ ( rect.Right - rect.Left );
double dy = (double) ( p.Y - rect.Bottom )
/ ( rect.Top - rect.Bottom );
IList<XYZ> corners = uiview.GetZoomCorners();
XYZ a = corners[0];
XYZ b = corners[1];
XYZ v = b - a;
XYZ q = a
+ dx * v.X * XYZ.BasisX
+ dy * v.Y * XYZ.BasisY;
// If the current view happens to be a 3D view, // we could simply use it right away. In // general we have to find a different one to // run the ReferenceIntersector in.View3D view3d = GetView3d( doc );
XYZ viewdir = view.ViewDirection;
XYZ origin = q + 1000 * viewdir;
// Find all elements://ReferenceIntersector ri// = new ReferenceIntersector( view3d );// Find all elements except roofs:ElementFilter f = newElementCategoryFilter(
BuiltInCategory.OST_Roofs, true );
ReferenceIntersector ri
= newReferenceIntersector( f,
FindReferenceTarget.Element, view3d );
ReferenceWithContext rc
= ri.FindNearest( origin, -viewdir );
string s = "Element not found";
if( null != rc )
{
Reference r = rc.GetReference();
Element e = doc.GetElement( r );
s = ElementDescription( e );
}
// Move tooltip to current cursor // location and set tooltip text.
_form.Location = p + newSize( _form.Offset );
_form.SetText( s );
}
}
Toggle Tooltip On and Off Commands
To simplify the usage and cooperation with other add-ins, I implemented two trivial read-only external commands calling the external application ShowForm and HideForm methods to toggle my personal tooltips on and off:
I hope you find this as useful as I imagine it might become, and look forward to your feedback.
By the way, I will be talking about this sample and the possibilities it demonstrates along with many other topics in my Autodesk University presentation
CP4107 on
the new Revit 2013 UI API functionality, probably making this the most exciting one of my
three AU sessions.
Here is a
current snapshot of
the entire source code, Visual Studio solution and add-in manifest of the WinTooltip sample application, together the rest of my CP4107 sample material.
The latter is based on my
DevCamp Revit 2013 UI API samples.
Here are a couple of other items of interest before I wrap up, related to the Revit API, technology and purely human interst.
URLs and local system HTML links work fine out of the box.
A text file, however, needs special processing.
One way to launch it is by using the Process.Start method in the .NET System.Diagnostics namespace.
Saikat provides the source.
DesignScript
A very exciting new technology is now publicly available on Autodesk Labs:
DesignScript:
A powerful scripting language for exploratory programming.
Managing and distinguishing between a generative description of a design (as a script) and the resulting generated model.
A language to help designers build and analyze complex geometric models that would be difficult to model with interactive techniques.
Integrated into a host geometry application (currently AutoCAD).
The designer no longer directly models the resulting design: instead she develops a script whose execution generates the model.
This enables a completely different kind of design model to be created.
The design process is also different.
An apparently minor edit to the script can have a profound effect on the generated model, enabling the exploration of a vast array of alternatives, with much less effort than manual interactive modeling.
Here is a five-minute video explanation of
Design Computation by Robert Aish for Autodesk University last year:
Kean Walmsley provides a very nice explanation and
description of the DesignScript sample he
implemented for it together with Robert in 2008 for the Design Computation Symposium and AU mainstage presentation.
Now you can try it for yourself.
Unfortunately, DesignScript is not yet available for Revit...
Falk on Schafberg
For something completely unrelated to any technology at all, here is a nice little
video of
my friend
Falk recently
when we hiked over the
Wildhauser Schafberg (sheep
mountain) together, clearly illustrating some basic and sympathetic human and
ovine commonalities :-)
Question: How can I obtain the boundary of a floor slab using the Revit API, please?
Answer: I implemented a CmdSlabBoundary external command to
determine the slab boundary back
in the dawn of time, in 2008, in one of the first posts on this blog.
It determines the boundary edges of a floor slab, including holes, and creates a set of model curves along the bottom edges of the slab to highlight them.
Since then it has just been flat ported every year from one version to the next.
To ensure that it still works, I updated it slightly and tested in Revit 2013.
All I did this time around was to change the transaction mode from automatic to manual, since
automatic transaction mode is considered obsolete nowadays.
Here is the updated implementation:
[Transaction( TransactionMode.Manual )]
classCmdSlabBoundary : IExternalCommand
{
///<summary>/// Offset the generated boundary polygon loop/// model lines downwards to separate them from/// the slab edge.///</summary>constdouble _offset = 0.1;
///<summary>/// Determine the boundary polygons of the lowest/// horizontal planar face of the given solid.///</summary>///<param name="polygons">Return polygonal boundary/// loops of lowest horizontal face, i.e. profile of/// circumference and holes</param>///<param name="solid">Input solid</param>///<returns>False if no horizontal planar face was/// found, else true</returns>staticbool GetBoundary(
List<List<XYZ>> polygons,
Solid solid )
{
PlanarFace lowest = null;
FaceArray faces = solid.Faces;
foreach( Face f in faces )
{
PlanarFace pf = f asPlanarFace;
if( null != pf && Util.IsHorizontal( pf ) )
{
if( ( null == lowest )
|| ( pf.Origin.Z < lowest.Origin.Z ) )
{
lowest = pf;
}
}
}
if( null != lowest )
{
XYZ p, q = XYZ.Zero;
bool first;
int i, n;
EdgeArrayArray loops = lowest.EdgeLoops;
foreach( EdgeArray loop in loops )
{
List<XYZ> vertices = newList<XYZ>();
first = true;
foreach( Edge e in loop )
{
IList<XYZ> points = e.Tessellate();
p = points[0];
if( !first )
{
Debug.Assert( p.IsAlmostEqualTo( q ),
"expected subsequent start point"
+ " to equal previous end point" );
}
n = points.Count;
q = points[n - 1];
for( i = 0; i < n - 1; ++i )
{
XYZ v = points[i];
v -= _offset * XYZ.BasisZ;
vertices.Add( v );
}
}
q -= _offset * XYZ.BasisZ;
Debug.Assert( q.IsAlmostEqualTo( vertices[0] ),
"expected last end point to equal"
+ " first start point" );
polygons.Add( vertices );
}
}
returnnull != lowest;
}
///<summary>/// Return all floor slab boundary loop polygons/// for the given floors, offset downwards from the/// bottom floor faces by a certain amount.///</summary>staticpublicList<List<XYZ>> GetFloorBoundaryPolygons(
List<Element> floors,
Options opt )
{
List<List<XYZ>> polygons = newList<List<XYZ>>();
foreach( Floor floor in floors )
{
GeometryElement geo = floor.get_Geometry( opt );
//GeometryObjectArray objects = geo.Objects; // 2012//foreach( GeometryObject obj in objects ) // 2012foreach( GeometryObject obj in geo ) // 2013
{
Solid solid = obj asSolid;
if( solid != null )
{
GetBoundary( polygons, solid );
}
}
}
return polygons;
}
publicResult Execute(
ExternalCommandData commandData,
refstring message,
ElementSet elements )
{
UIApplication app = commandData.Application;
UIDocument uidoc = app.ActiveUIDocument;
Document doc = uidoc.Document;
// retrieve selected floors, or all floors, if nothing is selected:List<Element> floors = newList<Element>();
if( !Util.GetSelectedElementsOrAll(
floors, uidoc, typeof( Floor ) ) )
{
Selection sel = uidoc.Selection;
message = ( 0 < sel.Elements.Size )
? "Please select some floor elements."
: "No floor elements found.";
returnResult.Failed;
}
Options opt = app.Application.Create.NewGeometryOptions();
List<List<XYZ>> polygons
= GetFloorBoundaryPolygons( floors, opt );
int n = polygons.Count;
Debug.Print(
"{0} boundary loop{1} found.",
n, Util.PluralSuffix( n ) );
Creator creator = newCreator( doc );
using( Transaction t = newTransaction( doc ) )
{
t.Start( "Draw Slab Boundaries" );
creator.DrawPolygons( polygons );
t.Commit();
}
returnResult.Succeeded;
}
}
I ran this command on a minimal sample model containing just one floor slab with a couple of holes in it.
That produces the following result, in which I modified the model lines graphics display to distinguish them better – don't know why the model ellipse is not picking up the dotted line as well as the straight segments – maybe the tessellation is chopping it up too finely:
Here is
version 2013.0.99.4 of
The Building Coder samples including the updated CmdSlabBoundary external command.
Closing Revit by Posting WM_CLOSE to its Main Window Handle
Before closing this post, one last Revit API related note on closing Revit itself...
Shutting down and exiting the Revit session is not supported by the official Revit API.
I discussed
closing the active document and
why not to do so.
The same caveats obviosuly apply to shutting down the Revit session itself.
Still, Augusto Goncalves now presents a neat little solution to obtain the Revit main window handle and
close down Revit by
using SendMessage to post a WM_CLOSE request to it.
Using that, you could set up an ElementIntersectsSolidFilter for a filtered element collector to retrieve all foundation elements touching or intersecting that face, as described for
retrieving all touching beams.
You would obviously want to apply as many quick filters as possible before taking recourse to the slow geometric filtering, e.g. check for level, category, type, etc. first.
Another way to determine adjacency of Revit BIM elements is to make use of the ray casting functionality provided by the FindReferencesByDirection method, FindReferencesWithContextByDirection method and the ReferenceIntersector class, e.g. cast a ray downwards from the wall and detect the first foundation hit by it.
Last but not least, another way to determine relationships between associated objects such as a wall and its footing is to temporarily delete the host object, causing the dependent objects to be deleted as well.
The deleted element ids can be determined, and the whole operation undone to leave the model unchanged.
I cannot tell off-hand which of these methods is most useful and efficient in your specific case, although I would be very interested in seeing the results of some comparisons and benchmarks evaluating this.
Anyway, I note that The Building Coder sample command CmdWallFooting implementing the latter approach is pretty much out of date, having been simply flat ported across several Revit API releases in the past few years.
I therefore updated it for and tested it with Revit 2013, and can now confirm that it still works.
Here is a neat and simple solution using the DialogBoxShowing event to detach and discard worksets by Erik Eriksson of
White Arkitketer AB,
who already provided a useful workaround to
synchronize with central using
SendKeys in combination with the Idling and DocumentSynchronizedWithCentral events.
Here is the issue:
Question: We have a need to detach project files without worksets.
For instance, it would be great to just have an option to detach and discard worksets.
Why do we need this?
This would be useful for our in-house tools to diagnose project files.
For this to work, we need to have the project files without worksets, because we delete different types of elements and check the file size before and afterwards.
To determine a correct file size, we need to compact the files after every operation.
This cannot be done programmatically, but, for non-workshared files, it is performed automatically each time the project file is saved.
Currently, we execute the detach and discard operation manually.
However, with big projects containing many files, it would be nice to do less of these things manually.
Answer: The API currently does not provide this functionality directly.
However, the Revit 2013 API added some new document handling and worksharing changes which may help.
Here is the description from the What's New section in the Revit API help file RevitAPI.chm:
Worksharing properties
The information required to identify a workshared file on the central or local locations has changed due to changes to RevitServer.
As a result, the members
provide read access to the worksharing central GUID of the given server-based model.
This is applicable only to workshared models saved in Revit 2013 or later.
New overloads for Application.OpenDocumentFile and UIApplication.OpenAndActivateDocument
The new overloads support parameters OpenOptions and OpenOptionsForUI, respectively, to specify how a Revit document should be opened.
Both options classes currently offer the ability to detach the opened document from central if applicable.
The OpenOptions.DetachFromCentralOption property can be set to the default DoNotDetach or to DetachAndPreserve.
The OpenOptionsForUI.DetachFromCentralOption property can be set to the default DoNotDetach, DetachAndPreserveWorksets or DetachAndPrompt.
In other words, the OpenOptions argument that you pass in to the OpenDocumentFile and OpenAndActivateDocument methods can now use the DetachFromCentral option to specify whether or not a workset-enabled document is detached from its central document.
Response: The new API functionality still does not provide the option of discarding the worksets.
I want the file to be a normal project file again.
OpenOptions holds the same options as OpenOptionsForUI (besides DetachAndPrompt).
However, maybe I can catch that dialogue programmatically using the DialogBoxShowing event and override the result...
The full implementation of this method actually includes a few more statements taking care of other dialogue boxes, which I omitted here.
I checked the IsWorkshared property after opening the file, and it was set to false.
This indicates that it is no longer workshared, which means that the operation to discard the worksets succeeded.
When you discard worksets, the workshared central file goes back to being a normal non-workshared project file, just like what you obtain when starting from a normal project template.
One thing I do wonder about is whether CommandLink2 is always below CommandLink1 and above CommandLink3?
Answer: It would appear so, since the description of the TaskDialog.AddCommandLink method clearly states that "CommandLinks will always be shown in the dialog in the order of their ids".
Also, as always, keep in mind that there is no guarantee that this will continue working in future releases.
The order of these options can change at any time, and the required functionality might even be added to the official API one of these days.
Congratulations on solving this, Erik, and thank you for sharing it and providing this helpful explanation!
Setting IndependentTag Label to Pick Up a Parameter Value
I'll wrap up with a quick pointer to another AEC DevBlog article by Saikat Bhattacharya on
defining the label displayed by an IndependentTag.
Unfortunately this does not seem to be possible programmatically.
It can however easily be achieved using an appropriate template or tag family configuration.
Creating and populating a ribbon panel is not a very complicated matter, although it normally does require instantiating and manipulating quite a number of different cooperating class instances.
Now Victor Chekalin, or Виктор Чекалин, presents a much simpler solution for this using dedicated wrapper classes.
In his own words:
Create Buttons without Effort
As you know, you can create your own buttons or even your own tab in the Revit ribbon to perform a command.
For me, creating button always wasn't easy.
Especially I didn't like to pass the assembly location and name of the external Command class to perform when I click the button.
Also, the standard API requires me to create an ImageSource for each button image while I have an image stored in the assembly resources.
To avoid these troubles and ease my life I decided to create a utility which helps me to create my Revit ribbon buttons.
The utility I created is really useful for me and I want to share it with everybody.
Here are the main features:
Fluent interface:
You can create all your buttons in one single line of code.
Command name as generic parameter:
You don't need to write the command name as text and set the assembly location.
Images from resource:
You can easily use images from resources.
At first I set the tab, where I want to create my buttons.
I can specify my own tab or one of the system tabs, e.g. Autodesk.Revit.UI.Tab.AddIns.
If the tab doesn't exist, it will be created.
So, you can easily use a tab from a different add-in.
You specify a panel to group the buttons on the tab.
Next, the most important part, create the buttons.
I won't describe how to create the buttons.
The code is self-explanatory.
My utility currently supports push button, push button as stacked item and separator.
A common question when analysing a BIM is determining whether any of its elements intersect.
This was actually surprisingly difficult to answer programmatically in Revit a few releases back, as some of the interesting uses of the FindReferencesByDirection method show, for example the FindColumns SDK sample.
Later, that method was enhanced to lead to FindReferencesWithContextByDirection.
The FindColumns sample searches for intersections between walls and columns by shooting a ray along the wall centre line, raised above the bottom level by a foot or so, and detecting any columns hit by it.
One obvious problem with this approach is its inaccuracy – any column just partially intersecting the wall may easily be missed by the ray.
A few years ago, that was the best an add-in developer could hope for, and things looked even worse before the advent of these methods.
So what is the situation today?
In short:
Question: How can I programmatically detect whether two Revit elements intersect?
Answer: You can now perform a Boolean operation on Revit element solids and determine whether they intersect using the BooleanOperationsUtils class ExecuteBooleanOperation method.
Please note that the resulting intersection solid is transient, in-memory only.
It can be used for calculation and short-term visualisation using the analysis visualisation framework AVF, but it cannot be added to the Revit database or made persistent.
This class was introduced in the Revit 2012 API, and the
associated webcast showed
it being used impressively in a much improved version of the FindColumns SDK sample.
This version determines the exact intersection solids between all columns and walls instead of roughly approximating the intersection detection by casting a single ray, and displays them to the user through AVF.
The Boolean intersection demo is described in detail and the source can be downloaded from the discussion of a simpler derived sample
using AVF to highlight rooms.
Another example of making use of this class is given by the GeometryCreation_BooleanOperation SDK sample.
If you are interested in a succinct summary of all the key developments in the AEC technology industry in the past couple of months, here is a potantial cancidate: the
AEC Technology Updates, Fall 2012,
including some snippets of Autodesk related news such as Revit LT and the Vela acquisition.