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.
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.