JavaScript in AutoCAD: Revision clouds using Paper.js

Having introduced this series, it's time to look at some code. This first sample shows how to create and host a web-page that uses an external graphics library – in our case Paper.js – within an AutoCAD application. The main "trick" to this is going to be getting the data from the HTML page into AutoCAD, which we'll do by extending AutoCAD's shaping layer. Bear in mind that this code will work with AutoCAD 2015, but I can't guarantee it will do so with 2014 (the JavaScript API was very much a "preview" in that release).

Something I should say, right off the bat, is that the samples you'll see in the coming posts each have different origins, whether being based on different SDK samples or just from me coding them at different times. So – despite some effort from my side – there will be inconsistencies: some use jQuery, some do not, some have JavaScript embedded in the HTML page while others only use external files.

The samples all use some common approaches – especially with the AutoCAD-resident code – but you shouldn't take my samples as "best practice" for working with HTML/JavaScript as much as demonstrations of possibilities.

As the main HTML/CSS/JavaScript code is all hosted on my blog – and can be accessed there by the application – let's start with the piece that you'll need to build into a local, AutoCAD-resident .NET DLL. You can also download the various files and place them in your DLL folder and load them locally by uncommenting a few lines of code. In fact, this is the main way I've been developing/testing this code, which is why I permitted myself to omit the code we saw in this previous post to check whether a URL is valid and loadable before trying to load the HTML page it points to.

Our C# code has ended up doing a lot more that I initially expected it to – please don't be put off by its length. (If the details don't interest you, please do scroll down to check out the embedded YouTube video with the code in action.)

It implements a couple of commands to load the web-page into an HTML-hosting palette or document (called PAPER and PAPDOC, respectively) and also defines a function that will be called from our JavaScript code – via an extension to the Shaping Layer, more on that later – with the Paper.js project encoded as a JSON string passed in as an argument.

This function – which is tagged with the JavaScriptCallback attribute making it callable from JavaScript – extracts the "path" data from the JSON file and generates corresponding Polyline entities that get added to a new block which gets inserted into the active document (or drawing document that launched the initial command it in the case where the HTML page is loaded into its own document window). A lot of the work was to make sure we create the block at the right size – both based on the extents of the paths we get from Paper.js but also the current zoom factor in AutoCAD – and that it's centered on the cursor. The INSERT command will let you scale and rotate, but I wanted the initial size and position to make some sense.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows;

using Newtonsoft.Json.Linq;

using System;

using System.IO;

using System.Reflection;

 

namespace JavaScriptExtender

{

  public class Commands

  {

    private PaletteSet _pps = null;

    private static Document _launchDoc = null;

    private static double _fac;

 

    [JavaScriptCallback("CreatePath")]

    public string CreatePath(string jsonArgs)

    {

      var dm = Application.DocumentManager;

      var doc = GetActiveDocument(dm);

 

      // If we didn't find a document, return

 

      if (doc == null)

        return "";

 

      // Lock the document, as we will be modifying it

 

      using (var dl = doc.LockDocument())

      {

        var db = doc.Database;

        var ed = doc.Editor;

 

        // Unescape the string and remove surrounding quotes

        // (could use a less manual approach for this, but hey)


0;

        var str = jsonArgs.Replace("\\\"", "\"");

        if (str.StartsWith("\""))

          str = str.Substring(1);

        if (str.EndsWith("\"\n"))

          str = str.Remove(str.Length - 2);

 

        // Parse the JSON to extract the info we want

 

        var a = JArray.Parse(str);

        var kids = a[0][1]["children"];

        if (kids == null)

          return "";

 

        // We'll collect Polylines in a collection

 

        var paths = new DBObjectCollection();

 

        // Aggregate the various vertices in a single point

        // that we divide by the number of points to get the

        // mean (the centroid of the shape)

 

        var totalPoints = 0;

        var total = Point3d.Origin;

 

        // Also collect the bounding box of the polyline

 

        var ext = new Extents3d();

 

        // Create our Polylines relative to WCS

        // (we'll transform them later, once we've

        // collected the centroid)

 

        var plane = new Plane(Point3d.Origin, Vector3d.ZAxis);

 

        var pathNum = 0;

        foreach (var path in kids)

        {

          // Get the next path

 

          var p = path[1];

 

          // We currently only care about the segments, but

          // could also use the other information

          // (linewidth, etc.)

 

          var segments = p["segments"];

          var width = p["strokeWidth"];

          var cap = p["strokeCap"];

          var join = p["strokeJoin"];

 

          var pl = new Polyline();

 

          var last = Point3d.Origin;

 

          var segNum = 0;

          foreach (var seg in segments)

          {

            // Points are grouped in sets of 3 (although the

            // first 3 are together and then the next 2 plus

            // one previous point make up the groups)

 

            if (segNum > 0 && segNum % 2 == 0)

            {

        
60;     // Get our 3 points - we don't care about the

              // tangent information as we'll just construct

              // GeArcs instead

 

              var pt1 = seg.Previous.Previous[0];

              var pt2 = seg.Previous[0];

              var pt3 = seg[0];

 

              // Create corresponding Point3d objects

 

              var start =

                new Point3d((double)pt1[0], -(double)pt1[1], 0);

              var mid =

                new Point3d((double)pt2[0], -(double)pt2[1], 0);

              var end =

                new Point3d((double)pt3[0], -(double)pt3[1], 0);

              last = end;

 

              // Include our vertices in the extents

              // (the end point is the start of the next -

              // we'll use the last variable to get the

              // last end point)

 

              ext.AddPoint(start);

              ext.AddPoint(mid);

 

              // Aggregate all the start points so we can get

              // the average at the end

 

              total = start + total.GetAsVector();

              totalPoints++;

 

              // Create a CircularArc3d

 

              var ca = new CircularArc3d(start, mid, end);

 

              // Calculate the bulge factor using it

 

              var b =

                Math.Tan(0.25 * (ca.EndAngle - ca.StartAngle)) *

                ca.Normal.Z;

 

              // Add our vertex

 

              pl.AddVertexAt(

                pl.NumberOfVertices,

                ca.StartPoint.Convert2d(plane),

                b, 0, 0

              );

            }

            segNum++;

          }

 

          // Add the final vertex

 

          pl.AddVertexAt(

            pl.NumberOfVertices, last.Convert2d(plane), 0, 0, 0

          );

 

          // Add the last vertex for completeness

 

          total = last + total.GetAsVector();

          totalPoints++;

 

          // Also to our extents object

 

          ext.AddPoint(last);

 

          // Add our Polyline to the path collection

 

          paths.Add(pl);

          pathNum++;

        }

 

        // Now we can get the average vertex to make that

        // the insertion point (i.e. the centroid)

 

        total = total / totalPoints;

 

        // We want the geometry to be created based on the

        // current zoom factor. So let's try to get that

        // from the Editor (which will fail in HTML document

        // mode, hence the try-catch block)

 

        double fac =

          _launchDoc == null ? GetCurrentZoomFactor(doc) : _fac;

 

        // Create a transformation to both displace the

        // geometry - to use our centroid as the insertion

        // point - and to scale the geometry to be nice and

        // small (the INSERT command allows scaling upwards)

 

        var totVec = total.GetAsVector();

        var diag = ext.MaxPoint - ext.MinPoint;

 

        var mat =

          Matrix3d.Scaling(fac / diag.Length, Point3d.Origin).

            PostMultiplyBy(Matrix3d.Displacement(-totVec));

 

        // If we have some Polylines, add them to our block

        // or the current space

 

        if (paths.Count > 0)

        {

          var blockRoot = "CLOUD";

          bool createAndInsert = !String.IsNullOrEmpty(blockRoot);

          string blockName = null;

 

          using (var tr = doc.TransactionManager.StartTransaction())

          {

            BlockTableRecord btr;

 

            // If we're creating and inserting, starting by

            // creating the blank block definition

 

            if (createAndInsert)

            {

              // Get the block table, initially for read

 

              var bt =

                (BlockTable)tr.GetObject(

                  db.BlockTableId, OpenMode.ForRead

                );

 

              // Loop until we have a valid (unused) block name

 

              var num = 0;

              do

              {

                blockName = String.Format(blockRoot + "{0}", num);

                num++;

              }

              while (bt.Has(blockName));

 

              // Create our block definition, opening the block

              // table for write in order to add it

 

              btr = new BlockTableRecord();

              btr.Name = blockName;

              bt.UpgradeOpen();

              bt.Add(btr);

              bt.DowngradeOpen();

              tr.AddNewlyCreatedDBObject(btr, true);

            }

            else

            {

              // If we're adding to the current space (rather than

              // a new block), just open it for write

 

              btr =

                (BlockTableRecord)tr.GetObject(

                  db.CurrentSpaceId, OpenMode.ForWrite

                );

            }

 

            // Loop through our collection, adding all the entities

            // (after transforming them for displacement and scale)

            // and disposing of anything else (should be nothing)

 

            foreach (DBObject o in paths)

            {

              var ent = o as Entity;

              if (ent != null)

              {

                ent.TransformBy(mat);

                btr.AppendEntity(ent);

                tr.AddNewlyCreatedDBObject(ent, true);

              }

              else

              {

                o.Dispose();

              }

            }

 

            // At this stage we're done with the drawing changes,

            // so commit the transaction

 

            tr.Commit();

          }

 

          // Our Editor-based code is wrapped in a try-catch

          // block as accessing the Editor will cause

          // an exception to be thrown in HTML document mode

 

          try

          {

            // Let's try to insert our block, if we created one

 

            if (createAndInsert && !String.IsNullOrEmpty(blockName))

            {

              // If running in our own document window,

              // activate the associated drawing first

 

              if (dm.MdiActiveDocument != doc)

                dm.MdiActiveDocument = doc;

 

              // We'll use SendStringToExecute() as it works

              // well for these purposes

 

              doc.SendStringToExecute(

                "_.-INSERT " + blockName + "\n", true, true, false

              );

            }

            else

            {

              // If we're adding to the current space, just

              // have the screen redraw to show the results

 

              ed.UpdateScreen();

            }

          }

          catch { }

        }

      }

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

    }

 

    private static Matrix3d Dcs2Wcs(AbstractViewTableRecord v)

    {

      return

        Matrix3d.Rotation(-v.ViewTwist, v.ViewDirection, v.Target) *

        Matrix3d.Displacement(v.Target - Point3d.Origin) *

        Matrix3d.PlaneToWorld(v.ViewDirection);

    }

 

    // Get the current amount of zoom - I probably shouldn't use

    // the term "zoom factor" as it tends to mean something else.

    // But it's a factor - in drawing units - that shows how

    // zoomed into a model we are

 

    private static double GetCurrentZoomFactor(Document doc)

    {

      var fac = 0.0;

      try

      {

        // Let's try to get the Editor - will fail if called from

        // an HTML document

 

        var ed = doc.Editor;

 

        // Get the view and a diagonal vector across the extents

 

        var vtr = ed.GetCurrentView();

        var vec = new Vector3d(vtr.Width, vtr.Height, 0);

 

        // Capture the visible extents in an object

 

        var screenExt = new Extents3d();

 

        // Get the centre of the screen in WCS and use it

        // with the diagonal vector to add the corners to the

        // extents object

 

        var ctr =

          new Point3d(vtr.CenterPoint.X, vtr.CenterPoint.Y, 0);

        var dcs = Dcs2Wcs(vtr);

        screenExt.AddPoint((ctr + 0.5 * vec).TransformBy(dcs));

        screenExt.AddPoint((ctr - 0.5 * vec).TransformBy(dcs));

 

        // Calculate the length of the diagonal in WCS

        // then return a tenth of that as our factor

 

        var diag2 =

          (screenExt.MaxPoint - screenExt.MinPoint).Length;

        fac = diag2 / 10;

      }

      catch { }

 

      return fac;

    }

 

    private Document GetActiveDocument(DocumentCollection dm)

    {

      // If we're called from an HTML document, the active

      // document may be null

 

      var doc = dm.MdiActiveDocument;

      if (doc == null)

      {

        doc = _launchDoc;

      }

      return doc;

    }

 

    [CommandMethod("PAPDOC")]

    public static void PaperDocument()

    {

      _launchDoc = Application.DocumentManager.MdiActiveDocument;

      _fac = GetCurrentZoomFactor(_launchDoc);

      _launchDoc.BeginDocumentClose +=

        (s, e) => { _launchDoc = null; };

 

      Application.DocumentWindowCollection.AddDocumentWindow(

        "Paper.js Document", GetHtmlPathPaper()

      );

    }

 

    [CommandMethod("PAPER")]

    public void PaperPalette()

    {

      _launchDoc = null;

 

      _pps =

        ShowPalette(

          _pps,

          new Guid("B0B176D0-6402-4D1A-8D44-4CB2EA8F29C0"),

          "PAPER",

          "Paper.js Example",

          GetHtmlPathPaper()

        );

    }

 

    // Helper function to show a palette

 

    private PaletteSet ShowPalette(

      PaletteSet ps, Guid guid, string cmd, string title, Uri uri

    )

    {

      if (ps == null)

      {

        ps = new PaletteSet(cmd, guid);

      }

      else

      {

        if (ps.Visible)

          return ps;

      }

 

      if (ps.Count != 0)

      {

        ps[0].PaletteSet.Remove(0);

      }

 

      ps.Add(title, uri);

      ps.Visible = true;

 

      return ps;

    }

 

    // Helper function to get the path to our HTML files

 

    private static string GetHtmlPath()

    {

      // Use this approach if loading the HTML from the same

      // location as your .NET module

 

      //var asm = Assembly.GetExecutingAssembly();

      //return Path.GetDirectoryName(asm.Location) + "\\";

 

      return
"/wp-content/uploads/files/";

    }

 

    private static Uri GetHtmlPathPaper()

    {

      return new Uri(GetHtmlPath() + "paperclouds.html");

    }

  }

}

Again, none of this is better than what we already have with the REVCLOUD command inside AutoCAD, but it does show the interoperability potential of an HTML/JavaScript app that generates geometry inside AutoCAD.

Here's the code in action, generating a few revision clouds both from an AutoCAD-based palette and a separate document window (using a sample drawing borrowed from here):

If you're interested in the code that's hosted on my blog and accessed from there, read on!

Here's the HTML page that uses Paper.js to draw our clouds: the "Paperscript" code that does this is incredibly compact. The function at the bottom is one you'll see in each of the samples in this series (and is a technique I borrowed from the Isomer samples) – it just goes through and generates a set of buttons dynamically based on the JavaScript "commands" that are in the code.

<!doctype html>

<html>

  <head>

    <meta charset="UTF-8">

    <title>Clouds</title>

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

    <script type="text/javascript" src="js/paper-full.min.js">

    </script>

    <script

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

    </script>

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

    <script type="text/paperscript" canvas="canvas">

      function init() {

        project.currentStyle = {

          strokeColor: 'black',

          strokeWidth: 5,

          strokeJoin: 'round',

          strokeCap: 'round'

        };

      }

 

      // The user has to drag the mouse at least 30pt before the

      // mouse drag event is fired:

      tool.minDistance = 30;

 

      var path;

      function onMouseDown(event) {

        path = new Path();

        path.add(event.point);

      }

 

      function onMouseDrag(event) {

        path.arcTo(event.point, true);

      }

 

      function Commands() {}

 

      Commands.clear = function () {

        project.activeLayer.removeChildren();

        var c = document.getElementById("canvas");

        var ctx = c.getContext("2d");

        ctx.clearRect(0, 0, c.width, c.height);

      };

 

      Commands.insert = function () {

        sendPathToAutoCAD(project.exportJSON());

      };

 

      (function () {

 

        init();

 

        var canvas = document.getElementById('canvas');

        canvas.width = window.innerWidth - 25,

        canvas.height = window.innerHeight - 65;

 

        var panel = document.getElementById('control');

        for (var fn in Commands) {

          var button = document.createElement('div');

          button.classList.add('cmd-btn');

          button.innerHTML = fn;

          button.onclick = (function (fn) {

            return function () { fn(); };

          })(Commands[fn]);

 

          panel.appendChild(button);

          panel.appendChild(document.createTextNode('\u00a0'));

        }

      })();

 

    </script>

  </head>

  <body>

    <canvas id="canvas"></canvas>

    <div id="control"></div>

  </body>

</html>

Here's the extension to the Shaping Layer that is called from this code, which just passes through the provided JSON directly as an argument (and in reality won't care very much about the result).

function sendPathToAutoCAD(jsonArgs) {

  var jsonResponse =

    exec(

      JSON.stringify({

        functionName: 'CreatePath',

        invokeAsCommand: false,

        functionParams: jsonArgs

      })

    );

  var jsonObj = JSON.parse(jsonResponse);

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

    throw Error(jsonObj.retErrorString);

  }

  return jsonObj.result;

}

And for completeness, here's the CSS that (mainly) shows the nice buttons at the bottom:

#canvas {

  display: block;

  margin: 0 auto;

}

 

#control {

  position: absolute;

}

 

.cmd-btn {

  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;

  font-size: 24px;

  border: 3px solid #555;

  border-radius: 5px;

  color: #555;

  background: transparent;

  text-transform: uppercase;

  padding: 15px;

  width: 120px;

  text-align: center;

  display: inline

}

 

.cmd-btn:hover {

  cursor: pointer;

  color: #fff;

  background-color: #555;

}

That's it for the initial Paper.js and AutoCAD integration. At some point I'd quite li
ke to see investigate taking 2D geometry from AutoCAD and manipulating it using Paper.js (it has some nice path simplification capabilities for hand-drawn curves, for instance).

But next up is a look at integrating Isomer to display 2D isometric views of 3D AutoCAD geometry.

2 responses to “JavaScript in AutoCAD: Revision clouds using Paper.js”

  1. HI Sir,

    Nice to see the article. nice to hear your voice in the video.

    It has been long time since I left ADSK, Is everything going well? How about the kids, they are now big guys and girls.

    Recently I also worked with Three.js, is it possible to view the DWG file via three.js without AutoCAD installed? I did some research, it seems that there are some webservice to view the file without ActiveX. I want to view the file based on WEBGL directly. Looking forward to your suggestion.

    Please help to see hello to the kids, and if you have the chance to China, please let me know.

    Yours Sincerely,

    Hongxian QIN

    1. Hi Hongxian,

      Nice to hear from you! All is well at this end, thanks.

      Take a look at the View & Data API: developer.autodesk.com. This allows you to view DWGs - as well as 60 or so other CAD formats - in a zero-client HTML viewer based on Three.js and therefore (of course) WebGL. It's the viewing technology that drives our A360 viewer.

      All the best to you and the family,

      Kean

Leave a Reply to Hongxian Qin Cancel reply

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