Spheres are not as common as planar faceted objects in the architectural domain. In spite of that, this week has been the week of the spheres, after looking at how to generate spherical solids, display them using AVF, and use them for geometrical proximity filtering.
We'll round this off now by displaying lots of spheres. A convenient and interesting way to generate a large number of them is to use Kean Walmsley's Apollonian gasket and sphere packing web service to fill a sphere with solid spheres (project overview).
To give you a quick first impression of what it is all about, here are some views of different levels of Apollonian sphere packing using Revit transient solids generated by the GeometryCreationUtilities class and its CreateRevolvedGeometry method and displayed in the Revit graphics area using the Analysis Visualization Framework AVF. Apollonian packing with three levels:
Apollonian packing with five levels:
Apollonian packing with seven levels:
Retrieval and display of this data in Revit requires the following steps and functionality, listed here in the order of their implementation in the source code:
I'll simply present these sections of code with some comments on each.
The string formatting and parsing is required both to obtain input data from the user and to present some timing and statistical results at the end.
The input to the sphere packing algorithm web service consists of an outer sphere radius and the number of steps to execute, which varies from 2 upwards, where 10 is a pretty high number.
I implemented a little .NET form to request this input from the user together with a centre point defining the location in the Revit model to display the packing:
The following trivial methods are used to parse this input data and format the timing results:
/// <summary> /// Return a string for a real number /// formatted to two decimal places. /// </summary> public static string RealString( double a ) { return a.ToString( "0.##" ); } /// <summary> /// Return an integer parsed /// from the given string. /// </summary> static int StringToInt( string s ) { return int.Parse( s ); } /// <summary> /// Return a real number parsed /// from the given string. /// </summary> static double StringToReal( string s ) { return double.Parse( s ); } /// <summary> /// Return an XYZ point or vector /// parsed from the given string. /// </summary> static XYZ StringToPoint( string s ) { s.TrimStart( new char[] { '(', ' ' } ); s.TrimEnd( new char[] { ')', ' ' } ); string[] a = s.Split( new char[] { ',', ' ' } ); return ( 3 == a.Length ) ? new XYZ( StringToReal( a[0] ), StringToReal( a[1] ), StringToReal( a[2] ) ) : null; }
All trivial stuff, and still it helps to have it nicely sorted.
The use of Kean's sphere packing web service is straightforward.
You send it an HTTP request, it returns results in JSON format, you unpack them, and Bob's your uncle.
Kean describes it in detail and presents the source code implementing it in his discussion of consuming data from a restful web service.
It was originally designed for use in AutoCAD.NET. I simply grabbed his code and reuse it completely unchanged:
static dynamic ApollonianPackingWs( double r, int numSteps, bool circles ) { string json = null; // Call our web-service synchronously (this // isn't ideal, as it blocks the UI thread) HttpWebRequest request = WebRequest.Create( //"http://localhost:64114/api/" "http://apollonian.cloudapp.net/api/" + ( circles ? "circles" : "spheres" ) + "/" + r.ToString() + "/" + numSteps.ToString() ) as HttpWebRequest; // Get the response using( HttpWebResponse response = request.GetResponse() as HttpWebResponse ) { // Get the response stream StreamReader reader = new StreamReader( response.GetResponseStream() ); // Extract our JSON results json = reader.ReadToEnd(); } if( !String.IsNullOrEmpty( json ) ) { // Use our dynamic JSON converter to // populate/return our list of results var serializer = new JavaScriptSerializer(); serializer.RegisterConverters( new[] { new DynamicJsonConverter() } ); // We need to make sure we have enough space // for our JSON, as the default limit may well // get exceeded serializer.MaxJsonLength = 50000000; return serializer.Deserialize( json, typeof( List<object> ) ); } return null; }
The code implementing AVF functionality has also already been presented and discussed elsewhere. The last use I made of it was for the initial sphere display using AVF.
All I did here was to replace the PaintSolid method used there to handle multiple solids. I implemented this trivial helper class to associate a solid with a level in order to represent the levels in different colours in the visualisation:
class SolidAndLevel { public Solid Solid { get; set; } public int Level { get; set; } }
Here is the code to create an AVF display style, get or create a SpatialFieldManager, set up an analysis result schema, and display the spherical solids using PaintSolids, taking a list of SolidAndLevel instances as input:
void CreateAvfDisplayStyle( Document doc, View view ) { using( Transaction t = new Transaction( doc ) ) { t.Start( "Create AVF Style" ); AnalysisDisplayColoredSurfaceSettings coloredSurfaceSettings = new AnalysisDisplayColoredSurfaceSettings(); coloredSurfaceSettings.ShowGridLines = false; AnalysisDisplayColorSettings colorSettings = new AnalysisDisplayColorSettings(); AnalysisDisplayLegendSettings legendSettings = new AnalysisDisplayLegendSettings(); legendSettings.ShowLegend = false; AnalysisDisplayStyle analysisDisplayStyle = AnalysisDisplayStyle .CreateAnalysisDisplayStyle( doc, "Paint Solid", coloredSurfaceSettings, colorSettings, legendSettings ); view.AnalysisDisplayStyleId = analysisDisplayStyle.Id; t.Commit(); } } static int _schemaId = -1; void PaintSolids( Document doc, IList<SolidAndLevel> solids ) { Application app = doc.Application; View view = doc.ActiveView; if( view.AnalysisDisplayStyleId == ElementId.InvalidElementId ) { CreateAvfDisplayStyle( doc, view ); } SpatialFieldManager sfm = SpatialFieldManager.GetSpatialFieldManager( view ); if( null == sfm ) { sfm = SpatialFieldManager .CreateSpatialFieldManager( view, 1 ); } if( _schemaId != -1 ) { IList<int> results = sfm.GetRegisteredResults(); if( !results.Contains( _schemaId ) ) { _schemaId = -1; } } if( _schemaId == -1 ) { AnalysisResultSchema resultSchema = new AnalysisResultSchema( "PaintedSolids", "Description" ); _schemaId = sfm.RegisterResult( resultSchema ); } foreach( SolidAndLevel sl in solids ) { FaceArray faces = sl.Solid.Faces; Transform trf = Transform.Identity; foreach( Face face in faces ) { int idx = sfm.AddSpatialFieldPrimitive( face, trf ); IList<UV> uvPts = new List<UV>( 1 ); uvPts.Add( face.GetBoundingBox().Min ); FieldDomainPointsByUV pnts = new FieldDomainPointsByUV( uvPts ); List<double> doubleList = new List<double>( 1 ); doubleList.Add( sl.Level ); IList<ValueAtPoint> valList = new List<ValueAtPoint>( 1 ); valList.Add( new ValueAtPoint( doubleList ) ); FieldValues vals = new FieldValues( valList ); sfm.UpdateSpatialFieldPrimitive( idx, pnts, vals, _schemaId ); } } }
The spherical solid creation is absolutely unchanged from the discussion on Monday, so I can get right down to the mainline Execute method putting it all together. It
Each of the steps is timed, the spheres are counted, and the final results are presented, looking like this for three levels:
For five levels:
For seven levels:
Revit actually required some additional time on my system to complete and return from the command after these messages were displayed, and mostly that took longer than the entire command execution itself.
Here is the Execute mainline implementing these steps:
public Result Execute( ExternalCommandData commandData, ref string message, ElementSet elements ) { UIApplication uiapp = commandData.Application; UIDocument uidoc = uiapp.ActiveUIDocument; Application app = uiapp.Application; CreationApp creapp = app.Create; Document doc = uidoc.Document; if( !(doc.ActiveView is View3D) ) { message = "Please run this commahnd in a 3D view."; return Result.Failed; } XYZ centre = XYZ.Zero; double radius = 100.0; int steps = 3; using( Form1 f = new Form1() ) { if( DialogResult.OK != f.ShowDialog() ) { return Result.Cancelled; } centre = StringToPoint( f.GetCentre() ); radius = StringToReal( f.GetRadius() ); steps = StringToInt( f.GetLevel() ); } // Time the web service operation Stopwatch sw = Stopwatch.StartNew(); dynamic res = ApollonianPackingWs( radius, steps, false ); sw.Stop(); double timeWs = sw.Elapsed.TotalSeconds; // Create solids, going through our "dynamic" // list, accessing each property dynamically sw = Stopwatch.StartNew(); List<SolidAndLevel> solids = new List<SolidAndLevel>(); Dictionary<int, int> counters = new Dictionary<int, int>(); foreach( dynamic tup in res ) { double rad = System.Math.Abs( (double) tup.R ); Debug.Assert( 0 < rad, "expected positive sphere radius" ); XYZ cen = new XYZ( (double) tup.X, (double) tup.Y, (double) tup.Z ); int lev = tup.L; Solid s = CreateSphereAt( creapp, cen, rad ); solids.Add( new SolidAndLevel { Solid = s, Level = lev } ); if( !counters.ContainsKey( lev ) ) { counters[lev] = 0; } ++counters[lev]; } sw.Stop(); double timeSpheres = sw.Elapsed.TotalSeconds; // Set up AVF and paint solids sw = Stopwatch.StartNew(); PaintSolids( doc, solids ); sw.Stop(); double timeAvf = sw.Elapsed.TotalSeconds; int total = 0; string counts = string.Empty; List<int> keys = new List<int>( counters.Keys ); keys.Sort(); foreach( int key in keys ) { if( 0 < counts.Length ) { counts += ","; } int n = counters[key]; counts += n.ToString(); total += n; } string report = string.Format( "{0} levels retrieved with following sphere " + "counts: {1} = total {2}; times in seconds " + "for web service {3}, sphere creation {4} " + "and AVF {5}.", counters.Count, counts, total, RealString( timeWs ), RealString( timeSpheres ), RealString( timeAvf ) ); TaskDialog.Show( "Apollonian Packing", report ); return Result.Succeeded; }
Since Kean did all the web service implementation and JSON extraction work for me, it all boiled down to just putting together a few ready-made components. Thank you, Kean!
Please refer to Kean's project overview for all the nitty-gritty background details.
Here is Apollonian.zip containing the complete source code, Visual Studio solution and add-in manifest for this external command.
Anyway, this should provide enough spheres for this week and keep us all happy and occupied over the weekend.