December 2014

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 31      










« Overruling AutoCAD 2010’s entity display and explode using IronRuby | Main | Enabling AutoCAD’s offset to work on the contents of xrefs using .NET »

May 13, 2009

Using Boo with AutoCAD

I might also have called this post “Overruling AutoCAD 2010’s entity display and explode using Boo”, as it complements the equivalent posts for C#, F#IronPython and IronRuby, but I felt it appropriate to combine the post with an introduction to what Boo is all about.

Boo logo What is Boo and how did I come to look into it? Knowing of my recent interest in the various scripting technologies being made available for .NET, a colleague at Autodesk recently pointed me at the Boo programming language (and here is the official page for the language, including its various downloads).

First, to get this out the way: Boo is really cool. I can relate to the drivers behind it, which were to create a statically-typed language based on Python that works really well with .NET. And with many of the rough edges smoothed away, especially those related to object-orientation, which tends to feel as though it was bolted on to Python as an afterthought.

As it’s statically-typed, Boo provides compile-time code checking and in theory could also provide intellisense and code completion (to which I unfortunately find myself increasingly addicted). It uses type inference – just as F# does – so there’s no need for the complex variable declarations we’re used to seeing in C#.

I’ve had fun playing around with dynamically typed languages (such as IronPython and IronRuby, which are based on Microsoft’s Dynamic Language Runtime), but for now I still find the benefits brought by static typing to be compelling, especially when you have to launch a relatively heavyweight desktop application to run your code. As far as I can tell much of the productivity benefit of working with dynamic languages in such an environment come from using an integrated REPL – just as AutoLISP has via AutoCAD’s command-line – but right now we don’t have that kind of capability built into our products for other scripting languages.

While primarily statically-typed, Boo also has dynamic capabilities. For instance, you can very easily make use of duck typing within your code, just as we will get with C# 4.0’s dynamic keyword. Duck typing allows you to get away from deriving sub-classes and implementing interfaces: as long as you expose appropriately-named methods from your object, they will get called (for those of you who aren’t aware, the term comes from the phrase “If it walks like a duck and quacks like a duck, it must be a duck.” :-).

Boo is compiled: it allows you to create executables and class libraries (.DLLs) with custom .NET attributes, which makes it much easier to implement commands inside AutoCAD (we can use the standard NETLOAD command directly, rather than needing to implement something like PYLOAD and RBLOAD). Boo doesn’t use the DLR, as it’s not really a dynamic language, which means the IL generated is similar to what you’d expect from C# or VB.NET. Take a look using Reflector, if you’re interested.

How did I get started with Boo? My first step was to install BooLangStudio, a Boo integration for Visual Studio 2008. I didn’t even need to download the Boo language separately, which was nice. The BooLangStudio integration is still in Alpha but generally works pretty well. Aside from missing intellisense I also found that I wasn’t able to debug into Class Libraries, but in theory both of these capabilities are altogether doable. Oh, and I had a problem using the CopySourceAsHtml plugin, which I ended up having to rebuild to workaround an issue with the default tab size not being available for .boo source files.

To get the code working, I started by copying the Python code from the post I mentioned earlier and then proceeded to update it for the differences Boo has with Python.

I very much liked the OO- and CLR-related changes:

  • You specifically identify virtual method implementations with the override keyword
    • On the downside I found it important to nail down argument types with the as keyword, which is slightly more cumbersome
      • Unless you use duck typing you will generally need to cast more often, but that seems acceptable
  • The somewhat ugly __init__ methods are now named constructor, instead (yay)
  • The self argument is now implicit (double-yay)

All very logical modifications to the Python language, in my opinion.

Some additional things to call out:

  • Boo doesn’t yet support global variables, so these had to be declared as static members of a class
  • There were a few minor issues with enumerations that needed working around (but these were thankfully compile-time errors, so easy to chase down)

Here’s the Boo code:

namespace OverruleSample

 

import System

import Autodesk.AutoCAD.Runtime

import Autodesk.AutoCAD.ApplicationServices

import Autodesk.AutoCAD.DatabaseServices

import Autodesk.AutoCAD.EditorInput

import Autodesk.AutoCAD.GraphicsInterface

import Autodesk.AutoCAD.Geometry

import Autodesk.AutoCAD.Colors

 

# Set some global constants & variables

 

class Globals: 

  public static appName = "TTIF_PIPE"

  public static appCode = 1001

  public static radCode = 1040

  public static radius = 0.0

  public static overruling = false

 

class PipeDrawOverrule(DrawableOverrule):

 

  /*

  The base class for our draw overrules specifying the

  registered application name for the XData upon which

  to filter

  */

 

  protected _sweepOpts

 

  def constructor():

 

    /*

    Tell AutoCAD to filter on our application name

    (this means our overrule will only be called

    on objects possessing XData with this name)

    */

 

    cast(DrawableOverrule, self).SetXDataFilter(Globals.appName)

 

class LinePipeDrawOverrule(PipeDrawOverrule):

 

  /*

  An overrule to make a pipe out of a line

  */

 

  public static theOverrule = LinePipeDrawOverrule()

 

  def constructor():

    _sweepOpts = SweepOptions()

    super()

 

  override def WorldDraw(d as Drawable, wd as WorldDraw):

 

    radius = PipeRadiusForObject(d)

 

    if radius > 0.0:

      # Draw the line as is, with overruled attributes

 

      super(d, wd)

 

      ln = d as Line

 

      if not d.Id.IsNull and ln.Length > 0.0:

        # Draw a pipe around the line

 

        c = wd.SubEntityTraits.TrueColor

        wd.SubEntityTraits.TrueColor = EntityColor(0x00AFAFFF)

        wd.SubEntityTraits.LineWeight = LineWeight.LineWeight000

        start = ln.StartPoint

        end = ln.EndPoint

        norm = Vector3d(

          end.X - start.X,

          end.Y - start.Y,

          end.Z - start.Z

        )

        clr = Circle(start, norm, radius)

        pipe = ExtrudedSurface()

        try:

          pipe.CreateExtrudedSurface(clr, norm, _sweepOpts)

        except:

          doc = Application.DocumentManager.MdiActiveDocument

          doc.Editor.WriteMessage(

            "\nFailed with CreateExtrudedSurface."

          )

        clr.Dispose()

        pipe.WorldDraw(wd)

        pipe.Dispose()

        wd.SubEntityTraits.TrueColor = c

      return true

    return super(d, wd)

 

  override def SetAttributes(d as Drawable, t as DrawableTraits):

 

    b = super(d, t)

 

    radius = PipeRadiusForObject(d)

 

    if radius > 0.0:

      # Set color to magenta

      t.Color = 6

      # and lineweight to .40 mm

      t.LineWeight = LineWeight.LineWeight040

 

    return b

 

class CirclePipeDrawOverrule(PipeDrawOverrule):

 

  /*

  An overrule to make a pipe out of a circle

  */

 

  public static theOverrule = CirclePipeDrawOverrule()

 

  def constructor():

    _sweepOpts = SweepOptions()

    super()

 

  override def WorldDraw(d as Drawable, wd as WorldDraw):

 

    radius = PipeRadiusForObject(d)

 

    if radius > 0.0:

      # Draw the circle as is, with overruled attributes

 

      super(d, wd)

 

      cir = d as Circle

 

      # Needed to avoid ill-formed swept surface

 

      if cir.Radius > radius:

        # Draw a pipe around the circle

 

        c = wd.SubEntityTraits.TrueColor

        wd.SubEntityTraits.TrueColor = EntityColor(0x3FFFE0E0)

        wd.SubEntityTraits.LineWeight = LineWeight.LineWeight000

        start = cir.StartPoint

        cen = cir.Center

        norm = Vector3d(

          cen.X - start.X,

          cen.Y - start.Y,

          cen.Z - start.Z

        )

        clr = Circle(start, norm.CrossProduct(cir.Normal), radius)

        pipe = SweptSurface()

        pipe.CreateSweptSurface(clr, d, _sweepOpts)

        clr.Dispose()

        pipe.WorldDraw(wd)

        pipe.Dispose()

        wd.SubEntityTraits.TrueColor = c

      return true

    return super(d, wd)

 

  override def SetAttributes(d as Drawable, t as DrawableTraits):

 

    b = super(d, t)

 

    radius = PipeRadiusForObject(d)

 

    if radius > 0.0:

      # Set color to yellow

      t.Color = 2

      # and lineweight to .60 mm

      t.LineWeight = LineWeight.LineWeight060

 

    return b

 

class LinePipeTransformOverrule(TransformOverrule):

 

  /*

  An overrule to explode a linear pipe into Solid3d objects

  */

 

  public static theOverrule = LinePipeTransformOverrule()

 

  private _sweepOpts

 

  def constructor():

    _sweepOpts = SweepOptions()

 

  override def Explode(e as Entity, objs as DBObjectCollection):

 

    radius = PipeRadiusForObject(e)

 

    if radius > 0.0:

 

      ln = e as Line

      if not e.Id.IsNull and ln.Length > 0.0:

        # Draw a pipe around the line

 

        start = ln.StartPoint

        end = ln.EndPoint

        norm = Vector3d(

          end.X - start.X,

          end.Y - start.Y,

          end.Z - start.Z

        )

        clr = Circle(start, norm, radius)

        pipe = ExtrudedSurface()

        try:

          pipe.CreateExtrudedSurface(clr, norm, _sweepOpts)

        except:

          doc = Application.DocumentManager.MdiActiveDocument

          doc.Editor.WriteMessage(

            "\nFailed with CreateExtrudedSurface."

          )

        clr.Dispose()

        objs.Add(pipe)

      return

    super(e, objs)

 

class CirclePipeTransformOverrule(TransformOverrule):

 

  /*

  An overrule to explode a circular pipe into Solid3d objects

  */

 

  public static theOverrule = CirclePipeTransformOverrule()

 

  private _sweepOpts

 

  def constructor():

    _sweepOpts = SweepOptions()

 

  override def Explode(e as Entity, objs as DBObjectCollection):

 

    radius = PipeRadiusForObject(e)

 

    if radius > 0.0:

      cir = e as Circle   

      if cir.Radius > radius:

 

        start = cir.StartPoint

        cen = cir.Center

        norm = Vector3d(

          cen.X - start.X,

          cen.Y - start.Y,

          cen.Z - start.Z

        )

        clr = Circle(start, norm.CrossProduct(cir.Normal), radius)

        pipe = SweptSurface()

        pipe.CreateSweptSurface(clr, e, _sweepOpts)

        clr.Dispose()

        objs.Add(pipe)

      return

    super(e, objs)

 

def EnableOverrule(enable):

 

  /*

  Regen to see the effect

  (turn on/off Overruling and LWDISPLAY)

  */

 

  Overrule.Overruling = enable

  if enable:

    Application.SetSystemVariable("LWDISPLAY", 1)

  else:

    Application.SetSystemVariable("LWDISPLAY", 0)

 

  doc = Application.DocumentManager.MdiActiveDocument

  doc.SendStringToExecute("REGEN3\n", true, false, false)

  doc.Editor.Regen()

 

class Commands:

 

  [CommandMethod("OVERRULE1")]

  def Overrule1():

 

    # Only add the overrule if not currently attached

 

    if not Globals.overruling:

 

      ObjectOverrule.AddOverrule(

        RXClass.GetClass(typeof(Line)),

        LinePipeDrawOverrule.theOverrule,

        true

      )

      ObjectOverrule.AddOverrule(

        RXClass.GetClass(typeof(Line)),

        LinePipeTransformOverrule.theOverrule,

        true

      )

      ObjectOverrule.AddOverrule(

        RXClass.GetClass(typeof(Circle)),

        CirclePipeDrawOverrule.theOverrule,

        true

      )

      ObjectOverrule.AddOverrule(

        RXClass.GetClass(typeof(Circle)),

        CirclePipeTransformOverrule.theOverrule,

        true

      )

 

      Globals.overruling = true

      EnableOverrule(true)

 

  [CommandMethod("OVERRULE0")]

  def Overrule0():

 

    # Only remove the overrule if previously added

 

    if Globals.overruling:

 

      ObjectOverrule.RemoveOverrule(

        RXClass.GetClass(typeof(Line)),

        LinePipeDrawOverrule.theOverrule

      )

      ObjectOverrule.RemoveOverrule(

        RXClass.GetClass(typeof(Line)),

        LinePipeTransformOverrule.theOverrule

      )

      ObjectOverrule.RemoveOverrule(

        RXClass.GetClass(typeof(Circle)),

        CirclePipeDrawOverrule.theOverrule

      )

      ObjectOverrule.RemoveOverrule(

        RXClass.GetClass(typeof(Circle)),

        CirclePipeTransformOverrule.theOverrule

      )

 

      Globals.overruling = false

      EnableOverrule(false)

 

  # Should be able to use CommandFlags.UsePickSet instead of 2

  [CommandMethod("MP", cast(CommandFlags,2))]

  def MakePipe():

 

    doc = Application.DocumentManager.MdiActiveDocument

    db = doc.Database

    ed = doc.Editor

 

    # Ask the user to select the entities to make into pipes

 

    pso = PromptSelectionOptions()

    pso.AllowDuplicates = false

    pso.MessageForAdding = "\nSelect objects to turn into pipes: "

 

    selRes = ed.GetSelection(pso)

 

    # If the user didn't make valid selection, we return

    # Should be able to use PromptStatus.OK instead of 5100

 

    if selRes.Status != cast(PromptStatus,5100):

      return

 

    ss = selRes.Value

 

    # Ask the user for the pipe radius to set

 

    pdo = PromptDoubleOptions("\nSpecify pipe radius:")

 

    # Use the previous value, if if already called

 

    if Globals.radius > 0.0:

      pdo.DefaultValue = Globals.radius

      pdo.UseDefaultValue = true

 

    pdo.AllowNegative = false

    pdo.AllowZero = false

 

    pdr = ed.GetDouble(pdo)

 

    # Return if something went wrong

    # Should be able to use PromptStatus.OK instead of 5100

 

    if pdr.Status != cast(PromptStatus,5100):

      return

 

    # Set the "last radius" value for when

    # the command is called next

 

    Globals.radius = pdr.Value

 

    # Use a transaction to edit our various objects

 

    tr = db.TransactionManager.StartTransaction()

 

    # Loop through the selected objects

 

    for o as SelectedObject in ss:

 

      # We could choose only to add XData to the objects

      # we know will use it (Lines and Circles, for now)

 

      obj = tr.GetObject(o.ObjectId, OpenMode.ForWrite)

 

      SetPipeRadiusOnObject(tr, obj, Globals.radius)

 

    tr.Commit()

    tr.Dispose()

 

 def PipeRadiusForObject(obj as DBObject):

 

  /*

  Get the XData for a particular object

  and return the "pipe radius" if it exists

  */

 

  res = 0.0

 

  try:

    rb = obj.XData

    if rb is null:

      return res

 

    foundStart = false

 

    for tv as TypedValue in rb:

      if (tv.TypeCode == Globals.appCode

        and tv.Value == Globals.appName):

        foundStart = true

      else:

        if foundStart:

          if tv.TypeCode == Globals.radCode:

            res = tv.Value

            break

 

    rb.Dispose()

  except:

    return 0.0

  return res

 

def SetPipeRadiusOnObject(

  tr as Transaction, obj as DBObject, radius

):

 

  /*

  Set the pipe radius as XData on a particular object

  */

 

  db = obj.Database

 

  # Make sure the application is registered

  # (we could separate this out to be called

  # only once for a set of operations)

 

  rat as SymbolTable = tr.GetObject(

      db.RegAppTableId, OpenMode.ForRead

    )

 

  if not rat.Has(Globals.appName):

    rat.UpgradeOpen()

    ratr = RegAppTableRecord()

    ratr.Name = Globals.appName

    rat.Add(ratr)

    tr.AddNewlyCreatedDBObject(ratr, true)

 

  # Create the XData and set it on the object

 

  rb = ResultBuffer(

      TypedValue(Globals.appCode, Globals.appName),

      TypedValue(Globals.radCode, Globals.radius)

    )

  obj.XData = rb

  rb.Dispose()

Once the code is built into a Class Library, you should be able to load it via NETLOAD and execute the commands it defines, MP, OVERRULE0 and OVERRULE1.

As for the results of running these commands, they should be identical to what we’ve seen in the previous posts in this series. No need for yet more identical screenshots… :-)

I’m sure many of you are rolling your eyes at having yet another programming option presented to you… so far I’ve been presenting lots of different options, without necessarily telling people what they should use. This is deliberate: my aim is to present the information I’ve been able to glean on the various language options rather than to pass judgment on them.

All the languages I’ve presented thus far relate to the use of .NET, whether via the CLR or the DLR. The future may hold different decisions, depending on the product and its platform, but for now here’s my thought process when working with AutoCAD and .NET…

I personally expect to use C# and F# for the majority of projects upon which I work (the choice will depend on the problem domain), but I can imagine tackling the occasional task in IronPython – as it’s more stable than IronRuby at the time of writing - if I really need something more dynamic. This is likely to be for certain types of problem where dynamic – probably even self-modifying – code makes more sense to me… for the occasional project that is likely to benefit from duck typing I’m more likely to use Boo (or C#, in time). But then I’m still getting my head around the type of problem best suited to dynamic languages: these will, I’m sure, become more obvious, in time. As more of our products support dynamic language capabilities directly this will also play a role in the decision process.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e201157083bf7c970b

Listed below are links to weblogs that reference Using Boo with AutoCAD:

blog comments powered by Disqus

Feed/Share

10 Random Posts