Kean Walmsley


  • About the Author
    Kean on Google+

April 2014

Sun Mon Tue Wed Thu Fri Sat
    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      







« Performing Boolean operations on AutoCAD solids using .NET | Main | Google Chrome Web Lab’s Sketchbots: bits to atoms, zen-style »

January 30, 2013

Filtering lists of AutoCAD entities using LINQ

A comment on the last post made me think it’s probably worth diving into LINQ a bit further, as there’s clearly interest out there. Now I don’t actually use LINQ very much but whenever I do I tell myself I ought to use it more – it’s really very useful.

ChainA lot of LINQ is derived from the world of Functional Programming – in that it allows you to use higher order functions to manipulate data – and it’s really just another way in which .NET languages have been influenced by FP (and have evolved to incorporate its techniques in their increasingly multi-paradigm nature). This is a good thing.

When I think about using LINQ, I therefore tend to think back to “functional” languages – whether F# or LISP – to think about the types of operation that would streamline the process I’m trying to implement.

In the last post, we wanted to return the tail of a list (which means everything but the head, i.e. the first item). I chose to pass in the PromptSelectionResult object, as I felt it was more efficient to work from that directly:

private static ObjectId[] AllButFirst(PromptSelectionResult psr)

{

  // Use LINQ to skip the first item in the IEnumerable

  // and then return the results as an ObjectId array

 

  return

    psr.Value.Cast<SelectedObject>().Skip(1).

      Select(o => { return o.ObjectId; }).ToArray();

}

Breaking this down a little, we perform a cast on the contents of the IEnumerable (the Value property of PromptSelectionResult is a SelectionSet, which implements the IEnumerable interface), so that we know they’re SelectedObjects, and Skip the first of these results before using Select() to essentially map to a list of the referenced ObjectIds. And then we call ToArray() on the results to return an array of ObjectIds.

This is all well and good, but it’s a little specific to selection (in that it takes a PromptSelectionResult parameter). To generalise the behaviour – and make it ultimately more flexible – we can adjust the function to take an ObjectId array instead (as this is also an enumerable data-type in .NET):

private static ObjectId[] AllButFirst(ObjectId[] arr)

{

  return arr.Skip<ObjectId>(1).ToArray();

}

This is a bit simpler – as we no longer need to cast to an enumerable list of SelectedObjects and then map from these to ObjectIds – but means that we’re going to have to pass in an ObjectId array from the PromptSelectionResult object. We can do this via the GetObjectIds() method, which we assume (and this is really an important point) will return an array of ObjectIds in the order in which the objects were selected.

Let’s take a look at another example of using LINQ… this time we’ll essentially use it to filter a list of ObjectIds based on the type of object each belongs to. Here’s one version, which uses the LINQ extension methods to do the work in much the same way as we’ve seen above:

private static ObjectId[] OnlyOfClass(ObjectId[] ids, RXClass c)

{

  return

    ids.Where(id => { return id.ObjectClass == c; }).ToArray();

}

The beauty of this technique is that we don’t even need to open the object: the ObjectId class has a property that returns the type of the identified object. If we did need to go further, then it’d probably make sense to use the dynamic capabilities of ObjectId to open and close the associated object implicitly. But that’s for another day.

Now let’s rework the code a little to use the more elegant and “language-integrated” form (which is really just syntactic sugar – I haven’t actually taken the time to check but would expect the generated IL to be very similar if not identical).

private static ObjectId[] OnlyOfClass2(ObjectId[] ids, RXClass c)

{

  return

    (from id in ids where id.ObjectClass == c select id).ToArray();

}

To give these functions a try, let's now implement a quick command that allows you to select a bunch of objects and filter the results. I haven’t bothered implementing multiple commands for the different function alternatives (or to try with different object types) – that’s been left up to the reader.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System.Linq;

 

namespace SelectionManipulation

{

  public class Commands

  {

    [CommandMethod("SELFIL")]

    static public void FilterSelection()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      var ed = doc.Editor;

 

      var psr = ed.GetSelection();

      if (psr.Status != PromptStatus.OK)

        return;

 

      var ids =

        OnlyOfClass(

          psr.Value.GetObjectIds(),

          RXClass.GetClass(typeof(Line))

        );

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        ed.WriteMessage("\nFound {0} objects.", ids.Length);

 

        foreach (var id in ids)

        {

          var obj = tr.GetObject(id, OpenMode.ForRead);

 

          ed.WriteMessage(

            "\nThis object is really a {0}.", obj.GetType().Name

          );

        }

        tr.Commit();

      }

    }

 

    private static ObjectId[] AllButFirst(ObjectId[] arr)

    {

      return arr.Skip<ObjectId>(1).ToArray();

    }

 

    private static ObjectId[] OnlyOfClass(

      ObjectId[] ids, RXClass c

    )

    {

      return

        ids.Where(id => { return id.ObjectClass == c; }).ToArray();

    }

 

    private static ObjectId[] OnlyOfClass2(

      ObjectId[] ids, RXClass c

    )

    {

      return

        (from id in ids where id.ObjectClass == c select id).

        ToArray();

    }

  }

}

When we run the SELFIL command and select a number of entities – not all of which are lines – we will see something like this reported to the command-line:

Command: SELFIL

Select objects: Specify opposite corner: 8 found

Select objects:

Found 5 objects.

This object is really a Line.

This object is really a Line.

This object is really a Line.

This object is really a Line.

This object is really a Line.

In most cases, I’d expect people to want to implement a SelectionFilter to have this more “up-front” in the process – just as we saw last time for the user to only select Solid3d objects – but there are definitely cases where this is helpful. RealDWG clients, for instance, don’t have the Editor available to integrate more closely with AutoCAD’s selection mechanism, and so really need some way to easily filter large sets of entities. This approach should certainly help in that situation.

photo credit: James Jordan via photopin cc
blog comments powered by Disqus

10 Random Posts