June 24, 2009

Batch-processing AutoCAD drawings from LISP without SDI (take 2)

In this recent post we looked at an approach combining AutoLISP with a script generated on-the-fly to get around the fact that (command "_.OPEN" …) does nothing when SDI == 0. As mentioned in a comment in the post, I realised that the approach of using a single master script to do this is more prone to failure: a number of commands can cause scripts to stop executing, for instance, so it would be better practice to minimise the operations contained in a particular script to increase the application’s fault tolerance.

This modified approach was suggested by a member of our Engineering team in a recent thread (one that I came across after Monday’s post). It uses a data file to store a list of the drawings to process and only creates a script to load – and launch processing on – the next drawing in that list:

(defun C:BATCH(/ dwgs lsp-name data-name)

  (setq dwgs '("C:/A.DWG" "C:/B.DWG" "C:/C.DWG" "C:/D.DWG")

        lsp-name "c:/tmp.lsp"

        data-name "c:/dwgs.tmp"

  )

  (create-drawing-list data-name dwgs)

  (process-next-drawing data-name lsp-name "(create-circle)" T T)

  (princ)

)

 

(defun create-circle()

  (command "_.CIRCLE" "10,10,0" "5")

)

 

(defun create-drawing-list(data dwgs / f dwg)

 

  ;; Create data file containing the DWG names

 

  (setq f (open data-name "w"))

  (foreach dwg dwgs

    (write-line dwg f)

  )

  (close f)

)

 

;; Get the first drawing from the list, removing it

 

(defun get-next-drawing(data / dwg dwgs f)

 

  ;; Read in the whole list of DWGs

 

  (setq f (open data "r")

        dwgs '()

  )

  (while (setq dwg (read-line f))

    (setq dwgs (cons dwg dwgs))

  )

  (close f)

 

  ;; Reverse the list, take the head and write

  ;; back the remainder to the file

 

  (if (> (length dwgs) 0)

    (progn

      (setq dwgs (reverse dwgs)

            dwg (car dwgs)

            dwgs (cdr dwgs)

      )

 

      (setq f (open data "w"))

      (foreach dwg dwgs

        (write-line dwg f)

      )

      (close f)

    )

  )

  dwg

)

 

;; Process the current drawing and use a script to open

;; the next one in the list

 

(defun process-next-drawing(data lsp func save first / scr)

  (setq scr "c:/tmp.scr")

 

  ;; We only want to run the function if not the first

  ;; time called... the same for save

 

  (if (not first)

    (progn

      (eval (read func))

      (if save

        (command "_.QSAVE")

      )

    )

  )

 

  ;; Get the next DWG name from the file

 

  (setq dwg (get-next-drawing data))

 

  ;; If there is one, create a script to open it, reload

  ;; the application and process our function

 

  (if dwg

    (progn

      (create-script scr data dwg lsp save first)

      (command "_.SCRIPT" scr)

    )

 

    ;; For the last drawing we simply close it and

    ;; delete the now-empty data file

 

    (progn

      (vl-file-delete data)

      (vl-file-delete scr)

      (command "_.CLOSE")

    )

  )

) 

 

;; Create a script to close the current drawing and

;; open the next, calling back to our process function

;; (after having loaded the file that defines it)

 

(defun create-script(scr data dwg lsp save first / f)

  (setq f (open scr "w"))

  (if (not first)

    (write-line "_.CLOSE" f)

  )

  (write-line

    (strcat "_.OPEN \"" dwg "\"") f

  )

  (write-line

    (strcat "(load \"" lsp "\")") f

  )

  (write-line

    (strcat

      "(process-next-drawing \""

      data "\" \"" lsp "\" \"" func "\" "

      (if save "T" "nil") " nil)"

    )

    f

  )

  (close f)

  (princ)

)

I hope this is useful to people – as mentioned before, please do provide feedback on how/whether this works for you…

June 23, 2009

And now for a real birthday…

Our first daughter, Anokhi, was born yesterday evening at 22:46 CET, weighing 3.38 kg and measuring 47.5 cm in length. Her older brothers Kalan and Zephyr were thrilled to meet her today.

Little Anokhi

I’ll be on vacation properly from next week - spending time with our newly super-sized family - but will probably manage to squeeze in another post or two before mother and baby get home from the hospital.

June 22, 2009

Batch-processing AutoCAD drawings from LISP without SDI

As mentioned in this previous post, there has been some discussion internally around the future of SDI. Given the change this will bring to applications, SDI is going to be around until we next deliberately choose to break binary application compatibility (something we just did with AutoCAD 2010 and typically try to do only every three releases).

That said, SDI is very likely to go away at some point, so it does seem worth drilling further into the reasons for using it and trying to determine an appropriate way to remove current dependencies on it.

Thanks to all of you who responded to my previous post and provided input on your use of SDI. The most common theme was around the use of SDI to batch-process sets of drawings: opening each one, performing an operation and (optionally) saving before opening the next.

My understanding is that SDI makes life easier, in this situation, because the closing of one drawing is taken care of automatically when opening the next and so LISP applications can operate more easily across multiple drawings.

I did some tests, to see how it helps, but the LISP part of my brain has unfortunately atrophied, over the years: I wasn’t able to get a simple SDI-only, batch processing application to work, as it always stopped once a new drawing was open. It may be that some use of script files is needed – and this certainly can make life easier, as we’ll see below – but it would be good if someone could help me out by posting a comment or dropping me an email. I’m sure I’m missing something very simple.

Anyway, irrespective of whether I was able to use SDI successfully, or not, I was able to get something working from an MDI environment that I hope to be equivalent, functionality-wise.

Thanks to guidance from Wayne Brill, a member of DevTech Americas, I was able to put together some LISP code that makes use of a temporary script file to handle the opening, processing, (saving) and closing of drawings.

Here’s the LISP application:

(defun C:BATCH(/ dwgs scr-name lsp-name)

  (setq dwgs '("C:/A.DWG" "C:/B.DWG" "C:/C.DWG" "C:/D.DWG")

        scr-name "c:/tmp.scr"

        lsp-name "c:/batch.lsp"

  )

  (create-script scr-name dwgs lsp-name "(CreateCircle)" T)

  (command "_.SCRIPT" scr-name)

  (vl-file-delete scr-name)

  (princ)

)

 

(defun CreateCircle()

  (command "_.CIRCLE" "0,0,0" "30")

)

 

(defun create-script(scr dwgs lsp cmd save / f dwg)

  (setq f (open scr "w"))

  (foreach dwg dwgs

    (progn

      (write-line

        (strcat "_.OPEN \"" dwg "\"") f

      )

      (write-line

        (strcat "(load \"" lsp "\")") f

      )

      (write-line cmd f)

      (if save

        (write-line "_.QSAVE" f)

      )

      (write-line "_.CLOSE" f)

    )

  )

  (close f)

  (princ)

)

The script handles the opening of each drawing, reloading the LISP file (which I have saved in c:/tmp.lsp – this file is pointed at by the lsp-name variable in the C:BATCH function) inside each one and running the specified command/function before saving & closing. In this case we’re running a simple function that uses a command to create a circle – if we were doing something that didn’t require the drawing to be saved (if we were just querying data, for instance) we could pass nil instead of T into the (create-script) function.

To take a look at the script being used behind the scenes, you can simply comment out the call to (vl-file-delete) and open up the contents in your favourite text editor:

_.OPEN "C:/A.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

_.OPEN "C:/B.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

_.OPEN "C:/C.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

_.OPEN "C:/D.DWG"

(load "c:/tmp.lsp")

(CreateCircle)

_.QSAVE

_.CLOSE

I hope this approach goes some way towards helping people batch-process drawings from LISP without having to move to a different language. I’d really appreciate your feedback on this subject, as it would be good to get a more definitive approach nailed down before this change (eventually) becomes a requirement.

June 19, 2009

Happy birthday to me… happy birthday to me…

Well, happy birthday to “Through the Interface”, anyway. :-)

3 years, 394 posts and 2,559 comments later, TTIF is still going strong. In fact, it’s now the 2nd most visited Autodesk blog, after Shaan’s (some of our blogs with shorter posts publish their full content via RSS, so this doesn’t necessarily completely reflect the relative readership, but hey).

Here’s a graph of the page hits over the last three years… I think the number on the Y-axis reflects the total page hits in a monthly window (at least that’s what seems to make sense). [I’m not too worried about the way the graph tails off to the right – that just seems to be the way our analysis tool works.]

TTIF - 3 years of page hits

I’ve personally gained a great deal from my blogging experience over the last three years – your questions and the need to post regularly have driven my personal learning and have led me to having a lot of fun. Long may it continue! :-)

Thanks for all your support, keep your ideas and suggestions flowing, and please keep on helping us to make our products the best development platforms we can.

June 17, 2009

A new project wizard for AutoCAD .NET development

Some of you may remember a philosophical question I raised in a post some time ago. The overall consensus of the feedback we received was to prioritise delivering code samples (given a choice between code samples and wizard-like code-generation tools). Which we are doing, both via the ADN site and our developer blogs… but that doesn’t mean some investment in code-generation tools isn’t also appropriate, from time-to-time. :-)

Cyrille Fauvel, who manages the Media & Entertainment arm of DevTech but also occasionally works on technical activities that relate to our other products, has put together a new (currently draft) version of the AutoCAD .NET Wizard. This tool integrates with the Visual Studio 2008 IDE (as well as the 2008 editions of Visual C# Express and Visual Basic Express), adding C#/VB.NET project templates for AutoCAD development. (This version of the tool only works with the 2008 versions – we’re currently evaluating whether (and how) to support the 2005 versions of Visual Studio [Express] or simply to recommend using our previously posted templates.)

When you select one of these “Autodesk” project templates you will get a skeleton project that defines the application initialization protocol plus a few placeholder commands while setting up the ability to define localized commands.

Here are a few images stepping you through the installation of this Wizard (nothing very surprising, which is why I’ve kept the thumbnails so small :-):

AutoCAD 2010 NET Wizards install AutoCAD 2010 NET Wizards install location AutoCAD 2010 NET Wizards install confirmation AutoCAD 2010 NET Wizards install progress AutoCAD 2010 NET Wizards install complete

[Note: the current installer requests an install location, which is unnecessary. The installer only uses this location for temporary files, removing them afterwards. This step will be removed from a future build of the tool.]

Once installed (and Visual Studio 2008 has been re-started), you will get some new options when you start a new project…

AutoCAD 2010 NET Wizards new project menu item

… that include the “AutoCAD 2010 plug-in” project template:

AutoCAD 2010 NET Wizards new project template

We’ve numbered these templates “2010”, as they provide support for API features delivered with AutoCAD 2010, but if you don’t select these features the created project should work with older versions of AutoCAD. At least that’s the theory. :-)

When you first select one of these project types, you will be asked to select the location of your ObjectARX SDK:

AutoCAD 2010 NET Wizards configurator

You should specify this along with the assemblies you would like to reference for this particular project:

AutoCAD 2010 NET Wizards configurated

On completion of this dialog your skeleton project will be created.

[Something else to note: the tool currently creates projects which allow addition of Windows Presentation Foundation content – to do this we had to add a little piece of XML to the project file that specifies a target .NET Framework version of 3.0 or above as well as a GUID to identify the project as WPF-compatible. This tells Visual Studio to list WPF item-types when you add new items to the project, but I expect this to become another configuration option in a future version as some developers will want to target framework versions where WPF is unavailable.]

Let’s take a look at the skeleton code created for a C# project, reformatted to fit the width of the blog…

First the myPlugin.cs file:

using System;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.EditorInput;

 

// This line is not mandatory, but improves loading performance

[assembly: ExtensionApplication(

  typeof(Autodesk.AutoCAD.AutoCAD_2010_plug_in1.MyPlugin)

)]

 

namespace Autodesk.AutoCAD.AutoCAD_2010_plug_in1

{

 

  // This class is instantiated by AutoCAD once and kept alive for

  // the duration of the session. If you don't do any one time

  // initialization then you should remove this class.

  public class MyPlugin : IExtensionApplication

  {

 

    void IExtensionApplication.Initialize()

    {

      // Add one time initialization here

      // One common scenario is to setup a callback function here

      // that unmanaged code can call.

      // To do this:

      // 1. Export a function from unmanaged code that takes a

      //    function pointer and stores the passed in value in a

      //    global variable.

      // 2. Call this exported function in this function passing

      //    delegate.

      // 3. When unmanaged code needs the services of this managed

      //    module you simply call acrxLoadApp() and by the time

      //    acrxLoadApp returns  global function pointer is

      //    initialized to point to the C# delegate.

      // For more info see:

      //http://msdn2.microsoft.com/en-US/library/5zwkzwf4(VS.80).aspx

      //http://msdn2.microsoft.com/en-us/library/44ey4b32(VS.80).aspx

      //http://msdn2.microsoft.com/en-US/library/7esfatk4.aspx

      // as well as some of the existing AutoCAD managed apps.

 

      // Initialize your plug-in application here

    }

 

    void IExtensionApplication.Terminate()

    {

      // Do plug-in application clean up here

    }

  }

}

And now the myCommands.cs file:

using System;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.EditorInput;

 

// This line is not mandatory, but improves loading performances

[assembly: CommandClass(

  typeof(Autodesk.AutoCAD.AutoCAD_2010_plug_in1.MyCommands)

)]

 

namespace Autodesk.AutoCAD.AutoCAD_2010_plug_in1

{

 

  // This class is instantiated by AutoCAD for each document when

  // a command is called by the user the first time in the context

  // of a given document. In other words, non static data in this

  // class is implicitly per-document!

  public class MyCommands

  {

    // The CommandMethod attribute can be applied to any public

    // member function of any public class.

    // The function should take no arguments and return nothing.

    // If the method is an intance member then the enclosing class is

    // intantiated for each document. If the member is a static

    // member then the enclosing class is NOT intantiated.

    //

    // NOTE: CommandMethod has overloads where you can provide helpid

    // and context menu.

 

    // Modal Command with localized name

    [CommandMethod(

      "MyGroup", "MyCommand", "MyCommandLocal", CommandFlags.Modal

    )]

    public void MyCommand() // This method can have any name

    {

      // Put your command code here

    }

 

    // Modal Command with pickfirst selection

    [CommandMethod(

      "MyGroup", "MyPickFirst", "MyPickFirstLocal",

      CommandFlags.Modal | CommandFlags.UsePickSet

    )]

    public void MyPickFirst() // This method can have any name

    {

      PromptSelectionResult result =

        Application.DocumentManager.MdiActiveDocument.Editor.

          SelectImplied();

      if (result.Status == PromptStatus.OK)

      {

        // There are selected entities

        // Put your command using pickfirst set code here

      }

      else

      {

        // There are no selected entities

        // Put your command code here

      }

    }

 

    // Application Session Command with localized name

    [CommandMethod(

      "MyGroup", "MySessionCmd", "MySessionCmdLocal",

      CommandFlags.Modal | CommandFlags.Session

    )]

    public void MySessionCmd() // This method can have any name

    {

      // Put your command code here

    }

 

    // LispFunction is similar to CommandMethod but it creates a

    // lisp callable function. Many return types are supported not

    // just string or integer.

    [LispFunction("MyLispFunction", "MyLispFunctionLocal")]

    public int MyLispFunction(ResultBuffer args)

    // This method can have any name

    {

      // Put your command code here

 

      // Return a value to the AutoCAD Lisp Interpreter

      return 1;

    }

  }

}

The myCommands.resx file needed to define the local command names (for the MySessionCmd command) has also been added to the project automatically to help with command localization.

All well and good… but it may be that these project templates don’t quite fit your internal needs, whether due to the copyright notices in the source and assembly properties [something I do expect to be removed from a future build – it doesn’t make sense for Autodesk copyright notices to be placed in your project skeleton] or because you have a standard approach you’ve adopted for command registration (or whatever). The good news is that it’s pretty easy for you to modify these baseline templates for your own needs:

  1. Locate the template you wish to update, e.g. “C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\ProjectTemplates\CSharp\Autodesk\AutoCAD 2010 plug-in.zip” (this path will vary if you’re running on a 64-bit or localized OS or if you’re using an Express version of Visual Studio, but the technique should still work).
  2. Unzip the files somewhere.
  3. Modify the source code and project setup to meet your needs.
  4. Modify the template file (MyTemplate.vstemplate – an XML file providing Visual Studio with additional information not stored in the project), as needed.
  5. ZIP the files back up, choosing a new name for the .ZIP archive.
  6. Post the file within the ProjectTemplates folder structure.
  7. Run Visual Studio with the /InstallVSTemplates command parameter (the easiest way is to open a “Visual Studio 2008 Command Prompt” and use it to execute “devenv /InstallVSTemplates”).

As mentioned a few times, I do expect us to provide a new version of this tool with some minor wrinkles ironed out, but it’s pretty much ready-to-go (and certainly ready for people to try out and respond with their feedback :-). Please let us know how you get on, whether by posting a comment or by emailing our wizard developers.

June 15, 2009

Highlighting named blocks using AutoCAD 2010’s overrule API from .NET

This is a nice sample provided by Stephen Preston, who manages DevTech’s Americas team. Stephen has put this together in anticipation of his upcoming AU class on the overrule API introduced in AutoCAD 2010. [I know the final class list has not yet been announced, but Stephen is co-owner of the Customization & Programming track at this year’s AU and presumably has the inside skinny on the selected classes. Which means he has a head-start on preparing his material, lucky fellow. :-)]

The sample allows the user to enter a text string that it uses to highlight any block containing that string in its name. This is quite handy for identifying the instances of a particular block in a drawing, but it might also be modified to highlight other objects (you might want to highlight mis-spelt words or standards violations, for instance).

Here’s the C# code, reformatted for this blog:

using System;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.GraphicsInterface;

 

namespace MyCustomFilterOverrule

{

  // This is our custom DrawableOverrule class. We're just

  // overruling WorldDraw and IsApplicable.

  // This class is implemented as a singleton class, and

  // includes subroutines that are called by the CommandMethods

  // in another class

 

  public class MyDrawOverrule : DrawableOverrule

  {

    // Where properties have been defined, use the property rather

    // than the raw variable.

    // I'm using properties where I need some additional logic to

    // run as I get/set the variable.

 

    // The text we'll search for in our block name.

 

    private string mTxt;

 

    // Color Index of block highlight

 

    private short mColor = 3;

 

    // Used to track whether this Overrule has been registered

    // (so we don't try to register it more than once).

 

    private bool mRegistered = false;

 

    // Used to store one and only instance of our singleton class

 

    private static MyDrawOverrule mSingleton;

 

    // Used to reset Overruling value to the value it had before

    // we switched them on. (There may be other overrules in place)

 

    private static bool mOldOverruleValue;

 

    // The color we highlight blocks with

    private short HighlightColor

    {

      get { return mColor; }

      set { if (value >= 0 && value <= 127) mColor = value; }

    }

 

    // The text we'll search for in the block name

 

    private string SearchText

    {

      get { return mTxt; }

      set { mTxt = value; }

    }

 

    // Private constructor because its a singleton

 

    private MyDrawOverrule()

    {

      // Do nothing

    }

 

    // Shared propery to return our singleton instance

    // (and instantiate new instance on first call)

 

    public static MyDrawOverrule GetInstance

    {

      get

      {

        if (mSingleton == null)

        {

          mSingleton = new MyDrawOverrule();

        }

        return mSingleton;

      }

    }

 

    private void InitOverrule()

    {

      if (!mRegistered)

      {

        Overrule.AddOverrule(

          RXObject.GetClass(typeof(BlockReference)), this, false

        );

        SetCustomFilter();

        mOldOverruleValue = Overrule.Overruling;

        mRegistered = true;

      }

      Overrule.Overruling = true;

    }

 

    // Prompts user to select the color index they want to

    // highlight blocks with

 

    public void SetColor()

    {

      Editor ed =

        Application.DocumentManager.MdiActiveDocument.Editor;

 

      PromptIntegerOptions opts =

        new PromptIntegerOptions(

          "\nEnter block finder color index: "

        );

      opts.DefaultValue = HighlightColor;

      opts.LowerLimit = 0;

      opts.UpperLimit = 127;

      opts.UseDefaultValue = true;

      PromptIntegerResult res = ed.GetInteger(opts);

 

      // If requested highlight color is a new color,

      // then we want to change it

 

      if (res.Status == PromptStatus.OK &&

          HighlightColor != res.Value)

      {

        HighlightColor = (short)res.Value;

 

        // Regen is required to update changes on screen

 

        ed.Regen();

      }

    }

 

    public void FindText()

    {

      Editor ed =

        Application.DocumentManager.MdiActiveDocument.Editor;

 

      ed.WriteMessage(

        "\nCurrent block search text is \"{0}\".", SearchText

      );

      PromptStringOptions opts =

        new PromptStringOptions(

          "\nEnter new block search text: "

        );

      PromptResult res = ed.GetString(opts);

 

      // If the user cancelled then we exit the command

 

      if (res.Status != PromptStatus.OK)

        return;

 

      // If the user didn't type any text then we remove

      // the overrule and exit

 

      if (res.StringResult == "")

      {

        SearchText = "";

        ResetBlocks();

      }

      else

      {

        // Set search text for Overrule to that entered by user

 

        SearchText = res.StringResult.ToUpper();

        InitOverrule();

 

        // Turn Overruling on

 

        Overrule.Overruling = true;

 

        // Regen is required to update changes on screen.

 

        ed.Regen();

      }

    }

 

    // Removes our overrules

 

    public void ResetBlocks()

    {

      Editor ed =

        Application.DocumentManager.MdiActiveDocument.Editor;

 

      Overrule.Overruling = mOldOverruleValue;

      if (mRegistered)

      {

        Overrule.RemoveOverrule(

          RXObject.GetClass(typeof(BlockReference)), this

        );

        mRegistered = false;

        ed.Regen();

      }

    }

 

    // Overrule WorldDraw so we can draw our additional

    // graphics

 

    public override bool WorldDraw(Drawable drawable, WorldDraw wd)

    {

      // Better safe than sorry - check it really is a

      // BlockReference before continuing.

 

      BlockReference br = drawable as BlockReference;

      if (br != null)

      {

        // Now we want to draw a green box around the attributes

        // extents

 

        Extents3d ext = (Extents3d)br.Bounds;

        Point3d maxPt = ext.MaxPoint;

        Point3d minPt = ext.MinPoint;

        Point3dCollection pts = new Point3dCollection();

 

        // These are the vertices of the highlight box

 

        pts.Add(new Point3d(minPt.X, minPt.Y, minPt.Z));

        pts.Add(new Point3d(minPt.X, maxPt.Y, minPt.Z));

        pts.Add(new Point3d(maxPt.X, maxPt.Y, minPt.Z));

        pts.Add(new Point3d(maxPt.X, minPt.Y, minPt.Z));

 

        // Store current filltype and set to FillAlways

 

        FillType oldFillType = wd.SubEntityTraits.FillType;

        wd.SubEntityTraits.FillType = FillType.FillAlways;

 

        // Store old graphics color and set to the color we want

 

        short oldColor = wd.SubEntityTraits.Color;

        wd.SubEntityTraits.Color = HighlightColor;

 

        // Draw the filled polygon

 

        wd.Geometry.Polygon(pts);

 

        // Restore old settings

 

        wd.SubEntityTraits.FillType = oldFillType;

        wd.SubEntityTraits.Color = oldColor;

      }

 

      // Let the overruled Drawable draw itself.

 

      return base.WorldDraw(drawable, wd);

    }

 

    // This function is called if we call SetCustomFilter on our

    // custom overrule.

    // We add our own code to return true if the BlockReference

    // passed in is one we want to highlight.

 

    public override bool IsApplicable(RXObject overruledSubject)

    {

      // If it's a BlockReference, we check if the Block Name

      // contains our string

 

      BlockReference br = overruledSubject as BlockReference;

      if (br != null && SearchText != "")

      {

        // Returns whether the filter is applicable to this object

 

        return br.Name.Contains(SearchText);

      }

 

      // Only get to here if object isn't a BlockReference

 

      return false;

    }

  }

 

  // Our command class, which relays commands to MyDrawOverrule.

 

  public class myPlugin

  {

    [CommandMethod("SHOWBLOCKS")]

    public static void FindText()

    {

      MyDrawOverrule.GetInstance.FindText();

    }

 

    [CommandMethod("SHOWCOLOR")]

    public static void SetColor()

    {

      MyDrawOverrule.GetInstance.SetColor();

    }

  }

}

Here’s what happens when we OPEN the “Mechanical – Multileaders.dwg” sample drawing, NETLOAD our application and use SHOWBLOCKS to look for the “M045” text string:

Highlighted blocks

Here’s what we see if we broaden the search to include all blocks with the string “M0” in their name and change the highlight colour to 1 using SHOWCOLOR:

More highlighted blocks

To clear the selection, the user simply has to run SHOWBLOCKS and specify an empty string as the search term.

Stephen will be presenting both C# and VB.NET versions of this sample application during his class at this year’s AU. If you find overrules interesting, then I strongly recommend signing up for the session (I’ll let you know when registrations are open). I’m sure that during the class Stephen will be demonstrating other interesting capabilities made available to AutoCAD .NET developers by this very cool API.

June 12, 2009

A learning path for newbie programmers

I’m working to develop a “learning path” for people who know one or more Autodesk products from a user perspective but are interested in getting more into programming them: perhaps during their engineering studies they took one or two programming classes (perhaps not), and now feel that implementing some level of automation would be beneficial to themselves or to their colleagues. Or perhaps their boss has just told them to find out how all this stuff works. Or – and this is an altogether more sobering alternative – they have recently lost their jobs and are contemplating a career change, taking advantage of the Autodesk Assistance Program to get access to Autodesk software and are looking to complement existing design skills with the ability to customize and develop for our software.

I’d really like to hear your opinion on this topic, whatever your own personal motivation for learning programming. I knew programming before I knew AutoCAD, so I came at this from another direction and to some degree find myself too far along my own learning path to properly assess the current tools for learning programming fundamentals (it was just so long ago that I went through this myself). So this post is really a call for help: if any readers of this blog have recently tried to learn how to program for Autodesk software (or any software, for that matter, as I’m most interested in the resources related to the core programming fundamentals, rather than the product-specific side of things) then I’d very much appreciate you letting me know what worked for you and what didn’t.

Some examples: Microsoft provides an online learning service called Ramp Up – have any of you tried it to learn programming with .NET? What good books or – ideally – free resources have you used to get started with programming? Please post a comment or send me an email.

June 10, 2009

Updated DevTV: AutoCAD VBA to .NET Migration Basics

Stephen Preston has updated his “AutoCAD VBA to .NET Migration Basics” DevTV session.

I’ve updated the links in this previous post to point to the updated DevTV, as the original recording is likely to disappear during the coming days.

Here’s an update from Stephen on what’s new in this version:

New features of the migration tool:

  • Automatically adds ‘ObjectDBX’ Type Library to created VB6 project file to ensure Autodesk.AutoCAD.Interop.Common classes are correctly identified (previously these came in as Autodesk.AutoCAD.Interop)
  • Extra functionality to post-process your VB Express project for you:
    - Adds F5 startup application
    - Adds references to acmgd and acdbmgd dlls from the ObjectARX SDK
    - Sets project to use .NET Framework 3.5.

The first part of the recording is the same, but I’ve added a more detailed explanation of the migration tool functionality, and re-recorded the demos to include event handling.

Please check it out and pass on your feedback to Stephen - the comments he received about the original version were instrumental in steering this update.

June 08, 2009

Registering AutoCAD commands with localized names using .NET

While I was preparing this recent post I put together a simple project which registered localized names for commands, to make sure they were picked up and used to create the appropriate demand-loading Registry keys. It was a little tricky to do this from the current documentation, so thankfully I had access to this DevNote on the ADN site which helped a great deal (this is only available to ADN members but don’t worry if you’re not one: this post should provide equivalent information and in certain ways goes beyond the original example).

Anyway, it seemed a relevant topic to cover in its own post, so here we are today looking at this question: how to register localized command-names – possibly for multiple target languages – for your AutoCAD commands.

First of all we need to register some commands, the code for which proves to be pretty straightforward. Here’s the C# code defining our commands, which we will save in a file named Commands.cs:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

 

namespace LocalizedCommands

{

  public class LocCmds

  {

    [CommandMethod(

      "LOCCMDS", "HELLO", "helloCmdId", CommandFlags.Modal

    )]

    public void HelloCommand()

    {

      Application.DocumentManager.MdiActiveDocument.Editor.

        WriteMessage("\nHello!");

    }

 

    [CommandMethod(

      "LOCCMDS", "GOODBYE", "goodbyeCmdId", CommandFlags.Modal

    )]

    public void GoodbyeCommand()

    {

      Application.DocumentManager.MdiActiveDocument.Editor.

        WriteMessage("\nGoodbye!");

    }

  }

}

You’ll notice straight away that we’re using a special version of the CommandMethod attribute which specifies more than the usual items (command name and flags are the most common ones). We start with the command group (“LOCCMDS”), the global names (“HELLO” and “GOODBYE”), the localized resource IDs (“helloCmdId” and “goodbyeCmdId”) and then come the flags (CommandFlags.Modal).

The localized command name is provided as a resource ID to aid localization. Let’s see how this helps… we’ll add some resources to the project for our neutral culture (US English) and for two additional cultures (French and German).

We can add resource files by right-clicking the project and selecting Add –> New Item:

Add a new item to the project

From here we can use the Add New Item dialog to add new resource files with the names “Commands.resx” (which is for the neutral culture, US English), “Commands.fr-FR.resx” and “Commands.de-DE.resx” (these three files will need to be added one-by-one). It’s important that the resource files use the same base name as the .cs file (they do not have to use the class name – LocCmds – and, in fact, you may run into name collisions if you attempt to do so).

Adding a new resource file

Our solution

 

So far, so good. We should now see the resultant files in our solution explorer, and be able to open them by double-clicking them.

The files will be blank initially, and we will want to add two string resources to each, one for each of our commands, using the IDs “helloCmdId” and “goodbyeCmdId”.

 

Question

 

When we come to edit the individual string resources, we may be presented with a warning dialog (it is safe to select Yes):

 

Now we can go ahead and add localized string resources for our two commands in each of the three cultures:

Our resources

It should now be possible to build our application. If we take a look at the output folder, we should see sub-folders for each of our cultures:

Directory structure with resource DLLs

The en-US folder will be empty, as we’ve included that as our base culture, but the other two will contain resource DLLs (also known as satellite assemblies) containing our localized strings for that target culture.

Now to see these in action. In order to fake the loading of our assembly into different language versions of AutoCAD, I put together a simple application to change the UI culture (the one used by the resource manager to choose the resources to load) of the current thread. This clearly needs to be in a separate assembly, as we want to change the culture before we use NETLOAD to load the assembly containing our localized commands.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

using System.Threading;

using System.Globalization;

 

namespace CultureShift

{

  public class Commands

  {

    private void setCulture(string culture)

    {

      Thread.CurrentThread.CurrentUICulture =

        new CultureInfo(culture);

    }

 

    private void setNeutralCulture(string neutCult)

    {

      Thread.CurrentThread.CurrentUICulture =

        CultureInfo.CreateSpecificCulture(neutCult);

    }

 

    [CommandMethod("SETFR")]

    public void SetFrenchCulture()

    {

      setNeutralCulture("fr"); // or setCulture("fr-FR");

      GetCulture();

    }

 

    [CommandMethod("SETDE")]

    public void SetGermanCulture()

    {

      setNeutralCulture("de"); // or setCulture("de-DE");

      GetCulture();

    }

 

    [CommandMethod("SETEN")]

    public void SetEnglishCulture()

    {

      setNeutralCulture("en"); // or setCulture("en-US");

      GetCulture();

    }

 

    [CommandMethod("GETCUL")]

    public void GetCulture()

    {

      Application.DocumentManager.MdiActiveDocument.Editor.

        WriteMessage(

          "\nCurrent UI culture is {0}.",

          Thread.CurrentThread.CurrentUICulture.Name

        );

    }

  }

}

This code implements a number of commands to set the UI culture to US English (SETEN), French (SETFR) and German (SETDE) as well as to check the current UI culture (GETCUL). The code chooses to set the culture neutrally – which sets the language but not the location – but the end result is the same for the cultures we’ve chosen. One thing to bear in mind: this code only actually works if running from the debugger. It doesn’t crash, otherwise, but the current UI culture always ends up as “en-US”. I assume this is a threading issue, but as this is really only for testing purposes it’s a tolerable requirement to run everything from the debugger.

Here’s what happens when (launching AutoCAD from the Visual Studio 2009 debugger) we load our test code, set the current UI culture to French and then load the main application, checking which commands work and which do not:

Command: NETLOAD

Command: GETCUL

Current UI culture is en-US.

Command: SETFR

Current UI culture is fr-FR.

Command: GETCUL

Current UI culture is fr-FR.

Command: NETLOAD

Command: HELLO

Hello!

Command: GOODBYE

Goodbye!

Command: BONJOUR

Hello!

Command: AUREVOIR

Goodbye!

Command: GUTENTAG Unknown command "GUTENTAG".  Press F1 for help.

Command: BYE Unknown command "BYE".  Press F1 for help.

We can see that the global and the French-localized commands have indeed worked, while the German and US ones have not.

Let’s do the same for German:

Command: NETLOAD

Command: GETCUL

Current UI culture is en-US.

Command: SETDE

Current UI culture is de-DE.

Command: GETCUL

Current UI culture is de-DE.

Command: NETLOAD

Command: HELLO

Hello!

Command: GOODBYE

Goodbye!

Command: BONJOUR Unknown command "BONJOUR".  Press F1 for help.

Command: GUTENTAG

Hello!

Command: AUFWIEDERSEHEN

Goodbye!

Command: BYE Unknown command "BYE".  Press F1 for help.

And sure enough, while the German and global commands work, the others do not.

For completeness, if we just load our application – without either launching from the debugger or loading our test application to change the current UI culture – and run the commands, here’s what we see:

Command: NETLOAD

Command: HELLO

Hello!

Command: GOODBYE

Goodbye!

Command: HI

Hello!

Command: BYE

Goodbye!

Command: BONJOUR Unknown command "BONJOUR".  Press F1 for help.

Command: AUREVOIR Unknown command "AUREVOIR".  Press F1 for help.

Command: GUTENTAG Unknown command "GUTENTAG".  Press F1 for help.

Command: AUFWIEDERSEHEN Unknown command "AUFWIEDERSEHEN".  Press F1 for help.

My assumption is that the current UI culture will be set appropriately in the different language versions of AutoCAD, and so the localized resources will be chosen correctly. If someone trying the technique for real were able to confirm, I'd certainly appreciate it. :-)

June 04, 2009

Creating Fibonacci spirals in AutoCAD using F#

I recently stumbled across this post which inspired me to do something similar in AutoCAD (the fact that both posts cover Fibonacci spirals and use F# is about where the similarity ends - they do things quite differently).

Fibonacci spirals are an approximation of the golden spiral, which for old timers out there will be reminiscent of the AutoCAD R12 (it was R12, wasn’t it?) design collateral - the same as this one from AME 2.1 - which I still find cool after all these years. :-)

The first thing was to create a function that returns a portion of the Fibonacci sequence:

let fibs n =

  Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) (0I,1I)

  |> Seq.take n

  |> Seq.to_list

A few comments about this implementation:

  • I searched online for tail-recursive Fibonacci implementations (not that we’re likely to create a stack overflow with the number of recursions we’re going to do, but I like to do things right when I can :-)
    • Tail-recursive solutions are easy if returning a specific number, but as we need to return a portion of the Fibonacci sequence things get a little more complicated
    • Here’s a quick reminder of how we can check that tail call optimization has happened, when we do choose to use tail recursion
  • I ended up going for a lazily-evalulated solution, copied and modified from the Foundations of F# book by Robert Pickering
    • Unfold (OK – I admit this Wikipedia entry is beyond obscure for most of us mere mortals – this post may be of more help) applies a function to a seed value to create what may be an infinite sequence of numbers (which is precisely what the Fibonacci sequence is, of course)
    • We use the Seq data-type (essentially an IEnumerable in .NET) which is lazy
      • This means that it only actually evaluates the various items in the list as we ask for them
    • We then “take” the first n items from the list (n will be specified by the user), so only that number of items get evaluated
    • We convert the results to a list to return to the caller
  • I like the elegance of this solution and it’s certainly efficient enough for our purposes

If you load this code into F# interactive and execute it against the first 20 numbers in the sequence, you get:

> fibs 20;;

val it : bigint list

= [0I; 1I; 1I; 2I; 3I; 5I; 8I; 13I; 21I; 34I; 55I; 89I; 144I; 233I; 377I; 610I;

  987I; 1597I; 2584I; 4181I]

Otherwise the below implementation should be reasonably straightforward. We define a local addSegment function which we then call on each member of the subset of the Fibonacci sequence (in reverse, as we’re drawing the curves from large to small). We use the iteri function to do this, as it provides us with a useful index into the list which then allows us to decide which of the four directions we’re facing (the orientation of the arc rotates 90 degrees each time).

Here’s the complete F# code:

#light

 

// Declare our namespace and module name

 

module Fibonacci.Spiral

 

// Import managed assemblies

 

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open System

 

// A lazy Fibonacci sequence generator

 

let fibs n =

  Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) (0I,1I)

  |> Seq.take n

  |> Seq.to_list

 

[<CommandMethod("fib")>]

let fibonacciSpiral() =

 

  // Let's get the usual helpful AutoCAD objects

 

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

 

  // Ask the user how deep to go

 

  let pio =

    new PromptIntegerOptions("\nEnter number of levels: ")

  pio.AllowNone <- true

  pio.AllowZero <- false

  pio.AllowNegative <- false

  pio.DefaultValue <- 10

  pio.LowerLimit <- 1

  pio.UpperLimit <- 50

  pio.UseDefaultValue <- true

 

  let pir = ed.GetInteger(pio)

 

  if pir.Status = PromptStatus.OK then

 

    // We'll actually add one to the value provided

    // as this gives more logical results

 

    let levels = pir.Value + 1

 

    // "use" has the same effect as "using" in C#

 

    use tr =

      db.TransactionManager.StartTransaction()

 

    // Get appropriately-typed BlockTable and BTRs

 

    let bt =

      tr.GetObject

        (db.BlockTableId,OpenMode.ForRead)

      :?> BlockTable

 

    let ms =

      tr.GetObject

        (bt.[BlockTableRecord.ModelSpace],

        OpenMode.ForWrite)

      :?> BlockTableRecord

 

    // Create our polyline, set its defaults,

    // add it to the modelspace and the transaction

 

    let pl = new Polyline()

    pl.SetDatabaseDefaults()

    ms.AppendEntity(pl) |> ignore

    tr.AddNewlyCreatedDBObject(pl, true)

    pl.AddVertexAt

      (pl.NumberOfVertices, Point2d.Origin, 0.0, 0.0, 0.0)

 

    // We need a mutable start point variable for

    // each of our arcs to connect

 

    let start = ref Point3d.Origin

 

    // Add an arc segment to our polyline

 

    let addSegment i size =

 

      // i is the index in the list provided by iteri

 

      // Decide the directions of the "axes" and the arc's start

      // angle based on one of four possibilities

 

      let (xdir, ydir, startAngle) =

        match (i % 4) with

        | 0 -> (Vector3d.XAxis, Vector3d.YAxis, 0.0)

        | 1 -> (-Vector3d.YAxis, Vector3d.XAxis, Math.PI * 1.5)

        | 2 -> (-Vector3d.XAxis, -Vector3d.YAxis, Math.PI)

        | 3 -> (Vector3d.YAxis, -Vector3d.XAxis, Math.PI / 2.0)

        | _ -> failwith "Invalid modulus remainder!"

 

      // The end angle is 90 degrees from the start

 

      let endAngle = startAngle + Math.PI / 2.0

 

      // The center of the arc is bottom right-hand corner

      // of the box (direction goes along the bottom from

      // left to right, so we go "size" units along the

      // direction from the start point)

 

      let center = !start + xdir * float size

 

      // Bulge is defined as the tan of one quarter of the

      // included angle (and negative, as we're going

      // clockwise)

 

      let bulge = Math.Tan((endAngle - startAngle) / -4.0)

 

      // We need to convert our 3D start point to a 2D point

      // on the plane of the polyline

 

      let pos = (!start).Convert2d(pl.GetPlane())

 

      // Now we just add the vertex at the end and mutate our

      // start variable to contain the end of the arc

 

      pl.AddVertexAt(pl.NumberOfVertices, pos, bulge, 0.0, 0.0)

      start.contents <- center + ydir * float size

 

    // Here's where we plug it all together...

 

    // Get the first n fibonacci numbers, reverse the list and

    // call our function on each one (passing its index along,

    // too)

 

    fibs levels |> List.rev |> List.iteri addSegment

    tr.Commit()

Here’s what happens when we run the FIB command and select levels 1 to 8 (we need to call FIB eight times to do this), creating eight different Fibonacci spirals:

Levels 1 to 8 of our Fibonacci spiral

And here’s the result for level 50, as a comparison (although unless you zoom right in it might as well be level 20):

Level 50 Fibonacci spiral

RSS Feed

Search