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

IronRuby and RSpec with color

So I’m using Rspec to test some IronRuby code on my Vista box. I tried the —colour switch only to be told that I needed to install the win32console gem. No biggie right? I installed that gem and was told something about something about something, I don’t even remember it all and can’t find my notes now.

The thing that I do remember is that I still could not get color output from Rspec. So I did some googling (or is it googleing?) and found iron-term-ansicolor.rb in this github repo by hotgazpacho .

So I gave that a try. It worked well in ir, but still wouldn’t work with Rspec. hotgazpacho was opening Kernel#puts but it appeared that I needed to open IO#write to get console.exe to show the colored output. ( I use console.exe when I runs tests on my Vista box. It is great).

So, using the iron-term-ansicolor.rb code as a guide, I hacked together this code

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
36
37
38
39
40
41
42
43
44
45
46
47
# somefile.rb
require 'mscorlib'


# add the trailing m  because some of the rspec output spans multiple lines
ANSI_REGEXP = /\e\[(.+?)m(.+?)(?=(\e\[0m|\z))/m
SsC=System::ConsoleColor

class IO
alias_method :original_write, :write

def write(*args)
   count = 0
   args.each do |arg|

   fg_color = System::Console.ForegroundColor
   bg_color = System::Console.BackgroundColor

  ##dkb
   if ANSI_REGEXP.match(arg)
     data = extract_ansi_data(arg)
   else
     data = {:text => arg}
   end 

   System::Console.ForegroundColor = data[:foreground] || fg_color
   System::Console.BackgroundColor = data[:background] || bg_color
   count = original_write(data[:text])
   System::Console.BackgroundColor = bg_color
   System::Console.ForegroundColor = fg_color
 end
 count
end

private
def extract_ansi_data(arg)
 fg_color_map =    Hash[30,SsC.Black,31,SsC.Red,32,SsC.DarkGreen ,
                             33,SsC.DarkYellow, 34, SsC.DarkBlue,35,SsC.DarkMagenta,
                              36,SsC.DarkCyan,37,SsC.Gray]
 bg_color_map = Hash[40,SsC.Black,41,SsC.DarkRed,42,SsC.DarkGreen,
                          43,SsC.DarkYellow, 44,SsC.DarkBlue, 45,SsC.DarkMagenta, 
                          46,SsC.DarkCyan,47,SsC.Gray]
 matches = ANSI_REGEXP.match(arg)
 fg_color = fg_color_map[matches[1].to_i] || System::Console.ForegroundColor
 bg_color = bg_color_map[matches[1].to_i] || System::Console.BackgroundColor
 { :foreground => fg_color, :background => bg_color,:text => matches[2]}
end

(Save this code somewhere in the IronRuby load path)

Then, I dove into the rspec code and in rspec-1.2.9\lib\spec\runner\options.rb I made this small change to the def colour= method (about line 191)

1
2
3
4
 ##comment out the line below
 #require 'Win32/Console/ANSI' ;\
 ## add this line
    require 'dkb-iron-term-ansicolor'  #or whatever you name the code above

With these 2 changes, I can use the -c option of Rspec to get the glorious green/red output when testing my IronRuby code. There may be a better, cleaner way, but I did a bit of searching and could not find it. This way seems to work, plus it gave me a good excuse to dig into the Rspec code.

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

Twitter_logo_s Github

Categories

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