In the last post I introduced a very simple “Hello World!” IronRuby application working with AutoCAD, just as I’d previously done with IronPython. My idea for this post was to take the code from my second IronPython post – which showed how to jig an AutoCAD solid from IronPython – and get it working with IronRuby, forcing me to learn a little more Ruby in the process.
All started out well: to convert the basic syntax from Python to Ruby was straightforward, and I have a definite liking for the syntax of the Ruby language. Especially when working with object orientation: the code for implementing classes feels cleaner than Python and it’s really OO from top to bottom (everything’s an object). I even found a way to avoid all those namespaces everywhere in the code – you just assign them to symbols and use those intead.
So what didn’t work? Well, it turns out there’s a problem with the definition of the EntityJig class, which is the parent of our SolidJig: IronRuby tells me it doesn’t have a default constructor defined. It’s very similar in nature to the problem I had deriving the IronPython version of my SolidJig class, but in this case I had to make sure my code very specifically implemented an __init__ function taking an entity (IronRuby uses initialize() instead of __init__ and is really clean in the way it allows you to super-message to your parent class, but right now it seems there’s something getting in the way – IronRuby is at version 0.3, after all).
Here’s my IronRuby script – for the RBLOAD command look at the C# code in the last post.
require 'C:\Program Files\Autodesk\AutoCAD 2009\acmgd.dll'
require 'C:\Program Files\Autodesk\AutoCAD 2009\acdbmgd.dll'
require 'C:\Program Files\Autodesk\AutoCAD 2009\acmgdinternal.dll'
Ai = Autodesk::AutoCAD::Internal
Aiu = Autodesk::AutoCAD::Internal::Utils
Aas = Autodesk::AutoCAD::ApplicationServices
Ads = Autodesk::AutoCAD::DatabaseServices
Aei = Autodesk::AutoCAD::EditorInput
Ag = Autodesk::AutoCAD::Geometry
Ar = Autodesk::AutoCAD::Runtime
def print_message(msg)
app = Aas::Application
doc = app.DocumentManager.MdiActiveDocument
ed = doc.Editor
ed.WriteMessage(msg)
end
# Function to register AutoCAD commands
def autocad_command(cmd)
cc = Ai::CommandCallback.new method(cmd)
Aiu.AddCommand('rbcmds', cmd, cmd, Ar::CommandFlags.Modal, cc)
# Let's now write a message to the command-line
print_message("\nRegistered Ruby command: " + cmd)
end
def add_commands(names)
names.each { |n| autocad_command n }
end
# Let's do something a little more complex...
class SolidJig < Aei::EntityJig
# Initialization function
def initialize(ent)
# Call the base class and store the object
super
@sol = ent
end
# The function called to run the jig
def StartJig(ed, pt)
# The start point is specified outside the jig
@start = pt
@end = pt
return ed.Drag(self)
end
# The sampler function
def Sampler(prompts)
# Set up our selection options
jo = Aei::JigPromptPointOptions.new
jo.UserInputControls = (
Aei::UserInputControls.Accept3dCoordinates |
Aei::UserInputControls.NoZeroResponseAccepted |
Aei::UserInputControls.NoNegativeResponseAccepted)
jo.Message = "\nSelect end point: "
# Get the end point of our box
res = prompts.AcquirePoint(jo)
if @end == res.Value
return Aei::SamplerStatus.NoChange
else
@end = res.Value
end
return Aei::SamplerStatus.OK
end
# The update function
def Update()
# Recreate our Solid3d box
begin
# Get the width (x) and depth (y)
x = @end.X - @start.X
y = @end.Y - @start.Y
# We need a non-zero Z value, so we copy Y
z = y
# Create our box and move it to the right place
@sol.CreateBox(x,y,z)
@sol.TransformBy(
Ag::Matrix3d.Displacement(
Ag::Vector3d.new(
@start.X + x/2,
@start.Y + y/2,
@start.Z + z/2)))
rescue
return false
end
return true
end
end
# Create a box using a jig
def boxjig
app = Aas::Application
doc = app.DocumentManager.MdiActiveDocument
db = doc.Database
ed = doc.Editor
# Select the start point before entering the jig
ppr = ed.GetPoint("\nSelect start point: ")
if ppr.Status == Aei::PromptStatus.OK
# We'll add our solid to the modelspace
tr = doc.TransactionManager.StartTransaction
bt =
tr.GetObject(
db.BlockTableId,
Ads::OpenMode.ForRead)
btr =
tr.GetObject(db.CurrentSpaceId,Ads::OpenMode.ForWrite)
# Make sure we're recording history to allow grip editing
sol = Ads::Solid3d.new
sol.RecordHistory = true
# Now we add our solid
btr.AppendEntity(sol)
tr.AddNewlyCreatedDBObject(sol, true)
# And call the jig before finishing
begin
sj = SolidJig.new sol
ppr2 = sj.StartJig(ed, ppr.Value)
# Only commit if all completed well
if ppr2.Status == Aei::PromptStatus.OK
tr.Commit
end
rescue
print_message("\nProblem found: " + $! + "\n")
end
tr.Dispose
end
end
add_commands ["boxjig"]
When we run the RBLOAD command and select this script, our BOXJIG command gets defined.
Registered Ruby command: boxjig
When this is run, our Begin-Rescue block (equivalent to Try-Catch in most other modern programming languages :-) catches the exception and prints an error statement at the command-line:
Command: BOXJIG
Select start point:
Problem found: Parent does not have a default constructor. The default constructor must be explicitly defined.
I’m hoping the root cause of the issue is obvious to someone familiar with IronRuby, whether in the above calling code, the design of the API exposed by AutoCAD or the implementation of IronRuby itself.
I do prefer not to post code that doesn’t actually work, but I feel there’s still value in seeing the comparison between the Python and Ruby scripts, for people to get a feel for how the languages differ. And - apart from anything else – I’ve hit my head against this for long enough that I at least want to get a post out of it. :-)
Update:
Ivan Porto Carrero – who’s working on the book IronRuby in Action – is able to reproduce the issue outside of AutoCAD and has offered to log a bug. Hopefully it’ll get addressed in a future build of IronRuby (or someone will spell out how I can handle it cleanly from my side).