Saturday, March 15, 2014

Selecting Entities In ModelSpace Through Viewport

Very often, we need to select entities in ModelSpace, which are visible in a given Viewport on a layout. That is, we want to project the viewport's boundary (as a rectangle, or as a non-rectangle polygon) into ModelSpace and find all entities inside, fully or partially.

In other case, given a point, or an entity, in ModelSpace, we may want to determine which viewport or viewports on a layer the point/entity can be seen.

Note: there is a post in Autodesk's user forum on this topic.

I began with code that collects Viewport information on a given layout. The information is used to determine which entities in ModelSpace are visible through each Viewport, and is collected in one single transaction. Here is the code:

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.DatabaseServices;
    3 using Autodesk.AutoCAD.EditorInput;
    4 using Autodesk.AutoCAD.Geometry;
    5 using System;
    6 using System.Collections.Generic;
    7 
    8 namespace EntitiesInsideViewport
    9 {
   10     //Class to hold Viewport information, obtained
   11     //in single Transaction
   12     public class ViewportInfo
   13     {
   14         public ObjectId ViewportId { set; get; }
   15         public ObjectId NonRectClipId { set; get; }
   16         public Point3dCollection BoundaryInPaperSpace { set; get; }
   17         public Point3dCollection BoundaryInModelSpace { set; get; }
   18     }
   19 
   20     public class CadHelper
   21     {
   22         //Get needed Viewport information
   23         public static ViewportInfo[] SelectLockedViewportInfoOnLayout(
   24             Document dwg, string layoutName)
   25         {
   26             List<ViewportInfo> lst = new List<ViewportInfo>();
   27             TypedValue[] vals = new TypedValue[]{
   28                 new TypedValue((int)DxfCode.Start, "VIEWPORT"),
   29                 new TypedValue((int)DxfCode.LayoutName,layoutName)
   30             };
   31 
   32             PromptSelectionResult res =
   33                 dwg.Editor.SelectAll(new SelectionFilter(vals));
   34             if (res.Status==PromptStatus.OK)
   35             {
   36                 using (Transaction tran=
   37                     dwg.TransactionManager.StartTransaction())
   38                 {
   39                     foreach (ObjectId id in res.Value.GetObjectIds())
   40                     {
   41                         Viewport vport = (Viewport)tran.GetObject(
   42                             id, OpenMode.ForRead);
   43                         if (vport.Number!=1 && vport.Locked)
   44                         {
   45                             ViewportInfo vpInfo = new ViewportInfo();
   46                             vpInfo.ViewportId = id;
   47                             vpInfo.NonRectClipId = vport.NonRectClipEntityId;
   48                             if (!vport.NonRectClipEntityId.IsNull &&
   49                                 vport.NonRectClipOn)
   50                             {
   51                                 Polyline2d pl = (Polyline2d)tran.GetObject(
   52                                     vport.NonRectClipEntityId, OpenMode.ForRead);
   53                                 vpInfo.BoundaryInPaperSpace =
   54                                     GetNonRectClipBoundary(pl, tran);
   55                             }
   56                             else
   57                             {
   58                                 vpInfo.BoundaryInPaperSpace =
   59                                     GetViewportBoundary(vport);
   60                             }
   61 
   62                             Matrix3d mt = PaperToModel(vport);
   63                             vpInfo.BoundaryInModelSpace =
   64                                 TransformPaperSpacePointToModelSpace(
   65                                 vpInfo.BoundaryInPaperSpace, mt);
   66 
   67                             lst.Add(vpInfo);
   68                         }
   69                     }
   70 
   71                     tran.Commit();
   72                 }
   73             }
   74 
   75             return lst.ToArray();
   76         }
   77 
   78         private static Point3dCollection GetViewportBoundary(Viewport vport)
   79         {
   80             Point3dCollection points = new Point3dCollection();
   81 
   82             Extents3d ext = vport.GeometricExtents;
   83             points.Add(new Point3d(ext.MinPoint.X, ext.MinPoint.Y, 0.0));
   84             points.Add(new Point3d(ext.MinPoint.X, ext.MaxPoint.Y, 0.0));
   85             points.Add(new Point3d(ext.MaxPoint.X, ext.MaxPoint.Y, 0.0));
   86             points.Add(new Point3d(ext.MaxPoint.X, ext.MinPoint.Y, 0.0));
   87 
   88             return points;
   89         }
   90 
   91         private static Point3dCollection GetNonRectClipBoundary(
   92             Polyline2d polyline, Transaction tran)
   93         {
   94             Point3dCollection points = new Point3dCollection();
   95 
   96             foreach (ObjectId vxId in polyline)
   97             {
   98                 Vertex2d vx = (Vertex2d)tran.GetObject(vxId, OpenMode.ForRead);
   99                 points.Add(polyline.VertexPosition(vx));
  100             }
  101 
  102             return points;
  103         }
  104 
  105         private static Point3dCollection TransformPaperSpacePointToModelSpace(
  106             Point3dCollection paperSpacePoints, Matrix3d mt)
  107         {
  108             Point3dCollection points = new Point3dCollection();
  109 
  110             foreach (Point3d p in paperSpacePoints)
  111             {
  112                 points.Add(p.TransformBy(mt));
  113             }
  114 
  115             return points;
  116         }
  117 
  118         #region
  119         //**********************************************************************
  120         //Create coordinate transform matrix
  121         //between modelspace and paperspace viewport
  122         //The code is borrowed from
  123         //http://www.theswamp.org/index.php?topic=34590.msg398539#msg398539
  124         //*********************************************************************
  125         public static Matrix3d PaperToModel(Viewport vp)
  126         {
  127             Matrix3d mx = ModelToPaper(vp);
  128             return mx.Inverse();
  129         }
  130 
  131         public static Matrix3d ModelToPaper(Viewport vp)
  132         {
  133             Vector3d vd = vp.ViewDirection;
  134             Point3d vc = new Point3d(vp.ViewCenter.X, vp.ViewCenter.Y, 0);
  135             Point3d vt = vp.ViewTarget;
  136             Point3d cp = vp.CenterPoint;
  137             double ta = -vp.TwistAngle;
  138             double vh = vp.ViewHeight;
  139             double height = vp.Height;
  140             double width = vp.Width;
  141             double scale = vh / height;
  142             double lensLength = vp.LensLength;
  143             Vector3d zaxis = vd.GetNormal();
  144             Vector3d xaxis = Vector3d.ZAxis.CrossProduct(vd);
  145             Vector3d yaxis;
  146 
  147             if (!xaxis.IsZeroLength())
  148             {
  149                 xaxis = xaxis.GetNormal();
  150                 yaxis = zaxis.CrossProduct(xaxis);
  151             }
  152             else if (zaxis.Z &lt; 0)
  153             {
  154                 xaxis = Vector3d.XAxis * -1;
  155                 yaxis = Vector3d.YAxis;
  156                 zaxis = Vector3d.ZAxis * -1;
  157             }
  158             else
  159             {
  160                 xaxis = Vector3d.XAxis;
  161                 yaxis = Vector3d.YAxis;
  162                 zaxis = Vector3d.ZAxis;
  163             }
  164             Matrix3d pcsToDCS = Matrix3d.Displacement(Point3d.Origin - cp);
  165             pcsToDCS = pcsToDCS * Matrix3d.Scaling(scale, cp);
  166             Matrix3d dcsToWcs = Matrix3d.Displacement(vc - Point3d.Origin);
  167             Matrix3d mxCoords = Matrix3d.AlignCoordinateSystem(
  168                 Point3d.Origin, Vector3d.XAxis, Vector3d.YAxis,
  169                 Vector3d.ZAxis, Point3d.Origin,
  170                 xaxis, yaxis, zaxis);
  171             dcsToWcs = mxCoords * dcsToWcs;
  172             dcsToWcs = Matrix3d.Displacement(vt - Point3d.Origin) * dcsToWcs;
  173             dcsToWcs = Matrix3d.Rotation(ta, zaxis, vt) * dcsToWcs;
  174 
  175             Matrix3d perspectiveMx = Matrix3d.Identity;
  176             if (vp.PerspectiveOn)
  177             {
  178                 double vSize = vh;
  179                 double aspectRatio = width / height;
  180                 double adjustFactor = 1.0 / 42.0;
  181                 double adjstLenLgth = vSize * lensLength *
  182                     Math.Sqrt(1.0 + aspectRatio * aspectRatio) * adjustFactor;
  183                 double iDist = vd.Length;
  184                 double lensDist = iDist - adjstLenLgth;
  185                 double[] dataAry = new double[]
  186                 {
  187                     1,0,0,0,0,1,0,0,0,0,
  188                     (adjstLenLgth-lensDist)/adjstLenLgth,
  189                     lensDist*(iDist-adjstLenLgth)/adjstLenLgth,
  190                     0,0,-1.0/adjstLenLgth,iDist/adjstLenLgth
  191                 };
  192 
  193                 perspectiveMx = new Matrix3d(dataAry);
  194             }
  195 
  196             Matrix3d finalMx =
  197                 pcsToDCS.Inverse() * perspectiveMx * dcsToWcs.Inverse();
  198 
  199             return finalMx;
  200         }
  201 
  202         #endregion
  203     }
  204 }

Now the following code does 2 things we want to do very often: finding out which entities in ModelSpace are visible in which Viewport; and determining a given entity in ModelSpace is visible in which Viewports:

    1 using System.Collections.Generic;
    2 using Autodesk.AutoCAD.ApplicationServices;
    3 using Autodesk.AutoCAD.DatabaseServices;
    4 using Autodesk.AutoCAD.EditorInput;
    5 using Autodesk.AutoCAD.Geometry;
    6 using Autodesk.AutoCAD.Runtime;
    7 
    8 [assembly: CommandClass(typeof(EntitiesInsideViewport.MyCommands))]
    9 
   10 namespace EntitiesInsideViewport
   11 {
   12     public class MyCommands
   13     {
   14         //Use viewport boundary as selecting window/polygon
   15         //to find entities in modelspace visible in each viewport
   16         [CommandMethod("VpSelect")]
   17         public static void SelectByViewport()
   18         {
   19             Document dwg = Application.DocumentManager.MdiActiveDocument;
   20             Editor ed = dwg.Editor;
   21 
   22             //Save current layout name
   23             string curLayout = LayoutManager.Current.CurrentLayout;
   24 
   25             try
   26             {
   27                 //Get viewport information on current layout
   28                 ViewportInfo[] vports = GetViewportInfoOnCurrentLayout();
   29                 if (vports == null) return;
   30 
   31                 //Switch to modelspace
   32                 LayoutManager.Current.CurrentLayout = "Model";
   33 
   34                 //Select entities in modelspace that are visible
   35                 foreach (ViewportInfo vInfo in vports)
   36                 {
   37                     ObjectId[] ents = SelectEntitisInModelSpaceByViewport(
   38                         dwg, vInfo.BoundaryInModelSpace);
   39                     ed.WriteMessage("\n{0} entit{1} fond via Viewport \"{2}\"",
   40                         ents.Length,
   41                         ents.Length > 1 ? "ies" : "y",
   42                         vInfo.ViewportId.ToString());
   43                 }
   44 
   45                 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   46             }
   47             catch (System.Exception ex)
   48             {
   49                 ed.WriteMessage("\nCommand \"VpSelect\" failed:");
   50                 ed.WriteMessage("\n{0}\n{1}", ex.Message, ex.StackTrace);
   51             }
   52             finally
   53             {
   54                 //Restore back to original layout
   55                 if (LayoutManager.Current.CurrentLayout!=curLayout)
   56                 {
   57                     LayoutManager.Current.CurrentLayout = curLayout;
   58                 }
   59 
   60                 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   61             }
   62         }
   63 
   64         //Determine a given entity in modelspace is visible in
   65         //which viewports
   66         [CommandMethod("GetViewports")]
   67         public static void FindContainingViewport()
   68         {
   69             Document dwg = Application.DocumentManager.MdiActiveDocument;
   70             Editor ed = dwg.Editor;
   71 
   72             //Switch to modelspace
   73             string curLayout = LayoutManager.Current.CurrentLayout;
   74 
   75             try
   76             {
   77                 //Get viewport information on current layout
   78                 ViewportInfo[] vports = GetViewportInfoOnCurrentLayout();
   79                 if (vports == null) return;
   80 
   81                 //Pick an entity in modelspace
   82                 LayoutManager.Current.CurrentLayout = "Model";
   83                 ObjectId entId = PickEntity(ed);
   84                 if (entId.IsNull)
   85                 {
   86                     ed.WriteMessage("\n*Cancel*");
   87                 }
   88                 else
   89                 {
   90                     //Find viewport in which the selected entity is visible
   91                     List&lt;ObjectId> lst = new List&lt;ObjectId>();
   92                     foreach (ViewportInfo vpInfo in vports)
   93                     {
   94                         if (IsEntityInsideViewportBoundary(
   95                             dwg, entId, vpInfo.BoundaryInModelSpace))
   96                         {
   97                             lst.Add(vpInfo.ViewportId);
   98                             ed.WriteMessage(
   99                                 "\nSelected entity is visible in viewport \"{0}\"",
  100                                 vpInfo.ViewportId.ToString());
  101                         }
  102                     }
  103 
  104                     if (lst.Count == 0)
  105                         ed.WriteMessage(
  106                             "\nSelected entity is not visible in all viewports");
  107                     else
  108                         ed.WriteMessage(
  109                             "\nSelected entity is visible in {0} viewport{1}.",
  110                             lst.Count, lst.Count > 1 ? "s" : "");
  111                 }
  112 
  113                 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
  114             }
  115             catch (System.Exception ex)
  116             {
  117                 ed.WriteMessage("\nCommand \"GetViewports\" failed:");
  118                 ed.WriteMessage("\n{0}\n{1}", ex.Message, ex.StackTrace);
  119             }
  120             finally
  121             {
  122                 //Restore back to original layout
  123                 if (LayoutManager.Current.CurrentLayout != curLayout)
  124                 {
  125                     LayoutManager.Current.CurrentLayout = curLayout;
  126                 }
  127 
  128                 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
  129             }
  130         }
  131 
  132         private static ViewportInfo[] GetViewportInfoOnCurrentLayout()
  133         {
  134             string layoutName = LayoutManager.Current.CurrentLayout;
  135             if (layoutName.ToUpper() == "MODEL")
  136             {
  137                 Application.ShowAlertDialog("Please set a layout as active layout!");
  138                 return null;
  139             }
  140             else
  141             {
  142                 Document dwg = Application.DocumentManager.MdiActiveDocument;
  143                 ViewportInfo[] vports =
  144                     CadHelper.SelectLockedViewportInfoOnLayout(dwg, layoutName);
  145                 if (vports.Length == 0)
  146                 {
  147                     Application.ShowAlertDialog(
  148                         "No locked viewport found on layout \"" + layoutName + "\".");
  149                     return null;
  150                 }
  151                 else
  152                 {
  153                     return vports;
  154                 }
  155             }
  156         }
  157 
  158         private static ObjectId[] SelectEntitisInModelSpaceByViewport(
  159             Document dwg, Point3dCollection boundaryInModelSpace)
  160         {
  161             ObjectId[] ids = null;
  162 
  163             using (Transaction tran=dwg.TransactionManager.StartTransaction())
  164             {
  165                 //Zoom to the extents of the viewport boundary in modelspace
  166                 //before calling Editor.SelectXxxxx()
  167                 ZoomToWindow(boundaryInModelSpace);
  168 
  169                 PromptSelectionResult res =
  170                     dwg.Editor.SelectCrossingPolygon(boundaryInModelSpace);
  171                 if (res.Status==PromptStatus.OK)
  172                 {
  173                     ids = res.Value.GetObjectIds();
  174                 }
  175 
  176                 //Restored to previous view (view before zoomming)
  177                 tran.Abort();
  178             }
  179 
  180             return ids;
  181         }
  182 
  183         private static void ZoomToWindow(Point3dCollection boundaryInModelSpace)
  184         {
  185             Extents3d ext =
  186                     GetViewportBoundaryExtentsInModelSpace(boundaryInModelSpace);
  187 
  188             double[] p1 = new double[] { ext.MinPoint.X, ext.MinPoint.Y, 0.00 };
  189             double[] p2 = new double[] { ext.MaxPoint.X, ext.MaxPoint.Y, 0.00 };
  190 
  191             dynamic acadApp = Application.AcadApplication;
  192             acadApp.ZoomWindow(p1, p2);
  193         }
  194 
  195         private static Extents3d GetViewportBoundaryExtentsInModelSpace(
  196             Point3dCollection points)
  197         {
  198             Extents3d ext = new Extents3d();
  199             foreach (Point3d p in points)
  200             {
  201                 ext.AddPoint(p);
  202             }
  203 
  204             return ext;
  205         }
  206 
  207         private static ObjectId PickEntity(Editor ed)
  208         {
  209             PromptEntityOptions opt =
  210                 new PromptEntityOptions("\nSelect an entity:");
  211             PromptEntityResult res = ed.GetEntity(opt);
  212             if (res.Status==PromptStatus.OK)
  213             {
  214                 return res.ObjectId;
  215             }
  216             else
  217             {
  218                 return ObjectId.Null;
  219             }
  220         }
  221 
  222         private static bool IsEntityInsideViewportBoundary(
  223             Document dwg, ObjectId entId, Point3dCollection boundaryInModelSpace)
  224         {
  225             bool inside = false;
  226             using (Transaction tran = dwg.TransactionManager.StartTransaction())
  227             {
  228                 //Zoom to the extents of the viewport boundary in modelspace
  229                 //before calling Editor.SelectXxxxx()
  230                 ZoomToWindow(boundaryInModelSpace);
  231 
  232                 PromptSelectionResult res =
  233                     dwg.Editor.SelectCrossingPolygon(boundaryInModelSpace);
  234                 if (res.Status == PromptStatus.OK)
  235                 {
  236                     foreach (ObjectId id in res.Value.GetObjectIds())
  237                     {
  238                         if (id==entId)
  239                         {
  240                             inside = true;
  241                             break;
  242                         }
  243                     }
  244                 }
  245 
  246                 //Restored to previous view (before zoomming)
  247                 tran.Abort();
  248             }
  249 
  250             return inside;
  251         } 
  252     }
  253 }

Following picture shows the drawing I test the code against:




Friday, March 7, 2014

Real-Time AutoCAD Command Monitoring with ASP.NET SignalR

Introduction

What is ASP.NET SignalR? From to Microsoft's ASP.NET website, it says:

ASP.NET SignalR is a new library for ASP.NET developers that makes developing real-time web functionality easy. SignalR allows bi-directional communication between server and client. Servers can now push content to connected clients instantly as it becomes available. SignalR supports Web Sockets, and falls back to other compatible techniques for older browsers. SignalR includes APIs for connection management (for instance, connect and disconnect events), grouping connections, and authorization.

Go to here for more information on ASP.NET SignalR. Searching the Internet would bring up tons of links on this technology, including code samples. tutorials. While this technology is mainly meant for web application, it does support .NET client.

In my recent web application study/development, I had opportunity to explore this technology a little bit. Just a months ago, I explored using ASP.NET Web API to automate AutoCAD. I thought why not to give it a try to make ASP.NET SignalR and AutoCAD work together? 

Since SignalR is about bi-directional communication between server and client and primarily meant for web browser as the client side UI, I felt it would be interesting to make a running AutoCAD to communicate with a web browser.

The core of communication via SignalR is a set of API, called Hub API, hosted somewhere as communication server, the clients (multiple web browsers) talk to each other via the Hub. Like Web API, SignalR can either be hosted in a web server (IIS), or be self-hosted (as a console application, Windows service application...).

Many AutoCAD programmers may have done (or been asked to develop) something that monitors AutoCAD usage at some point. I did this kind of applications with AutoCAD VBA and .NET API. So, I decide to see how easy (or how difficult) to build a simple application that can monitor AutoCAD usage in real-time mode by tracking AutoCAD commands executed by an AutoCAD user. I did not mean to create an application that has practical value, just want it to be a proof of concept.

Here are couple of development considerations:

1. I decided to self-host SignalR's server in a console application. Thus my code can run with any Windows computer with .NET 4.5 installed without relying on a web server somewhere on the network for hosting SignalR server.

2. Since SignalR requires .NET 4.5, the AutoCAD add-in that uses SignalR .NET client API must also target 4.5. Although latest AutoCAD (2013/2014) only officially support .NET 4.0, AutoCAD NET add-in built on .NET 4.5 should work OK in general.


A Pilot Development
 
I started with a Visual Studio 2013 solution with 4 projects in it:

 
 
1. Project SignalRSelfHost

A console application that host SignalR's Hub API. For this project, NuGet Package Microsoft ASP.NET SignalR Self Host is added, which also brings in a few other dependency packages, such as Microsoft.Owin, NewtonSoft.Json.


The code of this project looks like:

using Microsoft.AspNet.SignalR;
using Microsoft.Owin.Cors;
using Microsoft.Owin.Hosting;
using Owin;
using SignalRCadData;
using System;
 
namespace SignalRSelfHost
{
    class Program
    {
        static void Main(string[] args)
        {
            string url = "http://localhost:8080";
            using (WebApp.Start(url))
            {
                Console.Write("server running on {0}", url);
                Console.WriteLine("Press any key to exit...");
                Console.ReadLine();
            }
        }
    }
 
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCors(CorsOptions.AllowAll);
            app.MapSignalR();
        }
    }
 
    public class MyHub : Hub
    {
        //Test method, not used in this blog
        public void Send(string name, string message)
        {
            Clients.All.addMessage(name, message);
        }
 
        //AutoCAD calls this method by sending an 
        //AcadCommandTrack object to MyHub
        public void RelayAcadMessage(AcadCommandTrack cmdTrack)
        {
            //Write to console, in order to see the server does
            //receive calls from SignalR client
            Console.Write("Received data: " + cmdTrack.ToString());
 
            //MyHub calls all connected clients where an Action
            //named as "getAcadMessage" (function in JavScript)
            //will do something on the client side
            Clients.All.getAcadMessage(cmdTrack.ToString());
 
            Console.WriteLine("...data has been sent to client.");
            Console.WriteLine("Press any key to exit...");
        }
    } 
}

Note: I came across an exception when running the console application the first time. It turned out the Microsoft.Owin.Security package brought in by Microsoft ASP.NET SignalR Self Host package as dependency package were out-of-date. After added Microsoft.Owin.Security package separately, the exception went away:



2. Project SignalRCadData

This project only contains a simple data model class AcadCommandTrack:

using System;
 
namespace SignalRCadData
{
    public class AcadCommandTrack
    {
        public string UserName { setget; }
        public string ComputerName { setget; }
        public string CommandName { setget; }
        public DateTime CmdExecTime { setget; }
        public string DwgFileName { setget; }
 
        public override string ToString()
        {
            return "Command \"" + CommandName + "\"" +
                " executed in drawing \"" + DwgFileName + "\"" +
                " at " + CmdExecTime.ToLongTimeString() +
                " on computer \"" + ComputerName + "\"" +
                " by \"" + UserName + "\"";
        }
 
        public AcadCommandTrack()
        {
            UserName = "None";
            ComputerName = "None";
            CommandName = "None";
            CmdExecTime = DateTime.Now;
            DwgFileName = "";
        }
    }
}


3. Project JavaScriptClient

This project is a web application with a single and simple HTML page AcadCmdTrack.html (the Default.html was created for test purpose, which I did not bother to remove it). To be able to act as SignalR client running inside a web browser, the NuGet package Microsoft ASP.NET SignalR JavaScript Client must be added into this project:


The AcadCmdTrack.html page's markup and JavaScript are quite simple:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>AutoCAD Command Tracking</title>
    <style type="text/css">
        .container {
            background-color#99CCFF;
            borderthick solid #808080;
            padding20px;
            margin20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <ul id="discussion"></ul>
    </div>
    <!--Script references. -->
    <!--Reference the jQuery library. -->
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <!--Reference the SignalR library. -->
    <script src="Scripts/jquery.signalR-2.0.2.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="http://localhost:8080/signalr/hubs"></script>
    <!--Add script to update the page and send messages.-->
    <script type="text/javascript">
        $(function () {
            //Set the hubs URL for the connection
            $.connection.hub.url = "http://localhost:8080/signalr";
 
            // Declare a proxy to reference the hub.
            var chat = $.connection.myHub;
 
            // Create a function that the hub can call to broadcast messages.
            chat.client.getAcadMessage = function (message) {
                // Html encode display name and message.
                var encodedMsg = $('<div />').text(message).html();
                // Add the message to the page.
                $('#discussion').append('<li><strong>' + encodedMsg + '</li>');
            };
 
            // Start the connection.
            $.connection.hub.start();
        });
    </script>
</body>
</html>

In the JavaScript, we can see how the web page makes connection to the SignalR server's hub MyHub:

var chat = $.connection.myHub;

and how it defines a function getAcadMessage() so that the SignalR hub on the server side can call an action on the client side.

 
4. Project ConsoleClient

Before I jumped into AutoCAD, I decided to first create a simple console application as a SignalR .NET client to simulate the work of AutoCAD: sending message (represented by class AcadCommandTrack) to SignalR server, so that the message can be instantly shown in web browser (that is, the web browser works like a real time monitor of an AutoCAD session).

The NuGet package Microsoft ASP.NET SignalR .NET Client must be added to this project:


Here is the code for this project:

using Microsoft.AspNet.SignalR.Client;
using System;
using SignalRCadData;
 
namespace ConsoleClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Connecting SignalR server...");
 
            var hubConnection = new HubConnection("http://localhost:8080");
 
            bool connected = true;
 
            //IHubProxy must be created before the connection's Start()
            IHubProxy hubProxy = hubConnection.CreateHubProxy("MyHub");
            hubConnection.Start().ContinueWith(task =>
                {
                    if (task.IsCompleted)
                    {
                        Console.WriteLine("Connected successfully.");
                    }
                    else
                    {
                        connected = false;
                        Console.WriteLine("Cannot connect to server: {0}", 
                            task.Exception.GetBaseException().Message);
                    }
                }).Wait();
            
            if (!connected)
            {
                Console.WriteLine("Press any key to exit...");
                Console.ReadLine();
            }
            else
            {
                //This make this client gets server call on the 
                //Action 'getAcadMessage'
                hubProxy.On<string>("getAcadMessage", param =>
                {
                    Console.WriteLine(param);
                });
 
                string serverMethod = "RelayAcadMessage";
                Console.WriteLine("Enter M to send message, or press Enter to exit...");
                while(true)
                {     
                    string input = Console.ReadLine();
                    if (string.IsNullOrEmpty(input)) break;
 
                    //Call the server's RelayAcadMessage() method with
                    //AcadCmdTrack object passed in.
                    //Then all connected SignalR clients will be called
                    //by the server, as long as an Action called
                    //'getAcadMessage' is defined inside the client
                    hubProxy.Invoke<AcadCommandTrack>(
                        serverMethod,
                        new AcadCommandTrack
                        {
                            CommandName = "EDM-YUAN",
                            UserName = "norman.yuan",
                            ComputerName = "LINE",
                            CmdExecTime = DateTime.Now,
                            DwgFileName="Xxxxxx.dwg"
                        }).ContinueWith(task =>
                            {
                                if (task.IsCompleted)
                                {
                                    Console.WriteLine("AcadCommandTrack message is sent.");
                                }
                                else
                                {
                                    Console.WriteLine("Calling \"" + serverMethod + "\" failed: {0}", 
                                        task.Exception.GetBaseException().Message);
                                }
                            }).ContinueWith(c=>
                                {
                                    Console.WriteLine("Enter M to send message, or press Enter to exit...");
                                });
                }
 
                hubConnection.Stop();
                hubConnection.Dispose();
            }
        }
    }
}


After the solution is built successfully, it is time to give it a try. In order to be able to make the server and clients projects all start, I right-clicked the solution in the Solution Explorer, and selected "Set Startup Project...":


so that the server project is started before the client projects.

Here is the video clip showing how they work together as server-client communication goes.


Put AutoCAD In Action

With the success of the solution SignalR_Study, I then brought AutoCAD into the picture in a second Visual Studio solution AcadSignalRStudy:


The solution added the project SignalRCadData from the first solution as external project, so that the AutoCAD addin project AcadSignalRClient can haave reference to it.

The project AcadSignalRClient targets .NET 4.51 because of the requirement of SignalR 2.0. According to Microsoft, the SignalR client can be in older version of SignalR server. That means I could have used older version of SignalR client, so that my AutoCAD addin can still target .NET 4.0. However, using .NET 4.51 in this AutoCAD addin did not cause any issue.

Here is the idea of the entire thing:

I am an very picky boss who supervise a bunch of CAD users. I want to analyse how a Cad user does a particular drafting task by monitoring what commands he/she uses to fulfill that task. So, I do:
  • Set up a SignalR server
  • In AutoCAD, I ask the user turn on AutoCAD command real-time tracking addin
  • Launch a SignalR client web application in my web browser, start watching how user commands AutoCAD working.
What the AutoCAD addin does is basically handling Document.CommandWillStart event. In the event handler a SignalR client (IHubProxy object) collect information on current AutoCAD command and sends the information to SignalR server. The SignalR server in turn sends the information to the connected web browser.

Here is the code for the AutoCAD addin:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Microsoft.AspNet.SignalR.Client;
 
[assemblyCommandClass(typeof(AcadSignalRClient.MyCommands))]
 
namespace AcadSignalRClient
{
    public class MyCommands
    {
        private static bool _trackCommand = false;
        private static HubConnection _hubConnection = null;
        private static IHubProxy _hubProxy = null;
        private const string HOST_URL = "http://localhost:8080";
 
        [CommandMethod("TrackCmd"CommandFlags.Session)]
        public static void RunMyCommand()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            if (!_trackCommand)
            {
                try
                {
                    ed.WriteMessage(
                        "\nConnecting to SignalR server...");
 
                    //Attach event handler to DocumentCollection/Document
                    AttachDocumentEventHandlers();
 
                    //Setup SignalR client
                    bool connected = CreateHubConnection();
                    if (connected)
                    {
                        ed.WriteMessage(
                            "\nConnection to SignalR server established!");
                        _trackCommand = true;
                    }
                    else
                    {
                        _trackCommand = false;
                        throw new InvalidOperationException(
                            "cannot connect to SignalR server at " + HOST_URL);
                    } 
    
                    if (_trackCommand)
                    {
                        ed.WriteMessage("\nCommand tracking is on.");
                    }
                }
                catch (System.Exception ex)
                {
                    ed.WriteMessage("\nError: {0}", ex.Message);
 
                    _trackCommand = false;
                    _hubConnection = null;
                    _hubProxy = null;
                }
            }
            else
            {
                _hubConnection.Stop();
                _hubConnection.Dispose();
                _hubConnection = null;
                _hubProxy = null;
 
                ed.WriteMessage("\nCommand tracking is off.");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
 
        #region private methods
 
        private static bool CreateHubConnection()
        {
            _hubConnection = new HubConnection(HOST_URL);
            _hubProxy = _hubConnection.CreateHubProxy("MyHub");
 
            bool connected = true;
 
            _hubConnection.Start().ContinueWith(task =>
            {
                if (!task.IsCompleted)
                {
                    connected = false;
                }
            }).Wait();
 
            return connected;
        }
 
        private static void AttachDocumentEventHandlers()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            dwg.CommandWillStart += dwg_CommandWillStart;
 
            Application.DocumentManager.DocumentCreated += 
                DocumentManager_DocumentCreated;
            Application.DocumentManager.DocumentToBeDestroyed += 
                DocumentManager_DocumentToBeDestroyed;
        }
 
        private static void DocumentManager_DocumentToBeDestroyed(
            object sender, DocumentCollectionEventArgs e)
        {
            e.Document.CommandWillStart -= dwg_CommandWillStart;
        }
 
        private static void DocumentManager_DocumentCreated(
            object sender, DocumentCollectionEventArgs e)
        {
            e.Document.CommandWillStart += dwg_CommandWillStart;
        }
 
        private static void dwg_CommandWillStart(
            object sender, CommandEventArgs e)
        {
            if (!_trackCommand) return;
 
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
            ed.WriteMessage("\nCommand {0} is about to start...", 
                e.GlobalCommandName.ToUpper());
 
            string dwgFileName = System.IO.Path.GetFileName(dwg.Name);
 
            SendMessageToSignalRServer(e.GlobalCommandName, dwgFileName);
        }
 
        private static void SendMessageToSignalRServer(
            string cmdName, string dwgFileName)
        {
            if (_hubConnection == nullreturn;
 
            string user = Environment.UserName;
            string computer = Environment.MachineName;
 
            SignalRCadData.AcadCommandTrack atrck = 
                new SignalRCadData.AcadCommandTrack()
                {
                    UserName = user,
                    ComputerName = computer,
                    CommandName = cmdName,
                    CmdExecTime = DateTime.Now,
                    DwgFileName=dwgFileName
                };
 
            //Call the hub method "RelayAcadMessage()" on 
            //SignalR server side
            _hubProxy.Invoke<SignalRCadData.AcadCommandTrack>(
                "RelayAcadMessage", atrck);
        }
 
        #endregion
    }
}
 
After the code was built successfully, I started run the projects in solution SignalR_Study first. Then I started solution "AcadSignalRStudy", which launches AutoCAD2014. after loading the addin DLL into AutoCAD, I entered the command "TrackCmd" to turn on monitoring to execution of AutoCAD commands. From this moment on, any command started can be watched in the monitoring web browser, until the command tracking process is turned off.

See this video clip showing how AutoCAD commands are monitored in real-time.


Final Thought

With the computing power moving to the Internet/cloud, more and more user applications run inside web browser instead of being native desktop application. Being so easily communicate an application inside an web browser by using SignalR is certainly a good thing to know and to use, when there is real business need. This article is just a scratch of it. While we AutoCAD programmers are still enjoying the ease and powerful AutoCAD .NET API when developing against one of the most important, traditional desktop application - AutoCAD, we should expand our programming knowledge/skill to embrace the coming tide of cloud based computing. ASP.NET SignalR is one of those things we could learn and get benefited from it, for now, at least.

Download the source code here.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.