Autocad Selection Set Filters Made Easy with IronRuby

Using selection set filters in the AutoCAD .Net api has always been less than enjoyable.
Take, for example, this bit of code from the online .Net Developer’s Guide

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
32
33
34
35

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;

[CommandMethod("FilterSelectionSet")]
public static void FilterSelectionSet()
{
  // Get the current document editor
  Editor acDocEd = Application.DocumentManager.MdiActiveDocument.Editor;

  // Create a TypedValue array to define the filter criteria
  TypedValue[] acTypValAr = new TypedValue[1];
  acTypValAr.SetValue(new TypedValue((int)DxfCode.Start, "CIRCLE"), 0);

  // Assign the filter criteria to a SelectionFilter object
  SelectionFilter acSelFtr = new SelectionFilter(acTypValAr);

  // Request for objects to be selected in the drawing area
  PromptSelectionResult acSSPrompt;
  acSSPrompt = acDocEd.GetSelection(acSelFtr);

  // If the prompt status is OK, objects were selected
  if (acSSPrompt.Status == PromptStatus.OK)
  {
      SelectionSet acSSet = acSSPrompt.Value;
      Application.ShowAlertDialog("Number of objects selected: " +
                                  acSSet.Count.ToString());
  }
  else
  {
      Application.ShowAlertDialog("Number of objects selected: 0");
  }
}

The thing that bothers me the most is having to build that ugly TypedValue array

1
2
  TypedValue[] acTypValAr = new TypedValue[1];
  acTypValAr.SetValue(new TypedValue((int)DxfCode.Start, "CIRCLE"), 0);

Being an old LISP hacker, I know most of the DxfCodes (8 for layer, 0 for type, 62 for color, etc) or I can find them easily. But why am I forced to remember those codes at all? If I know that I want to filter on type or layer or color, why do I have to either know the appropriate code or cast the enum member to an int. And why do I have to keep up with the index (ie, that 0 just after “CIRCLE”), )?

Why can’t I just do something like

1
2
3
filter = SsFilter.new
filter.Layer = "my_layer"
filter.Type = "Circle"

Well now I can. I have extended my acadhelper to include a new class called SsFilter

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
32
33
34
class SsFilter
        attr_accessor :data
        def initialize
                @data = {}
                @elements = {:Type => 0, :Text => 1, :BlockName => 2, :LineType => 6, :TextStyle => 7, :Layer => 8,
                     :StartPoint => 10, :CenterPoint => 10, :EndPoint => 11, :Elevation => 38, :Thickness => 39,
                     :TextHeight => 40, :TextWidth => 41, :Rotation => 50, :Oblique => 51, :Color => 62}
            @keys = @elements.keys     
        end
                
    def filter
       typed_value = Ads::TypedValue[]
       new_filter = System::Array.of( typed_value).new(@data.size)
        
       i = 0
       if @data.size > 0
          @data.each_pair do  |key,value| 
             new_filter.set_value(Ads::TypedValue.new( @elements[key], value), i)
             i+=1
          end        
       end
       Aei::SelectionFilter.new(new_filter)
   end
                
   def method_missing(method_name, *args)
      if method_name.to_s.match(/(\w+)=/) && @keys.include?($1.to_sym)
         @data[$1.to_sym] = args[0]
      elsif @keys.include?(method_name)
         return @data[method_name]
      else
         super(method_name, *args)
      end
   end
end

This new class uses a bit of method_missing magic to create a very friendly and readable way of creating a selection set filter.

The C# code above can now, using other methods from my acadhelper gem, be written as

1
2
3
4
5
6
7
8
9
10
11
12
require 'rubygems'
require 'acadhelper'
include AcadHelper

def filter_selection_set
     filter = SsFilter.new
     filter.Type = "Circle"
     ss = select_on_screen filter
     result = "Number of objects selected: "
     result += ss ?  ss.count.to_s : "0"
     alert result
end

And I did have to make a minor change to the select_on_screen method to support the new filter class
and still allow for hand-built filter arrays

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    def select_on_screen( filter_data=[])
        begin
            if filter_data.is_a?(SsFilter) #new test
                filter = filter_data.filter
            else                
                filter = build_selection_filter filter_data
            end        
            ssPrompt = ed.GetSelection( filter)
            if ssPrompt.Status == Aei::PromptStatus.OK
               return ssPrompt.Value 
            else   
                nil
            end         
        rescue Exception => e
          puts_ex e
        end
    end

The new class has been added to the gem at github but
it does not yet support logical grouping or relational tests. Those are coming soon

TIRADE (The IronRuby Autocad Development Environment)

Here is a sneak-peek at TIRADE

TIRADE extends my work on acadhelper and allows instant (or at least very rapid) gratification and enjoyment of Ruby goodness when extending AutoCAD with IronRuby.

TIRADE runs modelessly, integrates RSpec and Test::Unit testing directly (slowly, but directly), has syntax highlighting and will soon have (hopeably) code completion for the acadhelper wrappers and methods.

I used the ICSharpCode TextEditor control (with some local modifications) as the editor control and used the open source DockPanel Suite that very closely matches the Visual Studio docking that we all know and love.

After a bit more testing, I hope to release TIRADE on the world. Stay tuned

Saving Attributes to a Rails Application made Dead-Simple

Now that Ruby and AutoCAD are playing nicely together, I thought it would be fun to try to get AutoCAD to talk to a Ruby on Rails application.

My first thought was to use ActiveResource. (Seemed logical to me)
First, I used the IronRuby igems.bat to install rails. That went well so now I was off to the races.

Next I added

1
require 'activeresource'

to an .rb file and tried to load it into autocad. That didn’t work too well because, it seems, when creating a new hosted IronRuby run-time, the search paths are set to the local Autocad install directory and “.”. No worries, I added the appropriate paths (just copied the paths from my local ir.exe paths).

I tried again to load the file was rewarded for my efforts with a 10 line backtrace. I tracked the problem down to a call to

1
require 'iconv' 

in the likeliest of places, C:\ironruby-0.9.0\lib\IronRuby\gems\1.8\gems\activesupport-2.3.4\lib\active_support\inflector.rb

Long story made short, there was, in the local Autocad installation directory, a file called iconv.dll that was the culprit. So I remove the Autocad directory from the search path and from then on everything was too-easy, dead-simple.

I created a block whose attribute tags matched exactly the attributes of the model (Products, in this case) of my rails apps.
(manufacturer:string, modelnumber:string, color:string, size:string).

The helper method that I posted here uses an OpenStruct to mimic the attribute structure. Once that struct was created, I used the OpenStruct#marshal_dump method to create a hash to pass to the Product constructor of my ActiveResource object and then saved it. (It is actually harder to explain than it was to code. Is that a good thing or a bad thing?)

Here is the code. Of course, it is just proof of concept. It doesn’t handle errors very well or authentication (at all), but I think it does show how easy it can be to have Autocad communicate with a Ruby on Rails app.

The Rails application did not require anything special. I used script/generate scaffold and rake db:migrate. I did zero coding.

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
32
require 'acad2009.rb'
require 'AcadHelper'
 
include AcadHelper

#add a Loading message because requiring activeresource can be slow
puts "\nLoading" 
require 'activeresource'

class Product < ActiveResource::Base
   #connect to my MacBook Pro which is running my rails app        
   self.site = "http://192.168.1.102:3000/"  
end

def save_to_remote_db
        begin
            puts "\nSelect Block References to add to remote database:"
                select_on_screen.each do |ss_ent|
                        get_block_ref(ss_ent.ObjectId, :Read) do |attribs|
                           #all the magic happens in one line of code
                            if Product.new(attribs.marshal_dump).save  
                                    puts "\nProduct saved"
                            else
                                    puts "\nProblem saving product"
                            end        
                        end
                end
        
        rescue  Exception => e
                puts_ex e
        end
end

If the tables didn’t match exactly, one would just need a simple map of the fields before saving

1
2
3
4
5
prod = Product.new
prod.field1 = attrib.corresponding_field1
...
prod.fieldN = attrib.corresponding_fieldN
prod.save

There are no connection strings or any information about the database needed other than a URL and some knowledge of the table to which you are storing. ActiveResource automagically handles the rest. (no pun intended)

If you are playing along at home, the current version of acad2009.rb has the search path edits that worked on my system. You may (and probably will) have to adjust those paths to my your setup. You can easily use different acadXXXX.rb files to experiment with various setups.

Coming up next, AutoCAD talks to Google Maps!!!

Have fun!

BlockReference Attribute Handling

I am currently testing a method that simplifies the editing of block reference attributes.

Given a simple block reference with attributes Name, Sex, Age, and Height, wouldn’t it be nice to be able to
edit the attributes like:

1
2
attribs.age = 45
attribs.name = "David"

So I hacked together a little method that uses my TransHelper class and an OpenStruct to store the attribute tags and values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def get_block_ref(obj_id, mode=:Read)
    begin
        helper = TransHelper.new
        helper.trans([:Block])  do |tr, db, tables|
                ref = helper.get_obj obj_id, mode
                attrib_hash = {}
                ref.AttributeCollection.each do |id|
                        attrib = helper.get_obj id, :Read
                        attrib_hash[attrib.Tag.downcase] = attrib.TextString
                end
                @attribs = OpenStruct.new(attrib_hash)
                yield @attribs
                #update the attribs
                if mode == :Write
                        ref.AttributeCollection.each do |id|
                                attrib = helper.get_obj id, :Write
                                attrib.TextString = @attribs.send(attrib.Tag.downcase).to_s
                        end
                end        
            end    
        rescue Exception => e
                puts_ex e
        end
end

This little methods allows me to write an entire attribute editing method with very little coding. All that is really needed is some knowledge of the attribute tags (but surely we know the tag names if were are coding against the block, right?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def edit_block_ref
        begin
          ref_id = get_entity_id #from AcadHelper to pick an entity on the screen
          get_block_ref(ref_id, :Write) do |attribs|
            attribs.age = 50  #numbers can be used - get_block_ref will convert to string.  YEA!
            attribs.name = "Chuck"
            attribs.height = "72 inches"
            attribs.sex = "Male"
           end
        rescue Exception => e
          puts_ex e
        end
end

I’m still testing this, but feel free to take if for a test ride.

When I complete the testing, I will add the method to AcadHelper

Ruby and AutoCAD via IronRuby

Inspired by a blog post by Kean Walmsley here, I have been working on some wrapper and helper functions for driving AutoCAD using Ruby.

IronRuby and Kean’s loader function gives the AutoCAD developer full access to the managed API. But any that has coded against that API knows it can be verbose and lead to some not-so-readable code, like the C# code below.

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
32
33

[CommandMethod("AddLine")]
public static void AddLine()
{
// Get the current document and database

  Document acDoc = Application.DocumentManager.MdiActiveDocument;
  Database acCurDb = acDoc.Database;

  // Start a transaction
  using (Transaction acTrans = acCurDb.TransactionManager.StartTransaction())
  {
    // Open the Block table for read
    BlockTable acBlkTbl;
    acBlkTbl = acTrans.GetObject(acCurDb.BlockTableId, OpenMode.ForRead) as BlockTable;

      // Open the Block table record Model space for write
    BlockTableRecord acBlkTblRec;
    acBlkTblRec = acTrans.GetObject(acBlkTbl[BlockTableRecord.ModelSpace],
    OpenMode.ForWrite) as BlockTableRecord;

     // Create a line that starts at 5,5 and ends at 12,3
    Line acLine = new Line(new Point3d(5, 5, 0), new Point3d(12, 3, 0));
    acLine.SetDatabaseDefaults();

    // Add the new object to the block table record and the transaction
    acBlkTblRec.AppendEntity(acLine);
    acTrans.AddNewlyCreatedDBObject(acLine, true);
    // Save the new object to the database
    acTrans.Commit();
  }
}

In Ruby, using my AcadHelper module,that code becomes

1
2
3
4
5
6

def add_line
  line = create_line [1,1], [2,4,5]
  line.add_to_model_space
end

Add in a begin-rescue-end block and it still only 8 lines of, in my opinion, very readable code

1
2
3
4
5
6
7
8
9

def add_line
  begin
    line = create_line [1,1], [2,4,5]
    line.add_to_model_space
  rescue Exception => e
    puts_ex e
  end
end

I hope Ruby catches on as development option for AutoCAD.
As a long-time AutoLISP hacker, I love having an interpreted language to use for development.
Ruby offers that, along with full access to the API and the beauty and power of Ruby

I have posted an initial verision of my AcadHelper.rb on github.
And development continues. I will be updated everything as time allows. I have added auto-loading
of functions, create_text and create_mtext functions, Editor.GetEntity helper,
an exception and backtrace print helper.

Next on the list is selection sets. If anyone wants to help out or has ideas, please let me know

Pages

Github

Categories

There Must Be An Easier Way © David Blackmon. Valid XHTML and ATOM. Powered by Enki.