One of the RvtVa3c implementation aspects that I mentioned was the fact that we ran into some problems using the standard .NET Microsoft System.Runtime.Serialization.Json.DataContractJsonSerializer class and chose to replace it with the more reliable Json.NET component instead.
Serialisation is required by our early decision to define and generate the three.js JSON file format by representing all the required objects, their properties and relationships by a set of C# classes.
We instantiate and populate these classes in our custom exporter context implementation and serialise them out to JSON to generate the output file used to represent the BIM for the va3c viewer.
A little bit too late, Ben pointed out that we could have saved ourselves the whole effort of implementing and populating a C# class hierarchy by using dynamic JSON and the System.Dynamic.ExpandoObject instead, as he does in his GHva3c Grasshopper va3c exporter.
Anyway, we continued down the path of C# class definition and serialisation to generate the JSON output file starting from the root three.js scene object.
The original code using a DataContractJsonSerializer to serialise it and thus generate the JSON output looked like this:
using( FileStream stream = File.OpenWrite( filename ) ) { DataContractJsonSerializer serialiser = new DataContractJsonSerializer( typeof( Va3cScene ) ); serialiser.WriteObject( stream, _scene ); }
This became too buggy at a certain point. For instance, when writing to the same file repeatedly, the end of the updated file still contained data from the previous version, obviously corrupting the entire structure.
It can be easily replaced by the following code using Json.NET:
JsonSerializerSettings settings = new JsonSerializerSettings(); settings.NullValueHandling = NullValueHandling.Ignore; File.WriteAllText( _filename, JsonConvert.SerializeObject( _scene, Formatting.Indented, settings ) );
On the first run, though, this caused our external command to fail with an error reporting that the Newtonsoft.Json.dll assembly could not be loaded.
This is due to .NET restrictions requiring proper .NET application behaviour.
As mentioned, one way to resolve this issue is to install the entire application in a sub-folder of the Revit.exe directory, which is not always feasible.
If it lives elsewhere, the restriction can be somewhat circumvented using a .NET assembly resolver.
I mentioned this beast in the past, e.g. discussing the RvtUnit project for Revit Add-in unit testing and using REX without the REX framework.
Matt sets up such resolution handlers on a regular basis, so he quickly typed the following code from scratch by heart to implement and register it in the main RvtVa3c exporter method ExportView3D:
/// <summary> /// Custom assembly resolver to find our support /// DLL without being forced to place our entire /// application in a subfolder of the Revit.exe /// directory. /// </summary> System.Reflection.Assembly CurrentDomain_AssemblyResolve( object sender, ResolveEventArgs args ) { if( args.Name.Contains( "Newtonsoft" ) ) { string filename = Path.GetDirectoryName( System.Reflection.Assembly .GetExecutingAssembly().Location ); filename = Path.Combine( filename, "Newtonsoft.Json.dll" ); if( File.Exists( filename ) ) { return System.Reflection.Assembly .LoadFrom( filename ); } } return null; } /// <summary> /// Export a given 3D view to JSON using /// our custom exporter context. /// </summary> void ExportView3D( View3D view3d, string filename ) { AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; Document doc = view3d.Document; Va3cExportContext context = new Va3cExportContext( doc, filename ); CustomExporter exporter = new CustomExporter( doc, context ); // Note: Excluding faces just suppresses the // OnFaceBegin calls, not the actual processing // of face tessellation. Meshes of the faces // will still be received by the context. exporter.IncludeFaces = false; exporter.ShouldStopOnError = false; exporter.Export( view3d ); }
For completeness sake, here is the rest of the external command mainline implementation, prompting for interactive use of the output filename and folder, stored for reuse in subsequent runs in the same session:
#region SelectFile /// <summary> /// Store the last user selected output folder /// in the current editing session. /// </summary> static string _output_folder_path = null; /// <summary> /// Return true is user selects and confirms /// output file name and folder. /// </summary> static bool SelectFile( ref string folder_path, ref string filename ) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Title = "JSelect SON Output File"; dlg.Filter = "JSON files|*.js"; if( null != folder_path && 0 < folder_path.Length ) { dlg.InitialDirectory = folder_path; } dlg.FileName = filename; bool rc = DialogResult.OK == dlg.ShowDialog(); if( rc ) { filename = Path.GetFileName( dlg.FileName ); folder_path = Path.GetDirectoryName( filename ); } return rc; } #endregion // SelectFile public Result Execute( ExternalCommandData commandData, ref string message, ElementSet elements ) { UIApplication uiapp = commandData.Application; UIDocument uidoc = uiapp.ActiveUIDocument; Application app = uiapp.Application; Document doc = uidoc.Document; if( doc.ActiveView is View3D ) { string filename = doc.PathName; if( 0 == filename.Length ) { filename = doc.Title; } if( null == _output_folder_path ) { _output_folder_path = Path.GetDirectoryName( filename ); } filename = Path.GetFileName( filename ) + ".js"; if( SelectFile( ref _output_folder_path, ref filename ) ) { filename = Path.Combine( _output_folder_path, filename ); ExportView3D( doc.ActiveView as View3D, filename ); return Result.Succeeded; } return Result.Cancelled; } else { TaskDialog.Show( "va3c", "You must be in 3D view to export." ); } return Result.Failed; }
For the custom exporter code populating the objects representing the three.js scene hierarchy and the entire add-in implementation, please refer to the RvtVa3c GitHub repository and the Va3cExportContext implementation.
External Command Lifecycle
Matt added an additional consideration:
Question: I realized one thing that I didn't do here that I sometimes address – but it would make the blog post code a bit more complicated.
Do you know what the lifecycle is of a Revit command? Does it get disposed afterwards, or is it re-used, or left alive? I have a vague feeling that it doesn't “go away” right away.
As such, because we've registered this event on the current appDomain, you might still be getting callbacks into this event after you leave this app and run the next command (which can be confusing).
To address this, you would define the callback event handler first, add it, and then remove the handler at the end when the command ended.
So – it's up to you if that's a complexity that should be addressed or not? Because this example is only loading the Newtonsoft JSON specifically, it's pretty harmless.
Answer: A new external command implementation class is instantiated each time you launch the command, as I painfully discovered searching for a solution to the RoomEditorApp idling issues: "each new external command invocation generates a new different class instance."
For this reason, I was unable to unsubscribe from an external event that was subscribed to from an external command, and changed the subscriber to be the external application instead of the external command.
Hi Jeremy,
I have met a problem when creating a sweep in Revit that the size of sweep object created by codes can not be stretched,my codes is:
Sweep sweep1 =m_familyDocument.FamilyCreate.NewSweep(true, curves, sketchPlane, profile, 0, ProfilePlaneLocation.Start);
So I think add a dimension will address the question.Before adding a dimension I should create corresponding referenceplan in front(south)elevation,here comes the problem: How can I create referenceplane in front(south)elevation??I can find front(south)elevation with codes
http://spiderinnet.typepad.com/blog/2012/10/revit-api-coding-good-practice-filter-elevations-with-class-type-and-directions-instead-of-names.html
then I use the following codes add referenceplane
void addReferencePlanes()
{
//
double PH = mmToFeet(800.0);
double WW = mmToFeet(400.0);
double CW = mmToFeet(600.0);
double BW = mmToFeet(1600);
double BT = mmToFeet(400);
//
//
IEnumerable eastElevations = FindElevations.Find(_doc, FindElevations.Direction.South, false);
View viewplan = eastElevations.First() as View;
if(viewplan!=null)
TaskDialog.Show("eastElevations", viewplan.Name.ToString());
XYZ source = new XYZ(-4.3, 4.7, 1.7);
XYZ target = new XYZ(4.3, 4.7, 1.7);
ReferencePlane refFront = _doc.FamilyCreate.NewReferencePlane(source, target, XYZ.BasisZ, viewplan);
refFront.Name = "Center";
if(refFront==null)
TaskDialog.Show("Wrong", "ReferencePlane is null");
// get the bubble and free ends from front ref plane and offset by td.
//
XYZ p1 = refFront.BubbleEnd;
TaskDialog.Show("refFront.BubbleEnd", p1.ToString());
XYZ p2 = refFront.FreeEnd;
TaskDialog.Show("refFront.FreeEnd", p2.ToString());
XYZ HDownBubbleEnd = new XYZ(p1.X, p1.Y -BT, p1.Z);
XYZ HDownFreeEnd = new XYZ(p2.X, p2.Y -BT, p2.Z);
//
ReferencePlane refPlane = _doc.FamilyCreate.NewReferencePlane(HDownBubbleEnd, HDownFreeEnd, XYZ.BasisZ, viewplan);
refPlane.Name = "OffsetHDown";
HDownBubbleEnd = new XYZ(p1.X, p1.Y +PH, p1.Z);
HDownFreeEnd = new XYZ(p2.X, p2.Y +PH, p2.Z);
refPlane = _doc.FamilyCreate.NewReferencePlane(HDownBubbleEnd, HDownFreeEnd, XYZ.BasisZ, viewplan);
refPlane.Name = "OffsetHUp";
//
//
ReferencePlane refLeft = findElement(typeof(ReferencePlane), "中心(左/右)") as ReferencePlane;
//
p1 = refLeft.BubbleEnd;
//TaskDialog.Show("refLeft.BubbleEnd", p1.ToString());
p2 = refLeft.FreeEnd;
//TaskDialog.Show("refLeft.BubbleEnd", p2.ToString());
XYZ VLeftBubbleEnd = new XYZ(p1.X -WW-CW, p1.Y, p1.Z);
XYZ VLeftFreeEnd = new XYZ(p2.X - WW - CW, p2.Y, p2.Z);
//
refPlane = _doc.FamilyCreate.NewReferencePlane(VLeftBubbleEnd, VLeftFreeEnd, XYZ.BasisZ, viewplan);
refPlane.Name = "OffsetVLeft2";
VLeftBubbleEnd = new XYZ(p1.X - WW, p1.Y, p1.Z);
VLeftFreeEnd = new XYZ(p2.X - WW, p2.Y, p2.Z);
refPlane = _doc.FamilyCreate.NewReferencePlane(VLeftBubbleEnd, VLeftFreeEnd, XYZ.BasisZ, viewplan);
refPlane.Name = "OffsetVLeft1";
VLeftBubbleEnd = new XYZ(p1.X +BW-CW- WW, p1.Y, p1.Z);
VLeftFreeEnd = new XYZ(p2.X +BW-CW- WW, p2.Y, p2.Z);
refPlane = _doc.FamilyCreate.NewReferencePlane(VLeftBubbleEnd, VLeftFreeEnd, XYZ.BasisZ, viewplan);
refPlane.Name = "OffsetVRight";
}
Only in FloorView can see the adding six referenceplanes,in front(sourth) Elevation View can see the OffsetVLeft2,OffsetVLeft1,OffsetVRight referenceplane,how can I address this problem,I just want to create a sweep with dimensions consulting RvtCmd_FamilyCreateColumnLShape.cs(http://usa.autodesk.com/adsk/servlet/index?siteID=123112&id=2484975)Revit 2014 API Labs.
Posted by: Ariel | May 21, 2014 at 22:24
Dear Ariel,
The Family API labs demonstrate retrieving references and adding constraints to all six faces of a simple rectangular column, and even more to the L shaped column.
The most recent version was just uploaded to GitHub:
https://github.com/ADN-DevTech/RevitTrainingMaterial
The references are obtained and constraints added in the method addAlignments in the module 2_ColumnLshape.cs:
https://github.com/ADN-DevTech/RevitTrainingMaterial/blob/master/Labs/3_Revit_Family_API/SourceCS/2_ColumnLshape.cs
Does that achieve what you are seeking?
Cheers, Jeremy.
Posted by: Jeremy Tammik | May 28, 2014 at 11:19
Dear Jeremy,
Thank you for your replying,during coding I find sweep is quite different from extrusion.I just imitating the document you recommend before to building my own sweep.Now I can creating a sweep in revit,and adding alignments,adding parameters,adding dimensions,all these are realized,however,when I want to change the parameter,the model does not follow the change and the result is mistake. I add reference planes in front elevation with coding,but in the front elevation of revit,all the reference planes I created are grey and can not be edited,So I think this is the key of the problem,but I can not address it.
Posted by: Ariel | May 28, 2014 at 23:11
When creating a sweep in revit we should first draw a sweeppath,and then choose to switch to front or back elevation.In my code I just add reference plans in front elevation and leaving the choosing process.Is this the major problem??
Cheers,Ariel
Posted by: Ariel | May 28, 2014 at 23:19
How do you add vA3C in as a plugin for Revit? I downloaded the files from: https://va3c.github.io/
Then added them to my plugins directory, no export button shows up. What am I doing wrong?
Posted by: Matt Rizzo | July 30, 2014 at 15:01
Dear Matt,
All Revit add-ins are installed the same way: place the add-in manifest in a specific folder that Revit looks at and loads.
The add-in manifest with the filename extension *.addin specifies the location of the .NET assembly DLL.
For a simple add-in, I specify no path, just the DLL name, and place it in the same folder.
If you simply compile the add-in in Visual Studio, it will copy the add-in manifest and DLL to the right location for you automatically, due to special post-build events that are set up to do that.
For more info, look at the Revit API getting started material:
http://thebuildingcoder.typepad.com/blog/about-the-author.html#2
Cheers, Jeremy.
Posted by: Jeremy Tammik | July 30, 2014 at 18:22