Implementing an AutoCAD palette using HTML5 and JavaScript

After introducing the new JavaScript API as one of the new features in AutoCAD 2014, in the last post we looked at a simple command defined using JavaScript. In this post, we're going to implement a simple, palette-based UI inside AutoCAD using HTML5 and JavaScript.

Let's start by looking at the HTML code (with the JavaScript embedded, for simplicity):

<html>

  <head>

    <title>Draw a transient circle or rectangle</title>

    <script

      type="text/javascript"

      src="http://www.autocadws.com/jsapi/v1/Autodesk.AutoCAD.js">

    </script>

    <script type="text/javascript">

 

      // Some global variables

 

      // The cursor for "OnMouseOver", as specified in the UI

 

      var cursor = ' ';

 

      // Colors for our transient definition, as they are

      // presented to the user in the UI

 

      var colors = new Array();

      colors['Red'] = "#ff0000";

      colors['Yellow'] = "#ffff00";

      colors['Green'] = "#00ff00";

      colors['Cyan'] = "#00ffff";

      colors['Blue'] = "#0000ff";

      colors['Magenta'] = "#ff00ff";

      colors['White'] = "#ffffff";

 

      // Some point information, as populated by user prompting

 

      var pt0 = new Array();

      var pt1 = new Array();

      var pt2 = new Array();

      var pt3 = new Array();

 

      // A function to create the XML definition for the

      // transient rectangular polyline we want to create

 

      function createPolyline() {

 

        // Get some parameters from the page

 

        var color = document.getElementById('ColCB').value;

        var linetype = document.getElementById('LTCB').value;

        var lineweight = document.getElementById('LWCB').value;

        var filled = document.getElementById('Filled').checked;

        var cursorType = document.getElementById('Cursor').value;

        if (cursorType == 'None')

          cursor = ' ';

        else

          cursor = ' cursor="' + cursorType + '"';

 

        // Set other rectangle corners based on the picked corners

 

        pt1[0] = pt0[0];

        pt1[1] = pt2[1];

        pt1[2] = pt0[2];

 

        pt3[0] = pt2[0];

        pt3[1] = pt0[1];

        pt3[2] = pt0[2];

 

        var drawable =

          '<?xml version="1.0" encoding="utf-8"?> \

          <!-- the event handler will receive the id--> \

          <drawable \

            xmlns="http://www.autodesk.com/AutoCAD/drawstream.xsd"\

            xmlns:t="http://www.autodesk.com/AutoCAD/transient.xsd"\

            t:onmouseover ="onmouseover"'

            +

            cursor

            +

          '>\

            <graphics color="' + colors[color] +

              '" id="id1" lineweight="' + lineweight +

              '" linetype="' + linetype +

              '" filled="' + filled + '">\

              <polyline isClosed="true">\

                <vertices>\

                  <vertex>' + pt0.toString() + '</vertex>\

                  <vertex>' + pt1.toString() + '</vertex>\

                  <vertex>' + pt2.toString() + '</vertex>\

                  <vertex>' + pt3.toString() + '</vertex>\

                </vertices>\

              </polyline>\

            </graphics>\

          </drawable>';

 

        return drawable;

      }

 

      // A function to create the XML definition for the

      // transient circle we want to create

 

      function createCircle() {

 

        // Get some parameters from the page

 

        var color = document.getElementById('ColCB').value;

        var linetype = document.getElementById('LTCB').value;

        var lineweight = document.getElementById('LWCB').value;

        var filled = document.getElementById('Filled').checked;

        var cursorType = document.getElementById('Cursor').value;

        if (cursorType == 'None')

          cursor = ' ';

        else

          cursor = ' cursor="' + cursorType + '"';

 

        // Use Pythagoras' theorem to get the radius of the circle

        // based on the points selected

 

        var radius =

          Math.sqrt(

            Math.pow(pt2[0] - pt0[0], 2) +

            Math.pow(pt2[1] - pt0[1], 2)

          );

 

        var drawable =

          '<?xml version="1.0" encoding="utf-8"?> \

          <!-- the event handler will receive the id--> \

          <drawable \

            xmlns="http://www.autodesk.com/AutoCAD/drawstream.xsd"\

            xmlns:t="http://www.autodesk.com/AutoCAD/transient.xsd"\

            t:onmouseover ="onmouseover"'

            +

            cursor

            +

          '>\

            <graphics color="' + colors[color] +

              '" id="id1" lineweight="' + lineweight +

              '" linetype="' + linetype +

              '" filled="' + filled + '">\

              <circle center ="' + pt0.toString() +

                '" radius ="' + radius.toString() + '"/>\

            </graphics>\

          </drawable>';

 

        return drawable;

      }

 

      // "Command" to draw a rectangular transient

 

      function drawRectangularTransient() {

 

        function on1stComplete(args) {

 

          var res = JSON.parse(args);

          if (res && res.value) {

            pt0[0] = res.value.x;

            pt0[1] = res.value.y;

            pt0[2] = res.value.z;

 

            // Once we have the first point, get the other corner

 

            var pco =

              new Acad.PromptCornerOptions(

                'Pick second corner point',

                new Acad.Point3d(pt0[0], pt0[1], pt0[2])

              );

            Acad.Editor.getCorner(pco).then(on2ndComplete, onError);

          }

        }

 

        function on2ndComplete(args) {

 

          var res = JSON.parse(args);

          if (res && res.value) {

            pt2[0] = res.value.x;

            pt2[1] = res.value.y;

            pt2[2] = res.value.z;

 

            // And when we have both first and second,

            // we can generate the XML for the transient

            // and ask for it to be drawn

 

            var doc = Acad.Application.activedocument;

            var drawable = createPolyline();

            var tran = new Acad.Transient();

            doc.transientManager.addTransient(tran, drawable);

          }

        }

 

        function onError(args) {

          alert('Unable to create rectangular transient: ' + args);

        }

 

        // The body of our function, where we get the first point

 

        var ppo =

          new Acad.PromptPointOptions(

            'Pick first corner point',

            new Acad.Point3d(0, 0, 0)

          );

        Acad.Editor.getPoint(ppo).then(on1stComplete, onError);

      }

 

      // "Command" to draw a circular transient

 

      function drawCircularTransient() {

 

        function on1stComplete(args) {

 

          var res = JSON.parse(args);

          if (res && res.value) {

            pt0[0] = res.value.x;

            pt0[1] = res.value.y;

            pt0[2] = res.value.z;

 

            // Once we have the center point, get one on the

            // circumference

 

            var ppo =

              new Acad.PromptPointOptions('Pick point on circle');

            ppo.useBasePoint = true;

            ppo.basePoint =

              new Acad.Point3d(pt0[0], pt0[1], pt0[2]);

            Acad.Editor.getPoint(ppo).then(on2ndComplete, onError);

          }

        }

 

        function on2ndComplete(args) {

 

          var res = JSON.parse(args);

          if (res && res.value) {

            pt2[0] = res.value.x;

            pt2[1] = res.value.y;

            pt2[2] = res.value.z;

 

            // And when we have both first and second,

            // we can generate the XML for the transient

            // and ask for it to be drawn

 

            var doc = Acad.Application.activedocument;

            var drawable = createCircle();

            var tran = new Acad.Transient();

            doc.transientManager.addTransient(tran, drawable);

          }

        }

 

        function onError(args) {

          alert('Unable to create circle transient: ' + args);

        }

 

        // The body of our function, where we get the first point

 

        var ppo =

          new Acad.PromptPointOptions(

            'Pick center point',

            new Acad.Point3d(0, 0, 0)

          );

 

        Acad.Editor.getPoint(ppo).then(on1stComplete, onError);

      }

    </script>

 

    <style type="text/css">

      td, body

        { font-family: sans-serif; font-size: 10pt; }

      body

        { background-color: #686868;

          padding:0;

          margin:5px 5px 5px 5px;

          color:#FFF; }

      textarea

        { font-family: Consolas; font-size: 8pt; } 

    </style>

  </head>

  <body>

    <!-- Create a tabular UI with headings and comboboxes -->

    <h3>Transient circle or rectangle</h3>

    <table border="0">

      <tr>

        <td align='right'>Color</td>

        <td>

          <select id='ColCB'>

            <option selected="selected">Red</option>

            <option>Yellow</option>

            <option>Green</option>

            <option>Cyan</option>

            <option>Blue</option>

            <option>Magenta</option>

            <option>White</option>

          </select>

        </td>

      </tr>

      <tr>

        <td align='right'>LineType</td>

        <td>

          <select id='LTCB'>

            <option selected="selected">LineTypeSolid</option>

            <option>Dashed</option>

            <option>Dotted</option>

            <option>Dash_Dot</option>

            <option>Short_Dash</option>

            <option>Medium_Dash</option>

            <option>Long_Dash</option>

            <option>Short_Dash_X2</option>

            <option>Medium_Dash_X2</option>

            <option>Long_Dash_X2</option>

            <option>Medium_Long_Dash</option>

            <option>Medium_Dash_Short_Dash_Short_Dash</option>

            <option>Long_Dash_Short_Dash</option>

            <option>Long_Dash_Dot_Dot</option>

            <option>Long_Dash_Dot</option>

            <option>Medium_Dash_Dot_Short_Dash_Dot</option>

            <option>Sparse_Dot</option>

            <option>Solid_6_Pixels_Blank_6_Pixels</option>

          </select>

        </td>

      </tr>

      <tr>

        <td align='right'>LineWeight</td>

        <td>

          <select id='LWCB'>

            <option>0</option>

            <option>5</option>

            <option>9</option>

            <option>13</option>

            <option>15</option>

            <option>18</option>

            <option>20</option>

            <option>25</option>

            <option selected="selected">30</option>

            <option>35</option>

            <option>40</option>

            <option>50</option>

            <option>53</option>

            <option>60</option>

            <option>70</option>

            <option>80</option>

            <option>90</option>

            <option>100</option>

            <option>106</option>

            <option>120</option>

            <option>140</option>

            <option>158</option>

            <option>200</option>

            <option>211</option>

          </select>

        </td>

      </tr>

      <tr>

        <td align='right'>Filled</td>

        <td><input type='checkbox' id='Filled'/></td>

      </tr>

      <tr>

        <td align='right'>Cursor</td>

        <td>

          <select

            id='Cursor'

            title='Cursor to be displayed on hover'>

            <option selected="selected">None</option>

            <option>None</option>

            <option>Arrow</option>

            <option>Ibeam</option>

            <option>Wait</option>

            <option>Cross</option>

            <option>UpArrow</option>

            <option>SizeNWSE</option>

            <option>SizeNESW</option>

            <option>SizeWE</option>

            <option>SizeNS</option>

            <option>SizeAll</option>

            <option>No</option>

            <option>Hand</option>

            <option>AppStarting</option>

            <option>Help</option>

          </select>

        </td>

      </tr>

      <tr>

        <th>&nbsp;</th>

        <td>

          <input

            type='button'

            style="width: 220px"

            onclick='drawRectangularTransient()'

            value='Draw Rectangle Transient' />

        </td>

      </tr>

      <tr>

        <th>&nbsp;</th>

        <td>

          <input

            type='button'

            style="width: 220px"

            onclick='drawCircularTransient()'

            value='Draw Circle Transient' />

        </td>

      </tr>

    </table>

  </body>

</html>

Some notes on the implementation…

  • As mentioned last time, we use callbacks to deal with code continuation on completion of selection events.
    • The two "commands" (they're not really commands in the AutoCAD sense – they're functions called when buttons are clicked on the palette) each have two selection events: for circle creation to pick the center and a point at the radius, and for rectangle creation to pick the two corners. You can already see the code starts to look a lot less linear because of this, but there you go: that's JavaScript for you.
  • The various selection methods set global state with the results.
    • There may well be a better way of doing this – I picked up much of this part of the implementation from some internal unit test samples – but this way works, at least.
  • You'll notice we create XML fragments to generate transient graphics, essentially via a declarative API that allows us to (among other things) assign HTML DOM-like event handlers such as onmouseover.

As mentioned when we first looked at the JavaScript API, we need to have a simple loader module that adds this palette into an AutoCAD-resident PaletteSet. Here's as simple piece of C# code for this:

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows;

using System;

 

namespace JavaScriptLoader

{

  public class Commands

  {

    static PaletteSet _ps = null;

 

    [CommandMethod("JSP")]

    public void JavaScriptPalette()

    {

      if (_ps == null)

      {

        _ps = new PaletteSet(

          "JavaScript Demo",

          new Guid("61135FFD-EE9A-4A5D-B3E1-993AB17E93BD")

        );

      }

 

      if (_ps.Count != 0)

      {

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

      }

 

      var p =

        _ps.Add(

          "JavaScript Test",

          new Uri(

            "/wp-content/uploads/files/" +

            "TransientPalette.html"

          )

        );

 

      _ps.Visible = true;

    }

  }

}

Here's a screenshot of the palette (that I've gone ahead and undocked) with the results of clicking on the "Draw Circle/Rectangle Transient" buttons a few times with various options set, selecting various points.

The HTML palette driving transient graphics creation

Some good news: I did find a way to invalidate the internal browser cache between AutoCAD sessions, to force a reload from the web-site when loading a page (whether WEBLOADing a .js file or using PaletteSet.Add() or one of the other new methods to load an HTML file). I deleted the contents of the following folder, which seemed to do the trick (and should be safe as they're stored beneath the temp folder): "C:\Users\username\AppData\Local\Temp\cache"

I still need to check in on the situation with UNDO: for now the transient graphics you create can either be removed programmatically (something I've kindly omitted from this sample 😉 or you can close the current drawing.

I expect my next post on the JavaScript API is going to focus more on some of the implementation details (such as the choice of browser control, the out-of-process architecture, etc.). I've so far avoided such details as I wanted to show some concrete examples beforehand.

I'm now going offline for the long Easter weekend (both Friday and Monday are holidays, here in Switzerland). I have a post queued up already for Monday, though, so do check back then.

8 responses to “Implementing an AutoCAD palette using HTML5 and JavaScript”

  1. Hi Kean,
    What is the html/js engine used by AutoCAD? is it webkit or Chrominium based?
    I've developed a while ago a feature in my application that injects COM objects in HTML based dialogs (IE activex based 🙁 ) allowing js code to interact with my application. This feature was pretty cool, it brought power and simplicity of HTML/JS into my application, it even allowed a colleague - which originally had only a few skills in programming but was quite comfortable with HTML - to develop some very interesting features.
    I guess the Acad JS API will open Acad development to new profile of programmers, that's very nice!
    I however ask for the HTML engine used because we experienced a lot of issues regarding IE versions -as web developer are used to deal with- mainly CSS issues but also some more frustrating js or activex issues.
    As AutoCAD now exists on OSX platforms, I guess IE engine has not been envisaged. So, I hope I'll be able to migrate my Com-based code to the new API (probably using .net), this would solve the issues we got with IE, unfortunately, I read your post on "AutoCAD 2014 for developers" and it seems the only way to extend the JS API is .net JavaScriptCallback or c++ acjsDefun(), not sure this allows me to inject full objects definitions into the JS engine.

    Loic

  2. Hi Loic,

    We went with Chromium (which is itself WebKit-based, I believe), as its JavaScript engine (V8) is extensible.

    I'll post more details soon, but hopefully that's enough for now. We'll have to see whether the extensibility model meets your specific (and no doubt fairly advanced) needs.

    Regards,

    Kean

  3. Hi Kean,

    Thank you for your answer

    I hope V8 extensibility model will be somewhat exposed, I just can't wait to work with such a cool engine!

    Cheers,
    Loic

  4. I am trying to use the browser storage for some user settings in my palette. But when AutoCad is closed and reopened the data is gone. Is there a way to persist between restarts?

    1. Kean Walmsley Avatar

      An excellent question... I haven't tried storing state in this way. Some thoughts... I'd probably see if I could save/retrieve cookies, failing that I'd create a .NET function that could be called from JS: this would certainly have more flexibility to store and retrieve state.

      Kean

      1. Thanks for responding Kean. I went straight to calling a .net function from JS. Its early but I believe this will work for me. Not sure if cookies can be saved or retrieved yet.

  5. We have a requirement of developing a DWG file editor where we can create a fresh Drawing, read existing DWG files, Edit it with custom tool items in tool bar and create a DWG design. Then the design output can be extracted as PDF or Image file or Drawing file
    How Autodesk gonna he le us?
    And what is the licensing cost .

  6. Can AutoCAD JS libraries be integrated with our ASP.NE Core webpages and provide us a look and feel of Online-editor where we can use our own custom items in toolbar and design and read DWG and other drawing formats?

Leave a Reply to Loic Cancel reply

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