Yesterday, I implemented and published a simple Node.js web server to display an SVG polygon.
In that post, you can define the polygon path manually in a text box and invoke the web server by clicking a button.
Obviously, the same system can be driven automatically.
Let's look at a way to extract the boundary polygon or a Revit room element, convert that to SVG, and invoke the web server to display it.
Here is the room element in Revit, intentionally weirdly shaped:
I invoke my new sweet SvgExport Revit add-in in that model, and. lo and behold, up pops the web browser displaying the result returned by my Heroku-hosted Node.js web server:
How is this achieved?
Well, the The 3D Web Coder discussion yesterday explains how the Node.js web server to display an SVG polygon is implemented and provides a simple interactive user interface to test it.
The SvgExport Revit add-in performs the following steps to drive it:
- Select a room in the model –
GetRoom
. - If the model only contains one spatial element and it happens to be a room, select that.
- Otherwise, if any elements have been pre-selected, pick the first room encountered among them.
- Otherwise, prompt the user to interactively select a room or a room tag.
- Determine its outer boundary loop.
- Convert that to an SVG path definition.
- Flip the Y coordinates to toggle the up direction.
- Invoke the web server locally or globally.
Here is the entire implementation of the SvgExport external command and its support methods:
#region Namespaces using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Autodesk.Revit.ApplicationServices; using Autodesk.Revit.Attributes; using Autodesk.Revit.DB; using Autodesk.Revit.DB.Architecture; using Autodesk.Revit.UI; using Autodesk.Revit.UI.Selection; using BoundarySegment = Autodesk.Revit.DB.BoundarySegment; #endregion namespace SvgExport { [Transaction( TransactionMode.ReadOnly )] public class Command01 : IExternalCommand { const int _target_square_size = 100; /// <summary> /// Allow selection of room elements only. /// </summary> class RoomSelectionFilter : ISelectionFilter { public bool AllowElement( Element e ) { return e is Room; } public bool AllowReference( Reference r, XYZ p ) { return true; } } /// <summary> /// Get a room from the given document. /// If there is only one single room in the entire /// model, return that with no further ado. /// Otherwise, if any elements have been pre-selected, /// pick the first room encountered among them. /// Otherwise, prompt the user to interactively /// select a room or a room tag. /// </summary> Result GetRoom( UIDocument uidoc, out Room room ) { room = null; Document doc = uidoc.Document; // Get all rooms in the model. FilteredElementCollector rooms = new FilteredElementCollector( doc ) .OfClass( typeof( SpatialElement ) ); if( 1 == rooms.Count() && rooms.FirstElement() is Room ) { // If there is only one spatial element // and that is a room, pick that. room = rooms.FirstElement() as Room; } else { Selection sel = uidoc.Selection; // Check the preselacted elements. ICollection<ElementId> ids = sel.GetElementIds(); foreach( ElementId id in ids ) { Element e = doc.GetElement( id ); if( e is Room ) { room = e as Room; break; } if( e is RoomTag ) { room = ( e as RoomTag ).Room; break; } } if( null == room ) { // Prompt for interactive selection. try { Reference r = sel.PickObject( ObjectType.Element, new RoomSelectionFilter(), "Please select pick a room" ); room = doc.GetElement( r.ElementId ) as Room; } catch( Autodesk.Revit.Exceptions .OperationCanceledException ) { return Result.Cancelled; } } } return Result.Succeeded; } /// <summary> /// Return an SVG representation of the /// given XYZ point scaled, offset and /// Y flipped to the target square size. /// </summary> string GetSvgPointFrom( XYZ p, XYZ pmid, double scale ) { p -= pmid; p *= scale; int x = (int) ( p.X + 0.5 ); int y = (int) ( p.Y + 0.5 ); // The Revit Y coordinate points upwards, // the SVG one down. y = -y; x += _target_square_size / 2; y += _target_square_size / 2; return x.ToString() + " " + y.ToString(); } /// <summary> /// Generate and return an SVG path definition to /// represent the given room boundary loop, scaled /// from the given bounding box to fit into a /// 100 x 100 canvas. /// </summary> string GetSvgPathFrom( BoundingBoxXYZ bb, IList<BoundarySegment> loop ) { // Determine scaling and offsets to transform // from bounding box to (0,0)-(100,100). XYZ pmin = bb.Min; XYZ pmax = bb.Max; XYZ vsize = pmax - pmin; XYZ pmid = pmin + 0.5 * vsize; double size = Math.Max( vsize.X, vsize.Y ); double scale = _target_square_size / size; StringBuilder s = new StringBuilder(); int nSegments = loop.Count; XYZ p0 = null; // loop start point XYZ p; // segment start point XYZ q = null; // segment end point int x, y; string sxy; foreach( BoundarySegment seg in loop ) { // Todo: handle non-linear curve. // Especially: if two long lines have a // short arc in between them, skip the arc // and extend both lines. p = seg.Curve.GetEndPoint( 0 ); Debug.Assert( null == q || q.IsAlmostEqualTo( p ), "expected last endpoint to equal current start point" ); q = seg.Curve.GetEndPoint( 1 ); if( null == p0 ) { p0 = p; // save loop start point s.Append( "M" + GetSvgPointFrom( p, pmid, scale ) ); } s.Append( "L" + GetSvgPointFrom( q, pmid, scale ) ); } s.Append( "Z" ); Debug.Assert( q.IsAlmostEqualTo( p0 ), "expected last endpoint to equal loop start point" ); return s.ToString(); } /// <summary> /// Invoke the SVG node.js web server. /// Use a local or global base URL and append /// the SVG path definition as a query string. /// Compare this with the JavaScript version used in /// http://the3dwebcoder.typepad.com/blog/2015/04/displaying-2d-graphics-via-a-node-server.html /// </summary> void DisplaySvg( string path_data ) { var local = false; var base_url = local ? "http://127.0.0.1:5000" : "https://shielded-hamlet-1585.herokuapp.com"; var d = path_data.Replace( ' ', '+' ); var query_string = "d=" + d; string url = base_url + '?' + query_string; System.Diagnostics.Process.Start( url ); } 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; Room room = null; Result rc = GetRoom( uidoc, out room ); SpatialElementBoundaryOptions opt = new SpatialElementBoundaryOptions(); opt.SpatialElementBoundaryLocation = SpatialElementBoundaryLocation.Center; // loops closed //SpatialElementBoundaryLocation.Finish; // loops not closed IList<IList<BoundarySegment>> loops = room.GetBoundarySegments( opt ); int nLoops = loops.Count; BoundingBoxXYZ bb = room.get_BoundingBox( null ); string path_data = GetSvgPathFrom( bb, loops[0] ); DisplaySvg( path_data ); return Result.Succeeded; } } }
The method DisplaySvg
that invokes the Heroku-hosted web server was copied almost verbatim from the original JavaScript function submit_form_svg_01
used to submit the form defined to
manually test the node.js server to display an SVG polygon.
It is interesting to compare the two implementations side by side, so that is exactly what I did in the follow-up discussion on driving the SVG node server from a desktop application.
The entire source code, Visual Studio solution and add-in manifest are provided in the SvgExport GitHub repository, and the version presented here is release 2015.0.0.0.
As I already pointed out yesterday on The 3D Web Coder, I could just as easily implement this locally in pure JavaScript, of course.
Hosting it in a separate server makes no sense whatsoever as long as it is just for my personal local use.
The server implementation is only of interest once we consider more widespread use, e.g., globally accessible sharing of data retrieved from multiple projects for multiple interested parties.
That is where we are headed as soon as we can :-)