Sunday, January 11, 2015

Getting objects in the modelspace by Type

So we're going to talk about getting objects from the modelspace. First things first, I'm not really a fan of getting objects from the modelspace utilizing "TypedValues" because they're a pain in the ass ;) (sorry to all of you "TypedValue" purists, but you know its true...), so we're going to explore a different way of doing it. We're also going to run some metrics on both of the possible solutions to see which way is actually more efficient. I say this like i don't already know, haha.

Getting started, lets talk about some of the more "advanced" features of my code usage in order to get an idea about what we are working with.And by "advanced" I mean relatively basic C# syntax, however, by comparison its not the run-of-the-mill AutoCAD dev code I see on the line. We will start at the top of the method:

NOTE: If you are familiar with the .net framework please feel free to skip the explanations. If not, you can use the links to review MSDN explanations and examples.

IEnumerable<T> : The most base type of  an object that can be iterated over.
params : You can pass a variable number of parameters to the method.
Func<T,T> : A Delegate type that takes a parameter and returns a value.


using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;

public IEnumerable<ObjectID> GetIdsByTypeIteration(params Type[] types)
{
    // We will use this Delegate to return the Class of the RXObject
    Func<Type, RXClass> getClass = RXObject.GetClass;

    // Make a HashSet of the Types of Entities we want to get from the modelspace.
    var acceptableTypes = new HashSet<rxclass>(

    // Use a little linq with a method group to iterate through the array
    types.Select(getClass));

    var doc = Application.DocumentManager.MdiActiveDocument;

    // Create a transaction to grab the modelspace. Here you can use a regular transaction
    // "TransactionManager.StartTransaction()". I prefer StartOpenClose out of habit
    // because its a bit faster.
    using (var trans = doc.TransactionManager.StartOpenCloseTransaction())
    {
        var modelspace = (BlockTableRecord) 
            // Here we utilize "SymbolUtilityServices" to get the Modelspace id from our database.
            trans.GetObject(SymbolUtilityServices.GetBlockModelSpaceId(doc.Database), OpenMode.ForRead);

        // We use a little more linq to cast the IEnumerable object and iterate thru its ObjectIds
        // we will check our hashset contains the RXClass of the ObjectId by accessing the "ObjectClass" 
        // property of the RXObject.
        var ids = modelspace.Cast<ObjectID>().Where(id => acceptableTypes.Contains(id.ObjectClass));

        // Commit the transaction.
        trans.Commit();

        return ids;
    }
}

So our implementation above is pretty basic, Analyzing the given code, we are iterating over a collection of ObjectIDs, which are structs (value types). During this process (in memory) we are copying the objects from unmanaged memory to the managed stack, which is good. Since we are working with a locally defined collection we will not be creating a really large collection on our LOH (Large Object Heap) and effectively avoiding LOH fragmentation. Because we iterate over the objects only one time, our Big O notation would be O(n), (given n is the count of available objectIds in the model space) which is the best case scenario. However, If you wanted to further check to see if the polyline was a certain color, you should change this implementation so that you don't have to iterate over a smaller collection again, ergo changing your big O notation and the effectiveness of your algorithm.

 That was probably to deep for the majority of people reading this, but it was meant to catch your attention.. Go read about Memory Management, It is interesting.:)

Moving forward. Our default implementation for the TypedValue approach would be a method that would have the functionality to get multiple types. The problem with this is types that are represented differently in the unmanaged code (C++). For instance, Polylines would be an issue since there is not a LWPolyline type in the managed API.

Compare the implementations of calling each method and think about best practices. Personally, I like to stay away from string literals as much as possible.

The iterative approach:
var ids = GetIdsByTypeIteration(typeof(Polyline), typeof(Polyline2d), typeof(Polyline3d));

The TypedValue approach:
var ids = GetIdsByTypeTypeValue("POLYLINE", "LWPOLYLINE", "POLYLINE2D", "POLYLINE3d");


Now, we are going to execute the same task, but using the TypedValue approach:
using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;

public ObjectIdCollection GetIdsByTypeTypeValue(params string[] types)
{
    // Get the document
    var doc = Application.DocumentManager.MdiActiveDocument;

    // Get the editor to make the selection
    Editor oEd = doc.Editor;

    // Add our or operators so we can grab multiple types.
    IList<TypedValue> typedValueSelection =  new List<TypedValue> {
                    new TypedValue(Convert.ToInt32(DxfCode.Operator), "<or"),
                    new TypedValue(Convert.ToInt32(DxfCode.Operator), "or>")
                }; 

    // We will need to insert our requested types into the collection.
    // Since we knew we would have to insert they types inbetween the operators..
    // I used a Enumerable type which gave me that functionallity. (IListf<T>)
    foreach (var type in types)
        typedValueSelection.Insert(1,new TypedValue(Convert.ToInt32(DxfCode.Start),type));
    
    SelectionFilter selectionFilter = new SelectionFilter(typedValueSelection.ToArray());

    // because we have to.. Not really sure why, I assume this is our only access point
    // to grab the entities that we want. (I am open to being corrected)
    PromptSelectionResult promptSelectionResult = oEd.SelectAll(selectionFilter);

    // return our new ObjectIdCollection that is "Hopefully" full of the types that we want.
    return new ObjectIdCollection(promptSelectionResult.Value.GetObjectIds());
}

As you can see they're both almost the same amount of code, which is cool. There is a new collection type in each. In the TypedValue implementation, you have to use some custom Acad Types.. but that is cool too...

To get these metrics, I made a drawing with > 200k objects comprised of Polylines, circles and lines. During each test, I roughly halved the amount of objects in the modelspace. Here are the metrics strait from the command line.. I'll let you be the judge... Feel free to leave feed back and/or download the solution for this post.



No comments:

Post a Comment