As mentioned in this previous post, where I gave the same treatment to IronPython, I’ve been trying to get display and explode overrules defined in IronRuby working properly in AutoCAD. IronRuby is still at version 0.3, so this effort has been hindered by a number of CLR interop bugs (it turns out).
I finally managed to work around these issues thanks to Ivan Porto Carrero, who is just finishing up his book, Iron Ruby in Action, and has been working with IronRuby since pre-Alpha 1 (brave fellow). Ivan’s help was invaluable: he ended up downloading and installing AutoCAD 2010 to work through the issues on my behalf, uncover the various problems and submitting bugs against IronRuby, where appropriate. There was a small measure of self-interest involved, as I’ve been working on some content Ivan will be including in his book – I just hope the material proves usable for him. Oh, and hopefully I’ll be getting a few copies of Ivan’s book to give away at my proposed AU 2009 class on “Developing for AutoCAD with IronPython and IronRuby”.
Incidentally, in spite of the workarounds implemented with Ivan’s help, I wasn’t able to get the previous IronRuby sample to jig a solid working. Hopefully the underlying IronRuby bug that’s stopping it from working will be addressed in an upcoming release.
The C# code defining the RBLOAD command has been update slightly:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using IronRuby.Hosting;
using IronRuby;
using Microsoft.Scripting.Hosting;
using System.Reflection;
using System;
namespace RubyLoader
{
public class Commands
{
[CommandMethod("-RBLOAD")]
public static void RubyLoadCmdLine()
{
RubyLoad(true);
}
[CommandMethod("RBLOAD")]
public static void RubyLoadUI()
{
RubyLoad(false);
}
public static void RubyLoad(bool useCmdLine)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
short fd =
(short)Application.GetSystemVariable("FILEDIA");
// As the user to select a .rb file
PromptOpenFileOptions pfo =
new PromptOpenFileOptions(
"Select Ruby script to load"
);
pfo.Filter = "Ruby script (*.rb)|*.rb";
pfo.PreferCommandLine =
(useCmdLine || fd == 0);
PromptFileNameResult pr =
ed.GetFileNameForOpen(pfo);
// And then try to load and execute it
if (pr.Status == PromptStatus.OK)
ExecuteRubyScript(pr.StringResult);
}
[LispFunction("RBLOAD")]
public ResultBuffer RubyLoadLISP(ResultBuffer rb)
{
const int RTSTR = 5005;
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
if (rb == null)
{
ed.WriteMessage("\nError: too few arguments\n");
}
else
{
// We're only really interested in the first argument
Array args = rb.AsArray();
TypedValue tv = (TypedValue)args.GetValue(0);
// Which should be the filename of our script
if (tv != null && tv.TypeCode == RTSTR)
{
// If we manage to execute it, let's return the
// filename as the result of the function
// (just as (arxload) does)
bool success =
ExecuteRubyScript(Convert.ToString(tv.Value));
return
(success ?
new ResultBuffer(
new TypedValue(RTSTR, tv.Value)
)
: null);
}
}
return null;
}
private static bool ExecuteRubyScript(string file)
{
// If the file exists, let's load and execute it
bool ret = System.IO.File.Exists(file);
if (ret)
{
try
{
LanguageSetup ls = Ruby.CreateRubySetup();
ScriptRuntimeSetup rs =
new ScriptRuntimeSetup();
rs.LanguageSetups.Add(ls);
rs.DebugMode = true;
ScriptRuntime runtime =
Ruby.CreateRuntime(rs);
runtime.LoadAssembly(
Assembly.GetAssembly(typeof(Commands))
);
ScriptEngine engine = Ruby.GetEngine(runtime);
engine.ExecuteFile(file);
}
catch (System.Exception ex)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ed.WriteMessage(
"\nProblem executing script: {0}", ex
);
}
}
return ret;
}
}
}
Inside the ExecuteRubyScript() function we’re now creating a runtime environment within which we load our C# assembly, as it defines some classes to workaround some derivation problems. Here is the additional C# source that needed to be added to our project:
namespace ConcreteClasses
{
public class DrawableOverrule :
Autodesk.AutoCAD.GraphicsInterface.DrawableOverrule
{
public int ParentSetAttributes(
Autodesk.AutoCAD.GraphicsInterface.Drawable drawable,
Autodesk.AutoCAD.GraphicsInterface.DrawableTraits traits
)
{
return base.SetAttributes(drawable, traits);
}
public bool ParentWorldDraw(
Autodesk.AutoCAD.GraphicsInterface.Drawable drawable,
Autodesk.AutoCAD.GraphicsInterface.WorldDraw wd
)
{
return base.WorldDraw(drawable, wd);
}
}
public class TransformOverrule :
Autodesk.AutoCAD.DatabaseServices.TransformOverrule
{}
}
This code works around two issues:
- IronRuby 0.3 has issues implementing abstract classes and our Overrule classes are abstract. So we derive “concrete” classes from these abstract classes – empty implementations would be enough, as neither DrawableOverrule no TransformOverrule include abstract function definitions that require overriding – and in our Ruby script we derive from these concrete classes.
- Our concrete DrawableOverrule is not empty as we need to work around another issue: super-messaging to our parent classes proved problematic, so we now expose methods (ParentSetAttributes() and ParentWorldDraw()) that do so explicitly.
I also hit a number of subtle issues related to Ruby’s naming conventions… Here’s what the IronRuby documentation says about naming conventions when using CLR types (which our types are, as they’re imported using .NET):
In an effort to make consuming .NET APIs in IronRuby more Rubyesque, IronRuby allows calling .NET code with Ruby idioms:
- CLR namespaces and interfaces must be capitalized as they are mapped onto Ruby modules
- CLR classes must be capitalized as they are mapped onto Ruby classes
- CLR methods that you call may either retain their original spelling (ie "WriteLine") or they may be used in a more Rubyesque form which is obtained by translating CamelCase to lowercase_and_delimited (ie "write_line").
- CLR virtual methods which you override from IronRuby must be in their lowercase_and_delimited form.
I was fine with item 1 (I’d hit this in my first attempt to write an IronRuby application, and found that I had to capitalicise even the variables I’d created as shortcuts to namespaces) and with item 2.
It was item 4 that caught me unawares: my WorldDraw() and SetAttributes() overides were simply not getting called: they had to be renamed to world_draw() and set_attributes(). That took some time to work out (and is no doubt one of the issues with the solid jigging code).
Item 3 proved to be quite fun: I ended up going back through the entire code sample, changing the CamelCase method calls to lowercase_and_delimited, even if this sometimes ended up looking a little strange (my personal favourite being Transaction.add_newly_created_d_b_object() :-).
Here’s the code from our .rb file:
require 'acmgd.dll'
require 'acdbmgd.dll'
Ai = Autodesk::AutoCAD::Internal
Aiu = Autodesk::AutoCAD::Internal::Utils
Aas = Autodesk::AutoCAD::ApplicationServices
Ads = Autodesk::AutoCAD::DatabaseServices
Aei = Autodesk::AutoCAD::EditorInput
Agi = Autodesk::AutoCAD::GraphicsInterface
Ag = Autodesk::AutoCAD::Geometry
Ac = Autodesk::AutoCAD::Colors
Ar = Autodesk::AutoCAD::Runtime
def print_message(msg)
app = Aas::Application
doc = app.document_manager.mdi_active_document
ed = doc.editor
ed.write_message(msg)
end
# Function to register AutoCAD commands
def autocad_command(cmd)
cc = Ai::CommandCallback.new method(cmd)
Aiu.add_command('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
APP_NAME = "TTIF_PIPE"
APP_CODE = 1001
RAD_CODE = 1040
def pipe_radius_for_object(obj)
# Get the XData for a particular object
# and return the "pipe radius" if it exists
res = 0.0
begin
rb = obj.XData
if rb.nil?
return res
end
foundStart = false
for tv in rb do
if (tv.type_code == APP_CODE and tv.value == APP_NAME)
foundStart = true
else
if foundStart
if (tv.type_code == RAD_CODE)
res = tv.value
break
end
end
end
end
rescue
return 0.0
end
return res
end
def set_pipe_radius_for_object(tr, obj, 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 =
tr.get_object(db.reg_app_table_id, Ads::OpenMode.for_read)
if (not rat.Has(APP_NAME))
rat.UpgradeOpen()
ratr = Ads::RegAppTableRecord.new
ratr.Name = APP_NAME
rat.Add(ratr)
tr.add_newly_created_d_b_object(ratr, true)
end
# Create the XData and set it on the object
rb = Ads::ResultBuffer.new(
Ads::TypedValue.new(APP_CODE, APP_NAME),
Ads::TypedValue.new(RAD_CODE, radius))
obj.XData = rb
rb.Dispose()
end
class PipeDrawOverrule < ConcreteClasses::DrawableOverrule
# The base class for our draw overrules specifying the
# registered application name for the XData upon which
# to filter
def initialize
# Tell AutoCAD to filter on our application name
# (this means our overrule will only be called
# on objects possessing XData with this name)
set_x_data_filter(APP_NAME)
end
end
class LinePipeDrawOverrule < PipeDrawOverrule
# An overrule to make a pipe out of a line
def initialize
@sweep_opts = Ads::SweepOptions.new
super
end
def world_draw(d, wd)
radius = pipe_radius_for_object(d)
if radius > 0.0
# Draw the line as is, with overruled attributes
# Should just be able to call super
parent_world_draw(d, wd)
if not d.id.is_null and d.length > 0.0
# Draw a pipe around the line
c = wd.sub_entity_traits.true_color
wd.sub_entity_traits.true_color =
Ac::EntityColor.new 0x00AFAFFF
wd.sub_entity_traits.line_weight =
Ads::LineWeight.line_weight_000
start = d.start_point
endpt = d.end_point
norm = Ag::Vector3d.new(
endpt.X - start.X,
endpt.Y - start.Y,
endpt.Z - start.Z)
clr = Ads::Circle.new start, norm, radius
pipe = Ads::ExtrudedSurface.new
begin
pipe.create_extruded_surface(clr, norm, @sweep_opts)
rescue
print_message "\nFailed with CreateExtrudedSurface."
end
clr.dispose()
pipe.world_draw(wd)
pipe.dispose()
wd.sub_entity_traits.true_color = c
end
return true
end
return super
end
def set_attributes(d, t)
# Should just be able to call super
i = parent_set_attributes(d, t)
radius = pipe_radius_for_object(d)
if radius > 0.0
# Set color to magenta
t.color = 6
# and lineweight to .40 mm
t.line_weight = Ads::LineWeight.line_weight_040
end
return i
end
end
class CirclePipeDrawOverrule < PipeDrawOverrule
# An overrule to make a pipe out of a circle
def initialize
@sweep_opts = Ads::SweepOptions.new
super
end
def world_draw(d, wd)
radius = pipe_radius_for_object(d)
if radius > 0.0
# Draw the circle as is, with overruled attributes
parent_world_draw(d, wd)
# Needed to avoid ill-formed swept surface
if d.radius > radius
# Draw a pipe around the circle
c = wd.sub_entity_traits.true_color
wd.sub_entity_traits.true_color =
Ac::EntityColor.new 0x3FFFE0E0
wd.sub_entity_traits.line_weight =
Ads::LineWeight.LineWeight000
start = d.StartPoint
cen = d.Center
norm = Ag::Vector3d.new(
cen.X - start.X,
cen.Y - start.Y,
cen.Z - start.Z)
clr =
Ads::Circle.new start, norm.cross_product(d.normal), radius
pipe = Ads::SweptSurface.new
pipe.create_swept_surface(clr, d, @sweep_opts)
clr.dispose()
pipe.world_draw(wd)
pipe.dispose()
wd.sub_entity_traits.true_color = c
end
return true
end
return parent_world_draw(d, wd)
end
def set_attributes(d, t)
# Should just be able to call super
i = parent_set_attributes(d, t)
radius = pipe_radius_for_object(d)
if radius > 0.0
# Set color to yellow
t.color = 2
# and lineweight to .60 mm
t.line_weight = Ads::LineWeight.line_weight_060
end
return i
end
end
class LinePipeTransformOverrule < ConcreteClasses::TransformOverrule
# An overrule to explode a linear pipe into Solid3d objects
def initialize
@sweep_opts = Ads::SweepOptions.new
end
def explode(e, objs)
radius = pipe_radius_for_object(e)
if radius > 0.0
if not e.Id.IsNull and e.Length > 0.0
# Draw a pipe around the line
start = e.start_point
endpt = e.end_point
norm = Ag::Vector3d.new(
endpt.X - start.X,
endpt.Y - start.Y,
endpt.Z - start.Z)
clr = Ads::Circle.new start, norm, radius
pipe = Ads::ExtrudedSurface.new
begin
pipe.create_extruded_surface clr, norm, @sweep_opts
rescue
print_message "\nFailed with CreateExtrudedSurface."
end
clr.dispose()
objs.add(pipe)
end
return
end
super
end
end
class CirclePipeTransformOverrule < ConcreteClasses::TransformOverrule
# An overrule to explode a circular pipe into Solid3d objects
def initialize
@sweep_opts = Ads::SweepOptions.new
end
def explode(e, objs)
radius = pipe_radius_for_object(e)
if radius > 0.0
if e.radius > radius
start = e.start_point
cen = e.center
norm = Ag::Vector3d.new(
cen.X - start.X,
cen.Y - start.Y,
cen.Z - start.Z)
clr =
Ads::Circle.new start, norm.cross_product(e.normal), radius
pipe = Ads::SweptSurface.new
pipe.create_swept_surface(clr, e, @sweep_opts)
clr.dispose()
objs.add(pipe)
end
return
end
super
end
end
def overrule(enable)
# Regen to see the effect
# (turn on/off Overruling and LWDISPLAY)
Ar::Overrule.Overruling = enable
if enable
Aas::Application.set_system_variable("LWDISPLAY", 1)
else
Aas::Application.set_system_variable("LWDISPLAY", 0)
end
doc = Aas::Application.document_manager.mdi_active_document
doc.send_string_to_execute("REGEN3\n", true, false, false)
doc.editor.regen()
end
$overruling = false
$radius = 0.0
def overrule1
begin
if !$overruling
$lpdo = LinePipeDrawOverrule.new
$cpdo = CirclePipeDrawOverrule.new
$lpto = LinePipeTransformOverrule.new
$cpto = CirclePipeTransformOverrule.new
Ads::ObjectOverrule.add_overrule(
Ar::RXClass::get_class(Ads::Line.to_clr_type),
$lpdo,
true)
Ads::ObjectOverrule.add_overrule(
Ar::RXClass::get_class(Ads::Line.to_clr_type),
$lpto,
true)
Ads::ObjectOverrule.add_overrule(
Ar::RXClass::get_class(Ads::Circle.to_clr_type),
$cpdo,
true)
Ads::ObjectOverrule.add_overrule(
Ar::RXClass::get_class(Ads::Circle.to_clr_type),
$cpto,
true)
$overruling = true
overrule(true)
end
rescue
print_message("\nProblem found: " + $! + "\n")
end
end
def overrule0
begin
if $overruling
Ads::ObjectOverrule.remove_overrule(
Ar::RXClass::get_class(Ads::Line.to_clr_type),
$lpdo)
Ads::ObjectOverrule.remove_overrule(
Ar::RXClass::get_class(Ads::Line.to_clr_type),
$lpto)
Ads::ObjectOverrule.remove_overrule(
Ar::RXClass::get_class(Ads::Circle.to_clr_type),
$cpdo)
Ads::ObjectOverrule.remove_overrule(
Ar::RXClass::get_class(Ads::Circle.to_clr_type),
$cpto)
$overruling = false
overrule(false)
end
rescue
print_message("\nProblem found: " + $! + "\n")
end
end
def makePipe()
begin
doc = Aas::Application.document_manager.mdi_active_document
db = doc.Database
ed = doc.Editor
# Ask the user to select the entities to make into pipes
pso = Aei::PromptSelectionOptions.new
pso.allow_duplicates = false
pso.message_for_adding =
"\nSelect objects to turn into pipes: "
sel_res = ed.GetSelection(pso)
# If the user didn't make valid selection, we return
if sel_res.Status != Aei::PromptStatus.OK
return
end
ss = sel_res.Value
# Ask the user for the pipe radius to set
pdo = Aei::PromptDoubleOptions.new "\nSpecify pipe radius:"
# Use the previous value, if if already called
if $radius > 0.0
pdo.default_value = $radius
pdo.use_default_value = true
end
pdo.allow_negative = false
pdo.allow_zero = false
pdr = ed.get_double(pdo)
# Return if something went wrong
if pdr.Status != Aei::PromptStatus.OK
return
end
# Set the "last radius" value for when
# the command is called next
$radius = pdr.value
# Use a transaction to edit our various objects
tr = db.transaction_manager.start_transaction()
# Loop through the selected objects
for o in ss do
# We could choose only to add XData to the objects
# we know will use it (Lines and Circles, for now)
obj = tr.get_object(o.object_id, Ads::OpenMode.for_write)
set_pipe_radius_for_object(tr, obj, $radius)
end
tr.Commit()
tr.Dispose()
rescue
print_message("\nProblem found: " + $! + "\n")
end
end
add_commands ["overrule1", "overrule0", "makePipe"]
When we build and load our C# module via NETLOAD, we can then use RBLOAD to load our .rb file:
Registered Ruby command: overrule1
Registered Ruby command: overrule0
Registered Ruby command: makePipe
As in the previous examples, we create some geometry to which we attach data using the MAKEPIPE command:
Which we then use as a pipe radius for our geometry by turning on our overrules using the OVERRULE1 command:
Here is the same geometry in a conceptual 3D view:
And finally we call EXPLODE to try out our TransformOverrule and see the resultant Solid3d objects: