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