Displaying a graph of AutoCAD drawing objects using JavaScript and .NET

It seems like I've been living in JavaScript land (and no, I deliberately didn't say "hell" – it's actually been fun 🙂 for the last few weeks, between one thing or another. But I think I've finally put the finishing touches on the last of the JavaScript API samples I've prepared for AU 2014.

This sample was inspired by Jim Awe – an old friend and colleague – who is working on something similar for another platform. So I can't take any credit for the way it works, just for the plumbing it took to make it work with AutoCAD.

It's basically an HTML palette using a handy open source library called D3.js – for Data-Driven Documents – and d3pie, a layer on top of that to simplify creating pie charts. The palette connects to the active drawing and asks to .NET to provides some data on the entities inside modelspace. From our .NET code we use LINQ to query the types of object from the modelspace's ObjectIds, which we then package up as JSON and return for display in HTML.

This is all that's needed to get this data, in case – it can all be done via the ObjectId without opening any of the entities (just the modelspace). LINQ is really great at this kind of query.

var q =

  from ObjectId o in ms

  group o by o.ObjectClass.DxfName into counts

  select new { Count = counts.Count(), Group = counts.Key };

When the user clicks on a wedge in the pie chart – representing the objects of a particular type – those objects get places in the pickfirst selection set, ready for something to be done with them.

Here's a screencast of the application working:

Here's the HTML…

<!doctype html>

<html>

<head>

  <title>Chart</title>

  <link rel="stylesheet" href="style.css">

  <style>

    html, body { height: 100%; width: 100%; margin: 0; padding: 0; }

    body { display: table; }

  </style>

  <script

    src="http://app.autocad360.com/jsapi/v2/Autodesk.AutoCAD.js">

  </script>

  <script src="js/acadext2.js"></script>

  <script src="js/d3.min.js"></script>

  <script src="js/d3pie.min.js"></script>

  <script src="js/chart.js"></script>

</head>

<body onload="init();">

  <div id="pieChart" class="centered-on-page">

  </div>

</body>

</html>

Here's the JavaScript, including the Shaping Layer extensions…

function getObjectCountsFromAutoCAD() {

  var jsonResponse =

    exec(

      JSON.stringify({

        functionName: 'GetObjectCountData',

      &
#160; invokeAsCommand: false,

        functionParams: undefined

      })

    );

  var jsonObj = JSON.parse(jsonResponse);

  if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {

    throw Error(jsonObj.retErrorString);

  }

  return jsonObj.result;

}

 

function selectObjectsOfType(jsonArgs) {

  var jsonResponse =

    exec(

      JSON.stringify({

        functionName: 'SelectObjectsOfType',

        invokeAsCommand: false,

        functionParams: jsonArgs

      })

    );

  var jsonObj = JSON.parse(jsonResponse);

  if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {

    throw Error(jsonObj.retErrorString);

  }

  return jsonObj.result;

}

var _pie = null;

 

function init() {

  registerCallback("refpie", refreshPie);

  loadPieData();

}

 

function refreshPie(args) {

  loadPieData();

}

 

function loadPieData() {

  var pieOpts = setupPieDefaults();

  try {

    var contents = getObjectCountsFromAutoCAD();

    if (contents) {

      pieOpts.data = contents;

      if (_pie)

        _pie.destroy();

      _pie = new d3pie("pieChart", pieOpts);

    }

  }

  catch (ex) {

    _pie.destroy();

  }

}

 

function clickPieWedge(evt) {

  selectObjectsOfType(

    { "class": evt.data.label, "expanded": evt.expanded }

  );

}

 

function setupPieDefaults() {

  var pieDefaults = {

    "header": {

      "title": {

        "text": "Object Types",

        "fontSize": 24,

        "font": "Calibri"

      },

      "subtitle": {

        "text": "Quantities of objects in modelspace.",

        "color": "#999999",

        "fontSize": 12,

        "font": "Calibri"

      },

      "titleSubtitlePadding": 9

    },

    "data": {

      // nothing initially

    },

    "footer": {

      "color": "#999999",

      "fontSize": 10,

      "font": "Calibri",

      "location": "bottom-left"

    },

    "size": {

      "canvasWidth": 400,

      "pieInnerR
adius"
: "49%",

      "pieOuterRadius": "81%"

    },

    "labels": {

      "outer": {

        "pieDistance": 32

      },

      "inner": {

        //"hideWhenLessThanPercentage": 3,

        "format": "value"

      },

      "mainLabel": {

        "fontSize": 11

      },

      "percentage": {

        "color": "#ffffff",

        "decimalPlaces": 0

      },

      "value": {

        "color": "#adadad",

        "fontSize": 11

      },

      "lines": {

        "enabled": true

      }

    },

    "effects": {

      "pullOutSegmentOnClick": {

        "effect": "linear",

        "speed": 400,

        "size": 8

      }

    },

    "misc": {

      "gradient": {

        "enabled": true,

        "percentage": 100

      }

    },

    "callbacks": {

      onClickSegment: clickPieWedge

    }

  };

 

  return pieDefaults;

}

And here's the C# code…

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows;

using Newtonsoft.Json.Linq;

using System;

using System.Linq;

using System.Runtime.InteropServices;

using System.Text;

 

namespace JavaScriptSamples

{

  public class ChartCommands

  {

    private PaletteSet _chps = null;

    private static Document _curDoc = null;

    private bool _refresh = false;

 

    [DllImport(

      "AcJsCoreStub.crx", CharSet = CharSet.Auto,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "acjsInvokeAsync")]

    extern static private int acjsInvokeAsync(

      string name, string jsonArgs

    );

 

    [CommandMethod("CHART")]

   
public void ChartPalette()

    {

      // We're storing the "launch document" as we're attaching

      // various event handlers to it

 

      _curDoc =

        Application.DocumentManager.MdiActiveDocument;

 

      // Only attach event handlers if the palette isn't already

      // there (in which case it will already have them)

 

      var attachHandlers = (_chps == null);

 

      _chps =

        Utils.ShowPalette(

          _chps,

          new Guid("F76509E7-25E4-4415-8C67-2E92118F3B84"),

          "CHART",

          "D3.js Examples",

          GetHtmlPathChart()

        );

 

      if (attachHandlers && _curDoc != null)

      {

        AddHandlers(_curDoc);

 

        Application.DocumentManager.DocumentActivated +=

          OnDocumentActivated;

 

        _curDoc.BeginDocumentClose +=

          (s, e) =>

          {

            RemoveHandlers(_curDoc);

            _curDoc = null;

          };

 

        // When the PaletteSet gets destroyed we remove

        // our event handlers

 

        _chps.PaletteSetDestroy += OnPaletteSetDestroy;

      }

    }

 

    [JavaScriptCallback("SelectObjectsOfType")]

    public string SelectObjectsOfType(string jsonArgs)

    {

      // Default result is an error

 

      var res = "{\"retCode\":1}";

 

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return res;

      var ed = doc.Editor;

 

      //ed.SetImpliedSelection(new ObjectId[]{});

 

      // Extract the DXF name to select from the JSON arguments

 

      var jo = JObject.Parse(jsonArgs);

      var dxfName = jo.Property("class").Value.ToString();

      var expanded = (bool)jo.Property("expanded").Value;

 

      // We'll select all the entities of this class

 

      var tvs =

        new TypedValue[] {

          new TypedValue((int)DxfCode.Start, dxfName)

        };

 

      // If the wedge is already expanded, we want to clear the

      // pickfirst set (so the default value is null)

 

      ObjectId[] ids = null;

      if (!expanded)

      {

    
60;   // Perform the selection

 

        var sf = new SelectionFilter(tvs);

        var psr = ed.SelectAll(sf);

        if (psr.Status != PromptStatus.OK)

          return res;

 

        // Get the results in our array

 

        ids = psr.Value.GetObjectIds();

      }

 

      // Set or clear the pickfirst selection

 

      ed.SetImpliedSelection(ids);

 

      // Set the focus on the main window for the update to display

      // (this works fine when floating, less well when docked)

 

      Application.MainWindow.Focus();

 

      // Return success

 

      return "{\"retCode\":0}";

    }

 

    [JavaScriptCallback("GetObjectCountData")]

    public string GetObjectData(string jsonArgs)

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return "{\"retCode\":1}";

 

      // Initialize the JSON string to return the count information

 

      var sb =

        new StringBuilder("{\"retCode\":0, \"result\":");

      sb.Append("{\"sortOrder\":\"value-desc\",\"content\":[");

 

      using (

        var tr = doc.TransactionManager.StartOpenCloseTransaction()

      )

      {

        bool first = true;

 

        var ms =

          (BlockTableRecord)tr.GetObject(

            SymbolUtilityServices.GetBlockModelSpaceId(doc.Database),

            OpenMode.ForRead

          );

 

        // Use LINQ to count the objects in the modelspace,

        // grouping the results by type (all done via ObjectIds,

        // no need to open the objects themselves)

 

        var q =

          from ObjectId o in ms

          group o by o.ObjectClass.DxfName into counts

          select new { Count = counts.Count(), Group = counts.Key };

 

        // Serialize the results out to JSON

 

        foreach (var i in q)

        {

          if (!first)

            sb.Append(",");

 

          first = false;

          sb.AppendFormat(

            "{{\"label\":\"{0}\",\"value\":{1}}}", i.Group, i.Count

          );

     
0;  }

        tr.Commit();

      }

      sb.Append("]}}");

 

      return sb.ToString();

    }

 

    private void OnDocumentActivated(

      object s, DocumentCollectionEventArgs e

    )

    {

      if (_chps != null && e.Document != _curDoc)

      {

        // We're going to monitor when objects get added and

        // erased. We'll use CommandEnded to refresh the

        // palette at most once per command (might also use

        // DocumentManager.DocumentLockModeWillChange)

 

        // The document is dead...

 

        RemoveHandlers(_curDoc);

 

        // ... long live the document!

 

        _curDoc = e.Document;

        AddHandlers(_curDoc);

 

        if (_curDoc != null)

        {

          // Refresh our palette by setting the flag and running

          // a command (could be any command, we've chosen REGEN)

 

          _refresh = true;

          _curDoc.SendStringToExecute(

            "_.REGEN ", false, false, false

          );

        }

        else

        {

          acjsInvokeAsync("refpie", "{}");

        }

      }

    }

 

    private void AddHandlers(Document doc)

    {

      if (doc != null)

      {

        if (doc.Database != null)

        {

          doc.Database.ObjectAppended += OnObjectAppended;

          doc.Database.ObjectErased += OnObjectErased;

        }

        doc.CommandEnded += OnCommandEnded;

      }

    }

 

    private void RemoveHandlers(Document doc)

    {

      if (doc != null)

      {

        if (doc.Database != null)

        {

          doc.Database.ObjectAppended -= OnObjectAppended;

          doc.Database.ObjectErased -= OnObjectErased;

        }

        doc.CommandEnded -= OnCommandEnded;

      }

    }

 

    private void OnObjectAppended(object s, ObjectEventArgs e)

    {

      _refresh = true;

    }

 

    private void OnObjectErased(object s, ObjectErasedEventArgs e)

    {

      _refresh = true;

    }

 

    private void OnCo
mmandEnded(object s, CommandEventArgs e)

    {

      // Invoke our JavaScript functions to refresh the palette

 

      if (_refresh && _chps != null)

      {

        acjsInvokeAsync("refpie", "{}");

        _refresh = false;

      }

    }

 

    private void OnPaletteSetDestroy(object s, EventArgs e)

    {

      // When our palette is closed, detach the various

      // event handlers

 

      if (_curDoc != null)

      {

        RemoveHandlers(_curDoc);

        _curDoc = null;

      }

    }

 

    private static Uri GetHtmlPathChart()

    {

      return new Uri(Utils.GetHtmlPath() + "chart.html");

    }

  }

}

I've actually found this to be quite a useful little sample: not just from the way it shows interactions from HTML5/JavaScript to .NET and back but also from a user perspective. If you want to quickly select all the objects of a particular type from a drawing – perhaps to change their layer or erase them, then this tool could be very handy. It's essentially a streamlined, graphical version of the QSELECT command.

22 responses to “Displaying a graph of AutoCAD drawing objects using JavaScript and .NET”

  1. Wasn't able to run it...are you missing any piece of code? The "Utils" class was easy to reproduce but there isn't any reference to "acadext2.js" file...

    1. Yes - you can delete that script ref (I folded the Shaping Layer functions into the other JS). You will need to get D3.js and d3pie.js, of course, or link to online versions of the libs). Should really have taken the time to host the complete version of the app on the blog (I'll post the full set of samples next week, I expect).

      Kean

      1. Yes, downloaded the other scripts. It loads, the DWG events are fired but the pie doesn't show. It seems the D3.min script is crashing from what I could see via F12 DevTools.

        1. Check it out...

      2. Ok, just got it...bad script names. As I downloaded the JavaScript files the correct names are:

        <script src="Autodesk.AutoCAD.js"></script>
        <script src="d3.min.js"></script>
        <script src="d3pie.js"></script>
        <script src="chart.js"></script>

        The download package has not "d3pie.min.js" file as you mentioned at your HTML code.

        1. Great you worked it out!

          Kean

  2. If only that pie chart was built into acad. Users would be able to see how much space app ids and civil 3d styles take up. Wait, its listing counts, not file size I think. Any way to calc file size splitup?

    1. Not without doing more number crunching at regular intervals (counting the number of each type is of course less work than counting/estimating objects' memory footprint).

      Kean

  3. I had it working. Anyway, the link keanw.com/... has empty source at the this time, users need to create their own files.

    I don't know how to get/set port number for JavaScript remote debugging. http://localhost:3864 does not work on my machine (tested in AutoCAD 2014).

    1. Glad you worked it out, too. Yes - I need to get all my AU samples hosted on my blog soon...

      I don't recall having used remote JS debugging (please do remind me if I've blogged about it and forgotten :-). Are you saying this worked in 2014?

      Kean

      1. An update to fix C# code. The JSON object received in C# does not match with JSON object requested from JavaScript. The "class" and "expanded" properties are children of "functionParams" property. Please see the below code to fix:

        // C# (fixed)
        var param = (JObject)jo.Property("functionParams").Value;
        var dxfName = param.Property("class").Value.ToString();
        var expanded = (bool)param.Property("expanded").Value;

        // JavaScript (original)
        function clickPieWedge(evt) {
        selectObjectsOfType(
        { "class": evt.data.label, "expanded": evt.expanded }
        );
        }

        function selectObjectsOfType(jsonArgs) {
        var jsonResponse = exec(
        JSON.stringify({
        functionName: 'SelectObjectsOfType',
        invokeAsCommand: false,
        functionParams: jsonArgs
        }));
        ...
        }

        1. Could you describe the behaviour for you when this fix isn't in place? The code works well on my side, so it may be I'm using a different version of the JS lib(s).

          Kean

          1. Hi Kean,

            The issue is when I click on the JavaScript chart to select all entities of a selected type on the screen, AutoCAD 2014 will shut down. The reason is jo.Property("class") is null.

            The requested JSON string was created in JavaScript by the code:

            JSON.stringify({ functionName: 'SelectObjectsOfType', invokeAsCommand: false, functionParams: "{ "class": evt.data.label, "expanded": evt.expanded }" })

            This JSON object from JavaScript has 3 properties "functionName", "invokeAsCommand", "functionParams". The property "functionParams" has 2 other child properties "class" and "expanded".

            This JSON string is transferred from JavaScript side to .NET side in AutoCAD, and the code in C# is:

            var jo = JObject.Parse(jsonArgs) with jsonArgs = {"functionName":"SelectObjectsOfType","invokeAsCommand":false,"functionParams":{"class":"CIRCLE","expanded":false}}

            From there, JSON Object jo has "class" as a nested property of "functionParams". Therefore, jo.Property("class") will return null and jo.Property("class").Value will bomb out AutoCAD.

            I did not test your original code in AutoCAD 2015, but my modified code works on AutoCAD 2014.

          2. Hi Kean,

            To confirm, your code works with AutoCAD 2015, but in 2014 the received JSON object in C# is different:

            AutoCAD 2015: {"class":"CIRCLE","expanded":false}

            AutoCAD 2014: {"functionName":"SelectObjectsOfType","invokeAsCommand":false,"functionParams":{"class":"CIRCLE","expanded":false}}

            AutoCAD 2015 is smarter than AutoCAD 2014 to exclude "functionName":"SelectObjectsOfType","invokeAsCommand":false out of the received JSON object, as they are redundant, and hide "functionParams" property. Only needed properties shown up in 2015, not everything in 2014.

            In conclusion, AutoCAD 2015 is different with AutoCAD 2014 in input parameter of JavaScriptCallback function.

            1. Yes, that's correct. AutoCAD 2014's JavaScript implementation was effectively a preview. I should have stated it explicitly in the post, but this code was developed for 2015, and you should really focus on the 2015 onwards for reliable JS support.

              Kean

              1. Thank you. I learned something different and improvement from 2015 vs 2014 JavaScript.

    2. I've just had a thought... have you tried hitting F12 with the dialog active? This should bring up the debugging tools with a (seemingly random) port number, as you've mentioned...

      Kean

      1. Hi Kean,

        I hit F12 in the active dialog (with canvas graphic in JavaScript) and no web browser popup shows ups. I am testing on AutoCAD P&ID 2014 in the office. I will test it in AutoCAD 2015 later at home.

        1. 2014 is different: there's a special registry key to create, etc. (the info is somewhere on my blog). 2015 is much easier, just hit F12.

          Kean

          1. Fenton had a blog about "Enabling AutoCAD 2014 JavaScript Debugging". But the remote DevTools URL is not available now (check at drawingfeed.visualta... )

      2. F12 works only on AutoCAD 2015 to show up the Developer Tools to debug JavaScript with an unused port number. In AutoCAD 2014, F12 key does not work.

        1. Exactly. Please see my earlier comment on 2014 vs. 2015.

          Kean

Leave a Reply to ffpm2018 Cancel reply

Your email address will not be published. Required fields are marked *