Through the Interface

April 2015

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    



Twitter





March 11, 2015

Adding support for global keywords at AutoCAD’s command-line using .NET – Part 2

Today we’re going to look at the implementation talked about in the last post: we’re going to see how it’s possible to use the Application.PreTranslateMessage() method to hack AutoCAD’s message-loop and basically convert typed keywords into global ones.

This is actually pretty neat (yes, even if I do say so myself :-) and frankly I’m surprised it works. Here’s the overall approach:

  • Track the characters typed into the command-line
    • Add individual characters into a list
    • Backspace removes the tail of the list
    • Arrow-keys invalidate the tracking: if the user accesses entries in the command-history we can’t deal with that, and even navigating left and right along the typed text is tricky
  • When we encounter a termination character – enter or space – the fun really starts…
    • We check the entered string to see whether it matches any in our global keywords list. If it doesn’t match any – or it matches too many – then we let the keyword get processed as normal (which should either result in a local keyword being interpreted or an error)
    • If we find a single keyword match, we swallow the “keydown” message – setting e.Handled to true – which means the enter/space won’t be processed
    • We create a string consisting of the right number of backspace characters (ASCII 8) to erase the existing keyword and append an underscore and the typed keyword
    • We use Document.SendStringToExecute() to send this to the command-line

I didn’t find AutoCAD’s keyword matching code accessible via an API, so I did my best to replicate it. I don’t really like doing that, but it was actually quite fun to think through how AutoCAD detects keywords. I could well have missed edge cases, though – if you come across any strange behaviour, please let me know!


CmdLineHelper

Looking at the above recording – recorded using the French version of AutoCAD 2015 – there are some important points to note: we’re only using global commands and keywords entered via the command-line (as opposed to clicking from the menu, as we saw last time). When a keyword matches one in the global list (such as “arc” or “line” during the PLINE command), an underscore gets prefixed directly in the command-line.

Here’s the complete project for you to play with and here’s the C# source file containing the implementation described above:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using System.Collections.Generic;

using System.Collections.Specialized;

using System.Text.RegularExpressions;

 

namespace CmdLineHelper

{

  public class KeywordCommands

  {

    // The keyword display window

 

    private KeywordWindow _window = null;

 

    // We will store the "core" set of keywords for when we have

    // nested keywords that need to be appended

 

    private KeywordCollection _coreKeywords = null;

 

    // Keystrokes to recreate the commands entered

 

    private List<char> _keystrokes = null;

 

    // Flag for whether we're tracking keystrokes or not

 

    private bool _tracking = false;

 

    // The previous value of DYNMODE, which we override to 0

    // during keyword display

 

    private int _dynmode = 0;

 

    // List of "special" commands that need a timer to reset

    // the keyword list

 

    private readonly string[] specialCmds = { "MTEXT" };

 

    // Constants for our keystroke interpretation code

 

    private const int WM_KEYDOWN = 256;

    private const int WM_KEYUP = 257;

    private const int WM_CHAR = 258;

 

    // 37 - left arrow (no char, keydown/up)

    // 38 - up arrow (no char, keydown/up)

    // 39 - right arrow (no char, keydown/up)

    // 40 - down arrow (no char, keydown/up)

    // 46 - delete (no char, keydown/up)

 

    private static readonly List<int> cancelKeys =

      new List<int> { 37, 38, 39, 40, 46 };

 

    // 13 - enter (char + keydown/up)

    // 32 - space (char + keydown/up)

 

    private static readonly List<int> enterKeys =

      new List<int> { 13, 32 };

 

    [CommandMethod("KWS")]

    public void KeywordTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      if (_window == null)

      {

        _window = new KeywordWindow(Application.MainWindow.Handle);

        _window.Show();

        Application.MainWindow.Focus();

 

        // Add our various event handlers

 

        // For displaying the keyword list...

 

        ed.PromptingForAngle += OnPromptingForAngle;

        ed.PromptingForCorner += OnPromptingForCorner;

        ed.PromptingForDistance += OnPromptingForDistance;

        ed.PromptingForDouble += OnPromptingForDouble;

        ed.PromptingForEntity += OnPromptingForEntity;

        ed.PromptingForInteger += OnPromptingForInteger;

        ed.PromptingForKeyword += OnPromptingForKeyword;

        ed.PromptingForNestedEntity += OnPromptingForNestedEntity;

        ed.PromptingForPoint += OnPromptingForPoint;

        ed.PromptingForSelection += OnPromptingForSelection;

        ed.PromptingForString += OnPromptingForString;

 

        // ... and removing it

 

        doc.CommandWillStart += OnCommandEnded;

        doc.CommandEnded += OnCommandEnded;

        doc.CommandCancelled += OnCommandEnded;

        doc.CommandFailed += OnCommandEnded;

 

        ed.EnteringQuiescentState += OnEnteringQuiescentState;

 

        // We'll also watch keystrokes, to see when global keywords

        // are entered

 

        Application.PreTranslateMessage += OnPreTranslateMessage;

 

        _keystrokes = new List<char>();

 

        // We need to turn off dynamic input: we'll reset the value

        // when we unload or in KWSX

 

        _dynmode = (short)Application.GetSystemVariable("DYNMODE");

        if (_dynmode != 0)

        {

          Application.SetSystemVariable("DYNMODE", 0);

          ed.WriteMessage(

            "\nDynamic input has been disabled and can be re-enabled"

            + " by the KWSX command."

          );

        }

        ed.WriteMessage(

          "\nGlobal keyword dialog enabled. Run KWSX to turn it off."

        );

      }

      else

      {

        ed.WriteMessage(

          "\nGlobal keyword dialog already enabled."

        );

      }

    }

 

    [CommandMethod("KWSX")]

    public void StopKeywordTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      if (_window == null)

      {

        // This means KWS hasn't been called...

 

        ed.WriteMessage(

          "\nGlobal keyword dialog already disabled."

        );

 

        return;

      }

      else

      {

        _window.Hide();

        _window = null;

      }

 

      // Remove our various event handlers

 

      // For displaying the keyword list...

 

      ed.PromptingForAngle -= OnPromptingForAngle;

      ed.PromptingForCorner -= OnPromptingForCorner;

      ed.PromptingForDistance -= OnPromptingForDistance;

      ed.PromptingForDouble -= OnPromptingForDouble;

      ed.PromptingForEntity -= OnPromptingForEntity;

      ed.PromptingForInteger -= OnPromptingForInteger;

      ed.PromptingForKeyword -= OnPromptingForKeyword;

      ed.PromptingForNestedEntity -= OnPromptingForNestedEntity;

      ed.PromptingForPoint -= OnPromptingForPoint;

      ed.PromptingForSelection -= OnPromptingForSelection;

      ed.PromptingForString -= OnPromptingForString;

 

      // ... and removing it

 

      doc.CommandWillStart -= OnCommandEnded;

      doc.CommandEnded -= OnCommandEnded;

      doc.CommandCancelled -= OnCommandEnded;

      doc.CommandFailed -= OnCommandEnded;

 

      ed.EnteringQuiescentState -= OnEnteringQuiescentState;

 

      Application.PreTranslateMessage -= OnPreTranslateMessage;

 

      Application.SetSystemVariable("DYNMODE", _dynmode);

 

      ed.WriteMessage(

        "\nGlobal keyword dialog disabled. Run KWS to turn it on."

      );

    }

 

    // Event handlers to display the keyword list

    // (each of these handlers needs a separate function due to the

    // signature, but they all do the same thing)

 

    private void OnPromptingForAngle(

      object sender, PromptAngleOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForCorner(

      object sender, PromptPointOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForDistance(

      object sender, PromptDistanceOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForDouble(

      object sender, PromptDoubleOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForEntity(

      object sender, PromptEntityOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForInteger(

      object sender, PromptIntegerOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForKeyword(

      object sender, PromptKeywordOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForNestedEntity(

      object sender, PromptNestedEntityOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForPoint(

      object sender, PromptPointOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForSelection(

      object sender, PromptSelectionOptionsEventArgs e

    )

    {

      // Nested selection sometimes happens (e.g. the HATCH command)

      // so only display keywords when there are some to display

 

      if (e.Options.Keywords.Count > 0)

        DisplayKeywords(e.Options.Keywords, true);

    }

 

    private void OnPromptingForString(

      object sender, PromptStringOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnCommandWillStart(

      object sender, CommandEventArgs e

    )

    {

      HideKeywords();

    }

 

    private void OnCommandEnded(object sender, CommandEventArgs e)

    {

      HideKeywords();

    }

 

    // Event handlers to clear & hide the keyword list

 

    private void OnEnteringQuiescentState(object sender, EventArgs e)

    {

      HideKeywords();

    }

 

    private void OnPreTranslateMessage(

      object sender, PreTranslateMessageEventArgs e

    )

    {

      if (_tracking)

      {

        // Use of the arrow keys or delete kills our tracking

 

        var wp = e.Message.wParam.ToInt32();

        if (

          e.Message.message == WM_KEYDOWN && cancelKeys.Contains(wp)

        )

        {

          _tracking = false;

        }

        else if (

          e.Message.message == WM_KEYDOWN && enterKeys.Contains(wp)

        )

        {

          // Get our characters and then clear the list

 

          var chars = _keystrokes.ToArray();

          _keystrokes.Clear();

 

          // If the keyword list contains our string, send it

          // with a prefix of backspaces (to erase the prior

          // characters) and an underscore

 

          var kw = new string(chars);

          if (_window.ContainsKeyword(kw))

          {

            e.Handled = true;

            LaunchCommand(kw, kw.Length, true);

          }

        }

        else if (e.Message.message == WM_CHAR)

        {

          // If we have a backspace character, remove the last

          // entry in our character list, otherwise add the

          // character to the list

 

          if (wp == 8) // Backspace

          {

            if (_keystrokes.Count > 0)

              _keystrokes.RemoveAt(_keystrokes.Count - 1);

          }

          else if (ValidCharacter(wp)) // Normal character

          {

            _keystrokes.Add((char)wp);

          }

        }

      }

    }

 

    // Helper to display our keyword list

 

    private void DisplayKeywords(

      KeywordCollection kws, bool append = false

    )

    {

      if (!append)

      {

        _coreKeywords = kws;

      }

 

      // First we step through the keywords, collecting those

      // we want to display in a collection

 

      var sc = new StringCollection();

      if (append)

      {

        sc.AddRange(ExtractKeywords(_coreKeywords));

      }

      sc.AddRange(ExtractKeywords(kws));

 

      // If we don't have keywords to display, make sure the

      // current list is cleared/hidden

 

      if (sc.Count == 0)

      {

        _window.ClearKeywords(true);

      }

      else

      {

        // Otherwise we pass the keywords - as a string array -

        // to the display function along with a flag indicating

        // whether the current command is considered "special"

 

        var sa = new string[sc.Count];

        sc.CopyTo(sa, 0);

 

        // We should probably check for transparent/nested

        // command invocation...

 

        var cmd =

          (string)Application.GetSystemVariable("CMDNAMES");

        _window.ShowKeywords(

          sa, Array.IndexOf(specialCmds, cmd) >= 0

        );

 

        //Application.MainWindow.Focus();

 

        // Start tracking keyword keystrokes

 

        _tracking = true;

      }

    }

 

    private string[] ExtractKeywords(KeywordCollection kws)

    {

      var sc = new List<string>();

      if (kws != null && kws.Count > 0)

      {

        foreach (Keyword kw in kws)

        {

          if (kw.Enabled && kw.Visible && kw.GlobalName != "dummy")

          {

            sc.Add(kw.LocalName); // Expected this to be GlobalName

          }

        }

      }

      return sc.ToArray();

    }

 

    private void HideKeywords()

    {

      _keystrokes.Clear();

      _tracking = false;

      _window.ClearKeywords(true);

    }

 

    internal static void GiveAutoCADFocus()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc != null)

        doc.Window.Focus();

      else

        Application.MainWindow.Focus();

    }

 

    internal static void LaunchCommand(

      string cmd, int numBspaces, bool terminate

    )

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      doc.SendStringToExecute(

        Backspaces(numBspaces) + "_" + cmd + (terminate ? " " : ""),

        true, false, true

      );

 

      GiveAutoCADFocus();

    }

 

    private static string Backspaces(int n)

    {

      return new String((char)8, n);

    }

 

    private static bool ValidCharacter(int c)

    {

      var r = new Regex("^[a-zA-Z0-9]$");

      return r.IsMatch(Char.ToString((char)c));

    }

 

    internal static bool KeywordsMatch(string typed, string keyword)

    {

      if (Match(typed, keyword))

        return true;

 

      // Find the index of the first uppercase character in

      // the keyword being matched against

 

      var chars = new List<char>(keyword.ToCharArray());

      var upp = chars.Find(c => Char.IsUpper(c));

      var nth = keyword.IndexOf(upp);

 

      // Perform a similar check as the first one, this time

      // starting with the first uppercase character

 

      return

        nth <= 0 ? false : Match(typed, keyword.Substring(nth));

    }

 

    private static bool Match(string typed, string keyword)

    {

      // We can't match a keyword that's shorter than what

      // was typed

 

      if (typed.Length > keyword.Length)

        return false;

 

      // Check the typed keyword against the initial section of the

      // keyword to match of the same length (in lowercase)

 

      var tlow = typed.ToLower();

      var klow = keyword.Substring(0, typed.Length).ToLower();

 

      bool matchComplete = true;

 

      if (keyword.Length > typed.Length)

      {

        var rest = keyword.Substring(typed.Length);

        matchComplete = (rest == rest.ToLower());

      }

 

      return (tlow == klow && matchComplete);

    }

  }

}

I made some additional – actually fairly significant – changes to the project since we saw the code posted:

  • There was an issue with the code I’d posted for the global command helper: even though I thought I’d tested it thoroughly, Editor.Command() didn’t work in this particular context. I went back to Document.SendStringToExecute()… I’ll make sure the post gets updated.
  • We didn’t actually need a Popup window for our global keywords list. We now create a normal WPF window and make AutoCAD its owner, which means it will be minimised and restored along with AutoCAD and doesn’t stay “topmost” in the Z order.

Overall the app is really starting to shape up. We’re thinking about how best to distribute it for feedback (possibly via Labs – we’ll see). If you have the chance to give it a try yourself in the meantime, please post a comment or drop me an email.

March 09, 2015

Adding support for global keywords at AutoCAD’s command-line using .NET – Part 1

This post carries on from this series from a couple of weeks ago:

The overall goal behind these posts was to create a “command-line helper” tool to make it easier for people who know the English version of AutoCAD (or an AutoCAD-based vertical) to work with a corresponding localized version. Basically because some people learn AutoCAD using an English version – perhaps during their studies – but then become a bit lost when they find they suddenly have to work with a local-language version in a professional setting.

In previous posts we’ve added the ability to launch global commands without the underscore prefix (we check for “unknown commands” and try them again with the prefix) as well as displaying a helpful “global keyword list” dialog while commands prompt for keywords. Here’s a reminder of what this dialog looks like:


KeywordHelper

Which is a good start, but the next big hurdle was around entering global keywords without the underscore: it’s all well and good to be able to launch a global command via the command-line, but if you then have to double-click a UI element to enter a global keyword (or remember to add your own underscore prefix when typing it yourself) then it’s incomplete.

I spent much of today adding global keyword support into the command-line: what we wanted to do was insert an underscore prefix in front of a complete keyword being typed in at the command-line assuming it matches an expected global keyword (of which we handily already have a list in our popup dialog).

I wasn’t at all sure this was possible, I have to say. My thinking was to use the powerful-but-scary Application.PreTranslateMessage event – which I talked out many moons ago – as this gives you really low level access to messages arriving via the input pipeline. But I wasn’t at all sure it would give you the access you need to achieve this.

It did turn out to be possible – as of about 30 minutes ago I managed to get something working – but there’s quite a bit of code I need to tidy up before posting. We’ll take a look at the solution either tomorrow or the day after. Watch this space!

March 06, 2015

Autodesk Memento now in public beta

One of the announcements at the recent REAL 2015 conference – and if you missed the event, as I did, you can see a great summary here – was the fact that the much-appreciated mesh manipulation tool from our Reality Solutions division, Autodesk Memento, has now entered public beta. It has graduated from Autodesk Labs and is now available for download without you having to log into the beta portal. It also has a brand new web-site describing why you might want to use it.

Memento

For those of you who have been tracking its progress, release on release, here are the main features in the posted v1.0.14.2:

  • FBX with cameras export
  • Camera location preview in canvas (note that you will need to reprocess your scenes in order to see the cameras)
  • Bridge tool exposed under the Edit menu
  • New ground plane that helps with orienting the model in space
  • Separated orient model and coordinate system
  • Improved dashboard
  • RCM package file format that now contains the texture as well, so your mesh is all in one native Memento file
  • Memento now checks for low disk space situations, notifying the user when they don’t have enough space to save
  • Zooming into empty space now stops at certain distance away from the object. Zooming out Or ‘Fit-to-view’ will bring the model back into view
  • Improvement in 'waiting in queue' time for ReCap processing

Here’s a video overview of the product’s features:




If you’re interested in using Memento, I recommend watching this recorded webinar that talks about how best to take photos which, when processed, will result in a well-formed mesh:




Over the coming weeks an online gallery will be available from the product, too, showcasing some of the amazing projects people have used Memento to create.

March 04, 2015

Morgan at the Geneva Motor Show 2015

I headed over to Geneva yesterday afternoon for the first press day of this year’s Geneva Motor Show. I was there courtesy of our friends at the Morgan Motor Company who launched their new Aero 8 (it was actually nice being there as a guest, in contrast to last year’s collaboration). Here’s the launch video for this exciting car:



I love this video, but I did have one minor gripe for the Morgan team: why was Jon Wells, Morgan’s Head of Design, shaving a piece of clay rather than using Autodesk tools to develop the design? I was joking – I can understand why for the aesthetics of the video they’d rather focus on the analog aspects of the process – but they did explain they had in fact shot footage of our tools being used but it had ended up on the cutting room floor (or whatever the virtual equivalent is). Understandably they just had too much great material for a video lasting 2 minutes and change.

I was just happy to know they’d used Autodesk software to help design this gorgeous vehicle.

Aero 8

In case anyone’s wondering how much this car costs… and my apologies if we’re getting into “product placement” territory, but I’m sure people are curious…

The previously generation of the Aero retailed at around £130K, but Morgan has worked hard to streamline the BOM so the base Aero 8 model now costs a touch under £58K excluding VAT (£66.5K including VAT). It seems as though this is being sold in the US at $130K (including a number of options not included in the base UK model). Full prices are here.

Something I hadn’t realised: Morgan now has a partnership with The Balvenie (one of my favourite Scotch whiskeys). During the evening, James Buntin, Brand Ambassador for The Balvenie, led a tasting session on the Morgan stand. Here’s a video describing some background to The Balvenie’s collaboration with Morgan. It also does a great job of painting a picture of Morgan as a company, something I didn’t do justice in the blog post describing my own factory visit.



Struthers London were also present on the stand: they’re the London-based watchmakers who hand-make the co-branded Morgan watches. They launched the Aero 8 edition of the Struthers for Morgan line at the show.

So did I place my order for an Aero 8 while I was there? No, I didn’t, although I do expect a lot more people to be interested in the car given the new pricing model. The first 5 or so minutes of this class from AU 2015 might help explain why I’m not in the market for one…

 

March 02, 2015

AutoCAD and Prince of Persia

Every so often I get hit by a wave of computing nostalgia. This weekend it was a veritable tsunami triggered by the discovery that a number of old MS-DOS games are available to play online in your browser, including the seminal Prince of Persia. This game has a strong connection with AutoCAD, for me, so today I’m blogging about that.


Prince of Persia intro 

I first started working with AutoCAD while I was still at high school – it must have been around 1989. After a successful (but mind-numbingly boring) summer project at a local manufacturing company, converting their old engineering calculation routines from PET BASIC to GW-BASIC (which mostly involved typing code in from dot matrix print-outs, as far as I recall), I started reading the manuals for their new CAD system, AutoCAD R10. In due course I was creating LISP routines to automate the 2D drawing of nozzles and flanges for their plastic pressure vessels. This work set me up with some spending money for my later studies, and I have very fond memories of that period of my life.

Back when I got started, our drawing office used Compaq DeskPro 386 machines. These were great machines, not only for running AutoCAD for DOS but also for running early PC video games. The game that captured our collective imagination, from around 1990-91, was Prince of Persia. We would play it at any opportunity outside normal working hours.

This game was simply unbelievable. Until then I’d probably been most impressed, character animation-wise, by ZX Spectrum games such as Tir Na Nog and (to some degree) Heavy on the the Magick. But Prince of Persia blew these out of the water – the way the main character moved was incredibly realistic.


Prince of Persia 

So on Saturday afternoon, after playing the game for a few minutes – sadly my muscle memory for the gameplay has faded to nothing and I don’t have the patience to reacquire it – I decided to look into its origins. Fairly quickly I came across the original User’s Guide for the game, at which point I realised the game was developed in San Rafael, Marin County. Close to where I’d myself lived and worked for a number of years at Autodesk!

This struck me as pretty amazing: both pieces of software I was using at the time had been developed in the same Northern Californian town. (OK, in fairness AutoCAD was developed in Mill Valley and Sausalito during the period in which Prince of Persia was developed – Autodesk moved its HQ to San Rafael in 1994 – but this was definitely a topic worth looking into.)

The creator of Prince of Persia was Jordan Mechner, at the time a twentysomething from New York and recent Yale graduate. He’d previously written the popular Karateka game (not one I played myself) and ended up moving out to the Bay Area to work on a new title – which became Prince of Persia – for Brøderbund Software. These were the days when mainstream computer games were still often written by individual – albeit highly talented – programmers, something that’s now only really possible in the indie games scene or for rare breakouts such as Minecraft. Although interestingly – and perhaps this is also something that makes him exceptional – Jordan’s passion was cinema: he was also working hard during this period to succeed as a screenwriter.

Jordan kept a journal during his years developing the game, which you can buy from Amazon or access via a historical snapshot of his blog. The journal makes fascinating reading, talking about the animation process, going from videoed real-world footage – often of his younger brother, David – to individual frames. This rudimentary, rotoscoped motion-capture approach has arguably become the foundation for most 3D games, today.

Prince of Persia rotoscopy

Image copyright Jordan Mechner.

If you’re interested in seeing how this process translated into the game at various stages of development, be sure to check out the videos Jordan has posted on Vimeo.

Reading the journal was heavily nostalgic for me: Jordan describes the video camera’s battery dying while he was trying to pull himself upwards, hanging off the bus shelter at North San Pedro Road (the freeway exit I used to take to go home to the place we rented in Santa Venetia). Basically in an attempt to capture the frames for climbing up onto a ledge from a hanging position (which is understandably really hard to do).

Given the benefit of hindsight, the journal is also poignant in places. In a number of entries Jordan describes the beauty of Tina LaDeau, the 18-year old actress – and daughter of a colleague of Jordan’s at Brøderbund – who came in to be captured for the role of the princess. In a later entry she’s described by a friend as having “the ephemeral beauty of an 18-year-old.” That turned out to be all too true: Tina tragically passed away in August 2012 at the far-too-young age of 41.

Jordan talks about many of the struggles behind making Prince of Persia a success: for instance, it very nearly flopped due to lackluster marketing as well as having been developed initially for the dying Apple II platform (its success eventually came once it was ported to the IBM PC). He also paints a vivid picture of his life at the time, describing personal relationships and current events such as the first Gulf War and the 1989 earthquake. He mentions eating at Marin Joe’s and the Royal Thai as well as a picnic at the Marin County Civic Center duck pond – all of which I remember well from living in San Rafael in the early 2000s.

After reading the journal for the Prince of Persia years in one sitting – it was compulsive reading – I decided to check out the game’s source code. While fans have been sharing disassembled versions for years, Jordan rediscovered the source for the original Apple II game a few years ago and posted it to GitHub. While the code itself is mostly incomprehensible to me, I’m always fascinated to see the how programmers during the early part of the personal computing era overcame significant resource constraints to push boundaries and essentially create works of art, much as Ian Bell did when creating Elite. One example from Jordan’s diary: the “Shadowman” character was born because so much of the available memory was taken with the main character’s animation frames. XORing the frames with a single pixel offset allowed Jordan to introduce another character with near-zero memory overhead – and it took about two minutes to implement. Later on additional adversaries were able to be introduced into the game, but you have to admire the ingenuity behind this initial one.

Here’s the point at the end of level 4 where Shadowman gets created as you jump through a mirror. Totally cool.


Birth of Shadowman 
Prince of Persia’s success didn’t end with the initial game (although admittedly I personally haven’t followed its progress since): the first game spawned a successful series of games and even a movie – which is great for Jordan finally to have made the crossover from video games to cinema, having written the first screenplay and been an executive producer for Prince of Persia: The Sands of Time. Interestingly the whole Assassin’s Creed series was essentially a Prince of Persia spin-off (the initial game was being developed by Ubisoft as “Prince of Persia: Assassins”). Describing this game’s impact on the entertainment industry as significant would be quite an understatement.

So if you feel like playing this game – whether for the first time or, as I did, to remind you of good times – head on over to the Internet archive and take it for a spin. There are lots of other gems posted there, too: is your favourite MS-DOS game?

February 26, 2015

Enabling global commands on localized AutoCAD versions using .NET

Here’s a quick piece of code to finish up the week to complement what we saw earlier. The idea is that on localized AutoCAD versions this code will allow the user to enter English commands without needing the underscore prefix. The code works by detecting an “unknown” command and then attempting to execute it again after prefixing an underscore to launch a global command. Which may or may not work, of course, so we certainly need to set a flag to avoid descending into an infinite loop of commands being called while prefixed by an ever-expanding legion of underscores.

Aside from that we have some code disabling auto-correct and auto-complete, as these certainly get in the way of the code working properly. These aren’t strictly system variables, so I haven’t jumped through the hoops to make sure they get set back properly afterwards. So be aware these capabilities are likely to be disabled – and then require manual re-enabling – once you’ve run this code.

Here’s the C# code in question:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

 

namespace CommandHelper

{

  public class Commands

  {

    // Mutex to stop unknown command handler re-entrancy

 

    private bool _launched = false;

 

    [CommandMethod("CMDS")]

    public void CommandTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      // AutoComplete and AutoCorrect cause problems with

      // this, so let's turn them off (we may want to warn

      // the user or reset the values, afterwards)

 

      doc.Editor.Command(

        "_.-INPUTSEARCHOPTIONS",

        "_autoComplete", "_No",

        "_autocoRrect", "_No",

        ""

      );

 

      // Add our command prefixing event handler

 

      doc.UnknownCommand += OnUnknownCommand;

    }

 

    [CommandMethod("CMDSX")]

    public void StopCommandTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      // Remove our command prefixing event handler

 

      doc.UnknownCommand -= OnUnknownCommand;

    }

 

    async void OnUnknownCommand(

      object sender, UnknownCommandEventArgs e

    )

    {

      var doc = sender as Document;

 

      // Check to make sure we're not re-entering the handler

 

      if (doc != null && !_launched)

      {

        try

        {

          // Set the mutex flag and call our command

 

          _launched = true;

          await doc.Editor.CommandAsync("_" + e.GlobalCommandName);

        }

        catch { } // Let's not be too fussy about what we catch

        finally

        {

          // Reset our flag, now we're done

 

          _launched = false;

        }

      }

    }

  }

}

Be warned: this code won’t do anything useful on English versions of AutoCAD, as in that context local commands also happen to be global. So a) you won’t get an unknown command event when you call a global command and b) if you do, prefixing an underscore ain’t gonna help. :-)

You’re also going to need at least AutoCAD 2015 for this code to work, as it depends on Editor.CommandAsync().

Next week I’m officially back from vacation, so you can expect my – as it turns out uninterrupted – posting schedule to return to (i.e. carry on as) normal.

February 25, 2015

Adding a global keyword menu to AutoCAD using WPF – Part 2

After introducing this project in the last post, now it’s time to share some code. The project, as it currently stands, contains three source files: the first one relates to AutoCAD – it implements the various commands we’ll use to attach event handlers to tell us when to display (or hide) keywords and the other two files relate to the UI we’ll use to display them. We’re going to use an invisible window which has a child popup containing a listbox of our keywords.

Here’s the application in action – for now in English AutoCAD, as that’s what I have installed – helping us with the keywords during the PLINE and HATCH commands:


KeywordHelper

Now for the source. Let’s start with the AutoCAD-related C# file, which I’ve called keyword-helper.cs:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using System.Collections.Specialized;

 

namespace KeywordHelper

{

  public class Commands

  {

    // The keyword display window

 

    private KeywordWindow _window = null;

 

    // List of "special" commands that need a timer to reset

    // the keyword list

 

    private readonly string[] specialCmds = { "MTEXT" };

 

    [CommandMethod("KWS")]

    public void KeywordTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      if (_window == null)

      {

        _window = new KeywordWindow();

        _window.Show();

        Application.MainWindow.Focus();

      }

 

      // Add our various event handlers

 

      // For displaying the keyword list...

 

      ed.PromptingForAngle += OnPromptingForAngle;

      ed.PromptingForCorner += OnPromptingForCorner;

      ed.PromptingForDistance += OnPromptingForDistance;

      ed.PromptingForDouble += OnPromptingForDouble;

      ed.PromptingForEntity += OnPromptingForEntity;

      ed.PromptingForInteger += OnPromptingForInteger;

      ed.PromptingForKeyword += OnPromptingForKeyword;

      ed.PromptingForNestedEntity += OnPromptingForNestedEntity;

      ed.PromptingForPoint += OnPromptingForPoint;

      ed.PromptingForSelection += OnPromptingForSelection;

      ed.PromptingForString += OnPromptingForString;

 

      // ... and removing it

 

      doc.CommandWillStart += OnCommandEnded;

      doc.CommandEnded += OnCommandEnded;

      doc.CommandCancelled += OnCommandEnded;

      doc.CommandFailed += OnCommandEnded;

 

      ed.EnteringQuiescentState += OnEnteringQuiescentState;

    }

 

    [CommandMethod("KWSX")]

    public void StopKeywordTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      if (_window == null)

      {

        _window.Hide();

        _window = null;

      }

 

      // Remove our various event handlers

 

      // For displaying the keyword list...

 

      ed.PromptingForAngle -= OnPromptingForAngle;

      ed.PromptingForCorner -= OnPromptingForCorner;

      ed.PromptingForDistance -= OnPromptingForDistance;

      ed.PromptingForDouble -= OnPromptingForDouble;

      ed.PromptingForEntity -= OnPromptingForEntity;

      ed.PromptingForInteger -= OnPromptingForInteger;

      ed.PromptingForKeyword -= OnPromptingForKeyword;

      ed.PromptingForNestedEntity -= OnPromptingForNestedEntity;

      ed.PromptingForPoint -= OnPromptingForPoint;

      ed.PromptingForSelection -= OnPromptingForSelection;

      ed.PromptingForString -= OnPromptingForString;

 

      // ... and removing it

 

      doc.CommandWillStart -= OnCommandEnded;

      doc.CommandEnded -= OnCommandEnded;

      doc.CommandCancelled -= OnCommandEnded;

      doc.CommandFailed -= OnCommandEnded;

 

      ed.EnteringQuiescentState -= OnEnteringQuiescentState;

    }

 

    // Event handlers to display the keyword list

    // (each of these handlers needs a separate function due to the

    // signature, but they all do the same thing)

 

    void OnPromptingForAngle(

      object sender, PromptAngleOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForCorner(

      object sender, PromptPointOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForDistance(

      object sender, PromptDistanceOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForDouble(

      object sender, PromptDoubleOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForEntity(

      object sender, PromptEntityOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForInteger(

      object sender, PromptIntegerOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForKeyword(

      object sender, PromptKeywordOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForNestedEntity(

      object sender, PromptNestedEntityOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForPoint(

      object sender, PromptPointOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForSelection(

      object sender, PromptSelectionOptionsEventArgs e

    )

    {

      // Nested selection sometimes happens (e.g. the HATCH command)

      // so only display keywords when there are some to display

 

      if (e.Options.Keywords.Count > 0)

        DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForString(

      object sender, PromptStringOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnCommandEnded(object sender, CommandEventArgs e)

    {

      _window.ClearKeywords(true);

    }

 

    // Event handlers to clear & hide the keyword list

 

    void OnEnteringQuiescentState(object sender, EventArgs e)

    {

      _window.ClearKeywords(true);

    }

 

    // Helper to display our keyword list

 

    private void DisplayKeywords(

      KeywordCollection kws

    )

    {

      // First we step through the keywords, collecting those

      // we want to display in a collection

 

      var sc = new StringCollection();

      if (kws != null && kws.Count > 0)

      {

        foreach (Keyword kw in kws)

        {

          if (kw.Enabled && kw.Visible && kw.GlobalName != "dummy")

          {

            sc.Add(kw.LocalName); // Expected this to be GlobalName

          }

        }

      }

 

      // If we don't have keywords to display, make sure the

      // current list is cleared/hidden

 

      if (sc.Count == 0)

      {

        _window.ClearKeywords(true);

      }

      else

      {

        // Otherwise we pass the keywords - as a string array -

        // to the display function along with a flag indicating

        // whether the current command is considered "special"

 

        var sa = new string[kws.Count];

        sc.CopyTo(sa, 0);

 

        // We should probably check for transparent/nested

        // command invocation...

 

        var cmd =

          (string)Application.GetSystemVariable("CMDNAMES");

        _window.ShowKeywords(

            sa, Array.IndexOf(specialCmds, cmd) >= 0 //, append

          );

      }

    }

 

    internal static void launchCommand(string cmd)

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      doc.SendStringToExecute(

        "_" + cmd + " ", true, false, true

      );

    }

  }

}

I was surprised that the English version of keywords on localized versions were accessible via the LocalName – rather than GlobalName – property. But apparently that’s how it works.

Next we have the XAML file for our KeywordWindow which, while invisible, contains the popup we’ll use to display the keywords. The file is called KeywordWindow.xaml.

<Window

  x:Class="KeywordHelper.KeywordWindow"

  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  Title="KeywordWindow" Height="0" Width="0"

  WindowStyle="None" ShowInTaskbar="False" AllowsTransparency="True"

  Loaded="Window_Loaded">

  <Window.Background>

    <SolidColorBrush Opacity="0" Color="White"/>

  </Window.Background>

  <Grid>

    <Popup Name="KeywordPopup" Placement="Custom">

      <ListBox x:Name="Keywords" Width="100" Height="auto">Keywords

        <ListBox.ItemContainerStyle>

          <Style

           TargetType="{x:Type ListBoxItem}"

           BasedOn="{StaticResource {x:Type ListBoxItem}}">

            <EventSetter

             Event="MouseDoubleClick"

             Handler="ListBoxItem_MouseDoubleClick"/>

          </Style>

        </ListBox.ItemContainerStyle>

      </ListBox>

    </Popup>

  </Grid>

</Window>

And finally the C# code-behind, KeywordWindow.xaml.cs:

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Controls.Primitives;

using System.Windows.Threading;

 

namespace KeywordHelper

{

  ///<summary>

  /// Interaction logic for KeywordWindow.xaml

  ///</summary>

 

  public partial class KeywordWindow : Window

  {

    private DispatcherTimer _t;

    private DateTime _lastOpened;

    private bool _special;

 

    public KeywordWindow()

    {

      InitializeComponent();

      KeywordPopup.CustomPopupPlacementCallback =

          new CustomPopupPlacementCallback(PlacePopup);

      _t = null;

    }

 

    public void ShowKeywords(

      string[] keywords, bool special = false, bool append = false

    )

    {

      // Store the flag in a member variable so we can access it

      // from a lambda event handler

 

      _special = special;

 

      // Get the listbox contents

 

      var items = ((ListBox)KeywordPopup.Child).Items;

 

      // The first test of difference is whether the number of items

      // is different

 

      if (append)

      {

        foreach (var kw in keywords)

        {

          items.Add(kw);

        }

      }

      else

      {

        bool different = keywords.Length != items.Count;

        if (!different)

        {

          // If lists are the same length, check the contents

          // item by item

 

          for (int i = 0; i < items.Count; i++)

          {

            var kw = keywords[i];

            var item = (string)items[i];

            if (String.Compare(kw, item) != 0)

            {

              different = true;

              break;

            }

          }

        }

 

        // If the items are different, let's clear the list and

        // rebuild it

 

        if (different)

        {

          items.Clear();

          foreach (var kw in keywords)

          {

            items.Add(kw);

          }

        }

      }

      KeywordPopup.IsOpen = true;

 

      // We're going to use a timer to close the popup in case

      // it isn't closed by one of the various callbacks we have

      // in place

 

      if (_t == null)

      {

        // Choose an interval of 2 seconds

 

        var ts = new TimeSpan(TimeSpan.TicksPerSecond * 2);

        _t = new DispatcherTimer { Interval = ts };

        _t.Tick += (s, e) =>

          {

            // If 2s or more has elapsed since the last popup

            // was displayed, close it

 

            if (_special && (DateTime.Now - _lastOpened >= ts))

              KeywordPopup.IsOpen = false;

          };

        _t.Start();

      }

 

      // Record when the latest popup was displayed

 

      _lastOpened = DateTime.Now;

    }

 

    public void ClearKeywords(bool hide)

    {

      // Optionally hide the popup

 

      KeywordPopup.IsOpen = !hide;

 

      // Clear the keyword contents

 

      ((ListBox)KeywordPopup.Child).Items.Clear();

    }

 

    private void ListBoxItem_MouseDoubleClick(

      object s, System.Windows.Input.MouseButtonEventArgs e

    )

    {

      // When an item is double-clicked, simply send it to the

      // command-line with an underscore prefix

 

      var item = (ListBoxItem)s;

      Commands.launchCommand((string)item.Content);

    }

 

    public CustomPopupPlacement[] PlacePopup(

      Size popupSize, Size targetSize, Point offset

    )

    {

      // We want to place the popup relative to the AutoCAD

      // main window

 

      var win =

        Autodesk.AutoCAD.ApplicationServices.Application.MainWindow;

 

      // Calculate the bottom-right of the popup - both x and y -

      // relative to the location of the parent window (this)

 

      var x =

        win.DeviceIndependentLocation.X +

        win.DeviceIndependentSize.Width - this.Left;

 

      // 33 is the height of the bottom window border/status bar

 

      var y =

        win.DeviceIndependentLocation.Y +

        win.DeviceIndependentSize.Height - this.Top - 33;

 

      // The above values need scaling for DPI

 

      var s =

        Autodesk.AutoCAD.Windows.Window.GetDeviceIndependentScale(

          IntPtr.Zero

        );

 

      // Get our scaled position, taking into account the

      // size of the popip

 

      var p =

        new System.Windows.Point(

          (int)(x * s.X - popupSize.Width),

          (int)(y * s.Y - popupSize.Height)

        );

 

      // Return that position as our custom placement

 

      return new CustomPopupPlacement[] {

        new CustomPopupPlacement(p, PopupPrimaryAxis.Vertical)

      };

    }

  }

}

So far I’ve had to code a few caveats for command behaviour: the HATCH command displays a selection prompt – without keywords – within a point prompt (which does have keywords). So I make sure we don’t clear the menu, in this case. Then there’s the MTEXT command, which performs a point selection for the window area – with keywords – before displaying its IPE (in-place editor). We use a timer to close the popup in the case that 500ms elapses without a request for keywords to be displayed. I have no doubt other commands will present other quirks, but we’ll address those as they crop up.

There’s still some work to do to hide the popup when AutoCAD is minimized – as well as to make sure our invisible window doesn’t appear in the Alt-Tab program switcher – but that’s left for the reader (or for another day, we’ll see).

One additional requirement I do want to address – probably in the next post – is to automatically prefix underscores on unknown commands, to see if someone has entered an English command by mistake on a localization version of AutoCAD.

February 23, 2015

Adding a global keyword menu to AutoCAD using WPF – Part 1

I’m up in the mountains, supposedly on vacation, but as one of our children woke up with a fever, I’m skipping the morning session on the slopes to stay home with him. Which gives me the chance to start writing up a little project I’ve been working on for our Localization team.

Here’s the idea… apparently it’s relatively common in certain countries for AutoCAD users to learn the product in English but then end up working with a localized version of the software. While it’s always possible to use global commands and keywords by prefixing an underscore, it’s not always something people remember to do. Which can understandably leads to frustration.

Our Localization team is keen to find a way to help these users, such as by streamlining their ability to use global commands and keywords in a localized AutoCAD product. One suggestion was to build an app that displays a list of the global keywords they can use either as an aide-memoire or to launch the keyword itself.

This seemed like a fun little project, so I ended up putting together an initial version over the weekend. Here are a few comments on what I ended up building:

1. An app that uses WPF to display a Popup in the bottom right of the AutoCAD window.

Keyword popup

2. The list gets populated by global keywords that AutoCAD prompts for. Luckily there are events that provide us with this information.

3. When an item in the list is double-clicked the keyword gets sent to the command-line – with the underscore prefix, of course.

4. Some commands need to be treated as “special”: as they request input but then don’t complete straightaway, we need to use a timer to close the popup after a certain interval elapses. For example, the MTEXT command prompts for a window for the text area, but then launches the in-place editor (IPE) rather than completing or having AutoCAD become quiescent. We want to close our keyword popup while the IPE is active.

That’s basically the scope of the project – for now, at least. As it’s so far possible to implement using published APIs, I’ll go ahead and share the code during the course of this week.

February 20, 2015

Styling your HTML5 progress meter with CSS3

After yesterday’s fun with creating an HTML5-based progress meter for AutoCAD, today we’re going to have some more fun styling it with CSS.

To recap, here’s the progress meter that comes “out of the box”, with the default styling from Chromium on Windows.


Original progress meter

The first thing we need to do for our various changes is to use CSS to disable the default styling, at which point we can then use CSS to override it.

      progress {

        width: 100%;

        -webkit-appearance: none;

      }

Here’s how our progress meter looks when unstyled:


Unstyled progress meter

Now that it’s stripped bare, we can apply some styling to make it look as we want. For inspiration I started with some code from this page:

      progress[value]::-webkit-progress-value {

        background-image:

          -webkit-linear-gradient(

            -45deg,

            transparent 33%, rgba(0, 0, 0, .1) 33%,

            rgba(0, 0, 0, .1) 66%, transparent 66%

          ),

          -webkit-linear-gradient(

            top,

            rgba(255, 255, 255, .25),

            rgba(0, 0, 0, .25)

          ),

          -webkit-linear-gradient(left, #09c, #f44);

 

        border-radius: 2px;

        background-size: 35px 20px, 100% 100%, 100% 100%;

      }

Which makes it nice and colorful, but for some reason makes me think of licorice.


Colourful progress meter

So to see what difference certain tweaks make, I changed the first colour in the 2nd “-webkit-linear-gradient” to #ccc and the second to #fff. This washed the colours away:


Monochrome progress meter

For the next iteration I borrowed from this page, which helped add some warmth back in:


Nice progress meter

From there I decided to try the blue-within-blue (yes, that’s a Dune reference ;-) styling used by AutoCAD’s standard progress meter. I didn’t manage to get the exact colours, but they look OK:


Blue progress meter

Then I decided to round off the corners, which is a simple matter of setting the border-radius to 50px in the main progress meter style but also to add a style for the background doing much the same:

      progress::-webkit-progress-bar {

        background: gray;

        border-radius: 50px;

        padding: 0px;

        box-shadow: 0 1px 0px 0 rgba(255, 255, 255, 0.2);

      }


Blue progress meter with rounded corners

At this point things were shaping up nicely. The final touch was to replicate the same kind of barber’s pole striping to the background, making it look like a striped reservoir filling with water. Overall it’s an effect I like a lot. And you can very easily tweak this to better suit your own application’s requirements, of course.


Final progress meter

Here’s the completed CSS which can be pasted into the <style> element in yesterday’s post (or placed in its own file and referenced from the HTML page, if you prefer that approach):

      progress[value]::-webkit-progress-value {

        background-image:

          -webkit-linear-gradient(

            -45deg,

            transparent 33%, rgba(0, 0, 0, .1) 33%,

            rgba(0, 0, 0, .1) 66%, transparent 66%

          ),

          -webkit-linear-gradient(

            top,

            rgba(255, 255, 255, .25),

            rgba(0, 0, 0, .25)

          ),

          -webkit-linear-gradient(left, #335EC4, #1F71F4);

 

        border-radius: 50px;

        background-size: 35px 20px, 100% 100%, 100% 100%;

      }

      progress::-webkit-progress-bar {

        background-image:

          -webkit-linear-gradient(

            -45deg,

            transparent 33%, rgba(0, 0, 0, .1) 33%,

            rgba(0, 0, 0, .1) 66%, transparent 66%

          ),

          -webkit-linear-gradient(

            top,

            rgba(255, 255, 255, .25),

            rgba(0, 0, 0, .25)

          ),

          -webkit-linear-gradient(left, #333, #666);

        border-radius: 50px;

        background-size: 35px 20px, 100% 100%, 100% 100%;

      }

      body {

        overflow: hidden;

        width: 98%;

        height: 98%;

      }

      hidden {

        display: none;

      }

      progress {

        width: 100%;

        -webkit-appearance: none;

      }

      .td-center  {

        text-align: center;

      }

      .td-right {

        text-align: right;

      }

      .center-div  {

        width: 100%;

        padding: 25% 0;

      }

      div {

        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

        font-size: large;

        font-weight: bold;

      }

Next week I’m hitting the slopes with my family as the kids are off school, the reason I’m not able to make it across to REAL 2015. Hopefully I’ll get the chance to post to this blog at least once, but there’s some chance I’ll end up taking the whole week off.

February 19, 2015

Creating your own AutoCAD progress meter using HTML5 and JavaScript

This week I’ve spent quite a bit of time looking into future API features. For one of them I needed to create a progress meter, and thought to myself “why not create one in HTML5?” And as it’s nothing specific to a future product release, I decided to go ahead and post it now.

For context, here’s the way AutoCAD’s standard progress meter currently looks, displayed using the code from this previous post:


Standard progress meter

So why would you go head and create your own progress meter? A few different reasons come to mind… yes, AutoCAD has its own, but perhaps you want something more visible (not tucked away in the bottom right corner of the application frame), pausable or more explicitly cancellable. Or perhaps you just want to style it differently – something we’ll take a look at in tomorrow’s post.

Even if you don’t want to create your own progress meter, the techniques shown in today’s post will be valuable if you want to create an HTML UI that’s tightly integrated with AutoCAD.

Overall the code is fairly straightforward: as with most HTML5 projects I’ve embarked upon, I ended up spending more time than expected to get the vertical alignment on the page looking good (mainly because the “old” approach of using tables with the valign attribute no longer works in HTML5… apart from understanding how vertical-align now works, there are still a number of approaches for managing vertical space).

The other big sticking point was around getting the various page elements to display consistently. For instance, very often the caption wouldn’t display the first time the dialog was shown in a session… I hit my head against this for ages. In the end I found that having the HTML page call back into our .NET app to say “the page has loaded, we’re ready to roll” was the cleanest approach.

Here’s the progress meter in action, running to completion. You’ll notice the dialog is quite big… that’s the minimum size of a modeless dialog. We could also use another modeless container, of course – such as an HTML palette or even a non-DWG document window – but for this scenario a modeless window made most sense.


Progress meter - completed

And here it is when it’s cancelled partway through:


Progress meter - cancelled

Here’s the HTML code:

<!doctype html>

<html>

  <head>

    <title>Progress</title>

    <style>

      body {

        overflow: hidden;

        width: 98%;

        height: 98%;

      }

      hidden {

        display: none;

      }

      progress {

        width: 100%;

      }

      .td-center  {

        text-align: center;

      }

      .td-right {

        text-align: right;

      }

      .center-div  {

        width: 100%;

        padding: 25% 0;

      }

      div {

        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

        font-size: large;

        font-weight: bold;

      }

      </style>

    <script

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

    </script>

    <script type="text/javascript">

      var progbar, limit, loaded = false;

 

      function updateProgress(value) {

        progbar.max = limit;

        progbar.value = value;

        progbar.getElementsByTagName('span')[0].innerHTML =

          Math.floor((100 / limit) * value);

      }

 

      function displayValue(prop, val) {

 

        if (prop == "progress") {

          updateProgress(val);

        }

        else if (prop == "limit") {

          limit = val;

        }

        else {

          // Display the specified value in our div for the specified

          // property

 

          var div = document.getElementById(prop);

          if (div != null) {

            if (typeof val === "string") {

              div.innerHTML = val;

            }

            else {

              div.innerHTML = val.toFixed(2);

            }

          }

        }

      }

 

      function showControls(show) {

        var prog = document.getElementById("progress");

        var butt = document.getElementById("cancel");

        if (show) {

          prog.classList.remove("hidden");

          butt.classList.remove("hidden");

        }

        else {

          prog.classList.add("hidden");

          butt.classList.add("hidden");

        }

      }

 

      function start() {

        showControls(true);

      }

 

      function ready() {

        return loaded;

      }

 

      function stop() {

        showControls(false);

        self.close();

      }

 

      function updateControls(args) {

 

        var obj = JSON.parse(args);

 

        var propName = obj.propName;

        var propVal = obj.propValue;

 

        // If the string represents a double (we test using

        // a RegExp), round it to 2 decimal places

 

        var val = 0.0;

        var found = false;

 

        if (typeof propVal === "number") {

          val = propVal;

          found = true;

        }

        else if (typeof propVal === "string") {

          var re = /^[+-] ?[0-9]{0,99}(?:\.[0-9]{1,99})?$/;

          if (propVal.match(re)) {

            val = parseFloat(propVal);

          }

          else {

 

            // Otherwise just display the string

 

            displayValue(propName, propVal);

          }

        }

        if (found) {

          displayValue(propName, val);

        }

      }

 

      // Shaping layer extensions

 

      function pageLoaded() {

        var jsonResponse =

          exec(

            JSON.stringify({

              functionName: 'Ready',

              invokeAsCommand: false,

              functionParams: undefined

            })

          );

        var jsonObj = JSON.parse(jsonResponse);

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

          throw Error(jsonObj.retErrorString);

        }

        return jsonObj.result;

      }

 

      function cancelOperation() {

        var jsonResponse =

          exec(

            JSON.stringify({

              functionName: 'CanOp',

              invokeAsCommand: false,

              functionParams: undefined

            })

          );

        var jsonObj = JSON.parse(jsonResponse);

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

          throw Error(jsonObj.retErrorString);

        }

        return jsonObj.result;

      }

    </script>

  </head>

  <body>

    <table class="center-div">

      <tr>

        <td class="td-center">

          <div id="caption">&nbsp;</div>

        </td>

      </tr>

      <tr>

        <td class="td-right" width="100%">

          <progress id="progress" class="hidden"

                    value="0" max="100">

            <span>0</span>%

          </progress>

        </td>

        <td class="td-right">

          <button id="cancel" class="hidden"

                  onclick="cancelOperation();">

            Cancel

          </button>

        </td>

      </tr>

      <tr>

        <td class="td-center">

          <div id="extra"></div>

        </td>

      </tr>

    </table>

    <script type="text/javascript">

      (function () {

        registerCallback("updval", updateControls);

        registerCallback("start", start);

        registerCallback("stop", stop);

        progbar = document.getElementById('progress');

 

        document.onkeydown = function (evt) {

          evt = evt || window.event;

          if (evt.keyCode == 27) {

            cancelOperation();

          }

        };

        window.onload = pageLoaded;

      })();

    </script>

  </body>

</html>

I created a C# class that mimics the ProgressMeter protocol – in fact it derives from the standard ProgressMeter class, adding a few additional capabilities – to make it easier to switch between the two, as needed. You won’t want to put yours in the Autodesk.AutoCAD.Runtime namespace – I simply did so for my own convenience.

using Autodesk.AutoCAD.ApplicationServices;

using System;

using System.IO;

using System.Reflection;

using System.Runtime.InteropServices;

 

namespace Autodesk.AutoCAD.Runtime

{

  // Use the standard ProgressMeter protocol

 

  public class ProgressMeterHtml : ProgressMeter

  {

    private static bool _ready;

    private static bool _cancelled;

    private int _pos;

 

    [DllImport(

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

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "acjsInvokeAsync")]

    extern static private int acjsInvokeAsync(

      string name, string jsonArgs

    );

 

    // Called by Progress.html when the page has loaded

 

    [JavaScriptCallback("Ready")]

    public string ReadyToStart(string jsonArgs)

    {

      _ready = true;

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

    }

 

    // Called by Progress.html to cancel the operation

 

    [JavaScriptCallback("CanOp")]

    public string CancelOperation(string jsonArgs)

    {

      _cancelled = true;

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

    }

 

    // Constructor

 

    public ProgressMeterHtml()

    {

      // Initialize static members

 

      _ready = false;

      _cancelled = false;

 

      // Load Progress.html from this module's folder

 

      var asm = Assembly.GetExecutingAssembly();

      var loc =

        Path.GetDirectoryName(asm.Location) + "\\progress.html";

 

      Application.ShowModelessWindow(new System.Uri(loc));

 

      // Wait for the page to load fully to avoid refresh issues

 

      while (!_ready)

      {

        System.Threading.Thread.Sleep(500);

        System.Windows.Forms.Application.DoEvents();

      }

 

      // Initialize our progress counter

 

      _pos = 0;

    }

 

    // Start the progress meter without a caption

 

    public override void Start()

    {

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

    }

 

    // Start the progress meter with a caption

 

    public override void Start(string displayString)

    {

      Start();

      Caption(displayString);

    }

 

    // Set the limit

 

    public override void SetLimit(int max)

    {

      SendProperty("limit", max);

    }

 

    // Advance the progress meter

 

    public override void MeterProgress()

    {

      SendProperty("progress", ++_pos);

    }

 

    // Stop the progess meter, whether it's finished or the

    // operation has been cancelled

 

    public override void Stop()

    {

      Caption(_cancelled ? "Cancelled" : "Completed");

      AdditionalInfo(" ");

 

      // We'll wait for a second and then close the dialog

 

      System.Threading.Thread.Sleep(1000);

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

    }

 

    // Cancels the current operation

 

    public void Cancel()

    {

      _cancelled = true;

    }

 

    // Returns whether the operation has been cancelled

 

    public bool Cancelled

    {

      get { return _cancelled; }

    }

 

    // Sets the dialog's caption

 

    public void Caption(string displayString)

    {

      SendProperty("caption", displayString);

    }

 

    // Sets the additional information text

 

    public void AdditionalInfo(string displayString)

    {

      SendProperty("extra", displayString);

    }

 

    // Helper function to set a property in the HTML page

 

    private void SendProperty(string name, object val)

    {

      bool enclose = val.GetType() == typeof(String);

      var args =

        "{\"propName\":\"" + name + "\",\"propValue\":" +

        (enclose ? "\"" : "") + val.ToString() +

        (enclose ? "\"" : "") + "}";

      acjsInvokeAsync("updval", args);

    }

  }

}

The calling code is almost identical to what we saw in the original ProgressMeter post:

using Autodesk.AutoCAD.Runtime;

using System.Windows.Forms;

 

namespace ProgressMeterTest

{

  public class Cmds

  {

    [CommandMethod("PB")]

    public void ProgressBarHtml()

    {

      const int ticks = 50;

 

      var pm = new ProgressMeterHtml();

      pm.Start("Testing Progress Bar");

      pm.AdditionalInfo("Show something extra");

      pm.SetLimit(ticks);

 

      // Now our lengthy operation

 

      for (int i = 0; i < ticks; i++)

      {

        System.Threading.Thread.Sleep(50);

 

        // Increment progress meter...

 

        pm.MeterProgress();

        Application.DoEvents();

 

        if (pm.Cancelled)

          break;

      }

      pm.Stop();

    }

  }

}

That’s it for today’s post. Tomorrow we’ll take a look at styling the HTML to see what we can do with it.

Feed/Share

10 Random Posts