August 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            








« Last day of voting for AU 2009 classes | Main | Using Boo with AutoCAD »

May 11, 2009

Overruling AutoCAD 2010’s entity display and explode using IronRuby

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:

  1. 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.
  2. 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:

  1. CLR namespaces and interfaces must be capitalized as they are mapped onto Ruby modules
  2. CLR classes must be capitalized as they are mapped onto Ruby classes
  3. 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").
  4. 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:

Select a Ruby script to load and execute

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:

Some basic linear geometry

Which we then use as a pipe radius for our geometry by turning on our overrules using the OVERRULE1 command:

Our overruled lines and circles with varying profile radii

Here is the same geometry in a conceptual 3D view:

3D conceptual display of our overruled geometry

And finally we call EXPLODE to try out our TransformOverrule and see the resultant Solid3d objects:

3D conceptual display of our exploded, overruled geometry

TrackBack

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

Listed below are links to weblogs that reference Overruling AutoCAD 2010’s entity display and explode using IronRuby:

blog comments powered by Disqus

Feed/Share

10 Random Posts