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
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.
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