September 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        










« Finding the location of a .NET module | Main | Linking Circles, Part 2: Getting persistent »

November 27, 2006

Linking Circles, Part 1: Using .NET events to relate AutoCAD geometry

I received this question some time ago from Paul Richardson from CAD System Engineering:

I have never been sure when to update objects programmatically. An example would be a user edits an entity that I’m tracking and I need to edit another entity in reaction to that change. Is there a standard used to cache the handle, and make the change.

Doesn’t seem editing of entities should be done in the event that is watching for the change. When/how does one do it? Doesn’t seem to be any info on a standard for this.

This is such an excellent question I'm going to spend a number of posts answering it. :-)

Introduction

But first, time for a little nostalgia. One of my first ObjectARX projects was back in 1996 - I'd been using LISP and ADS for several years by that point, but I decided what I really needed was a nice, juicy problem to help me immerse myself in ObjectARX. Along with a colleague at the time, I came up with the idea of using ObjectARX's notification mechanism to link circles together in a chain.

The idea was essentially that you "link" sets of two circles together, and whenever you move one of these circles, the other circle moves in the most direct line to stay attached to it. You would then be able to build up "chains" of linked circles, and the movement of the head of the chain would cause the rest of the chain to follow, with a ripple of notification events modifying one circle after the other.

It was my first major ObjectARX undertaking, so I was fairly heavy-handed with the architecture: each "link" was maintained by two persistent reactors - one attached to each of the linked entities. There were also a number of other reactors and objects involved in the whole system which, in spite of it's weight, worked pretty well. I demoed the sample to developers at a number of different events, to show the power of ObjectARX, and also built it into my first AutoCAD OEM demo application (called SnakeCAD :-).

Anyway - I hadn't thought about this code for several years, but then I received Paul's question and by chance stumbled across the source attached to an old email, so thought I'd spend some time reimplementing the system in .NET. I was able to recode the whole thing in less than a day, partly thanks to the additional experience of being 10 years longer-in-the-tooth, but mainly because of the advantages of using a much more modern development environment.

I'm going to serialize the code over a few posts. The first shows the basic implementation, which should allow you to focus on how the events do their stuff, and I'll later on deal with persistence of our data and some more advanced features (such as automatic linking and support for other circular - even spherical - objects).

The Basic Application

For this application I'm going to try something different, by putting line numbers in the below code (to make the explanation simpler) and providing the main class file as a download.

First, a little on the approach:

The basic application defines one single command - "LINK" (lines 162-194). This command asks the user to select two circles, which it then links together. It does this by using a special "link manager" object (the LinkedObjectManager class is defined from lines 23 to 115), which is used to maintain the references between the various circles.

This LinkedObjectManager stores one-to-many relationships by maintaining a Dictionary, mapping between ObjectIds and ObjectIdCollections. This means that any particular circle can be linked to multiple other circles. The relationships also get added bi-directionally, so the LinkedObjectManager will create a backwards link when it creates the forwards one (lines 37-38).

The linking behaviour is maintained by two main event callbacks: the first is Database.ObjectModified(), which is called whenever an object stored in the active drawing has been changed in some way. This event callback is implemented between lines 196 and 206. All it does is check whether the object that has been modified is one that is being "managed" by our link manager - if so, we add its ID to the list of entities to update later on (the collection that is declared on line 122).

This is really the answer to Paul's question: we store the ObjectId in a list that will get picked up in the Editor.CommandEnded() callback, where we go and update the various objects. My original implementation didn't do that: it opened the objects directly using Open()/Close() (which are marked as obsolete in the .NET API, as we're encouraging the use of Transactions instead), and made the changes right then. Overall the implementation in this version is safer and, I feel, more elegant - CommandEnded() is really the way to go for this kind of operation.

[Aside: for those of you that are ADN members, you should find additional information on this limitation on the ADN website. Here's an article that covers this for VBA, for instance: How to modify an object from object's Modified or document's ObjectAdded, ObjectModified, and so on events.]

The Editor.CommandEnded() callback is implemented between lines 219 and 227, and calls through to another function (UpdateLinkedEntities()) to do the heavy lifting (lines 230-316). This function checks the geometry of the linked objects - I've tried to keep the code fairly generic to make it easier for us to extend this later to handle non-circles - and moves the second one closer to the first one. This in turn fires the Database.ObjectModified() event again, which adds this entity's ObjectId into the list of entities to update. What's interesting about this implementation is that the foreach loop that is making the calls to UpdateLinkedEntities() for each object in the list (lines 222-225), will also take into account the newly added entities. This allows the change to ripple through the entire chain of circles.

Here's the C# code:

    1 using System;

    2 using System.Collections;

    3 using System.Collections.Generic;

    4 using Autodesk.AutoCAD.Runtime;

    5 using Autodesk.AutoCAD.ApplicationServices;

    6 using Autodesk.AutoCAD.DatabaseServices;

    7 using Autodesk.AutoCAD.EditorInput;

    8 using Autodesk.AutoCAD.Geometry;

    9

   10 [assembly:

   11   CommandClass(

   12     typeof(

   13       AsdkLinkingLibrary.LinkingCommands

   14     )

   15   )

   16 ]

   17

   18 namespace AsdkLinkingLibrary

   19 {

   20   /// <summary>

   21   /// Utility class to manage links between objects

   22   /// </summary>

   23   public class LinkedObjectManager

   24   {

   25     Dictionary<ObjectId, ObjectIdCollection> m_dict;

   26

   27     // Constructor

   28     public LinkedObjectManager()

   29     {

   30       m_dict =

   31         new Dictionary<ObjectId,ObjectIdCollection>();

   32     }

   33

   34     // Create a bi-directional link between two objects

   35     public void LinkObjects(ObjectId from, ObjectId to)

   36     {

   37       CreateLink(from, to);

   38       CreateLink(to, from);

   39     }

   40

   41     // Helper function to create a one-way

   42     // link between objects

   43     private void CreateLink(ObjectId from, ObjectId to)

   44     {

   45       ObjectIdCollection existingList;

   46       if (m_dict.TryGetValue(from, out existingList))

   47       {

   48         if (!existingList.Contains(to))

   49         {

   50           existingList.Add(to);

   51           m_dict.Remove(from);

   52           m_dict.Add(from, existingList);

   53         }

   54       }

   55       else

   56       {

   57         ObjectIdCollection newList =

   58           new ObjectIdCollection();

   59         newList.Add(to);

   60         m_dict.Add(from, newList);

   61       }

   62     }

   63

   64     // Remove bi-directional links from an object

   65     public void RemoveLinks(ObjectId from)

   66     {

   67       ObjectIdCollection existingList;

   68       if (m_dict.TryGetValue(from, out existingList))

   69       {

   70         m_dict.Remove(from);

   71         foreach (ObjectId id in existingList)

   72         {

   73           RemoveFromList(id, from);

   74         }

   75       }

   76     }

   77

   78     // Helper function to remove an object reference

   79     // from a list (assumes the overall list should

   80     // remain)

   81     private void RemoveFromList(

   82       ObjectId key,

   83       ObjectId toremove

   84     )

   85     {

   86       ObjectIdCollection existingList;

   87       if (m_dict.TryGetValue(key, out existingList))

   88       {

   89         if (existingList.Contains(toremove))

   90         {

   91           existingList.Remove(toremove);

   92           m_dict.Remove(key);

   93           m_dict.Add(key, existingList);

   94         }

   95       }

   96     }

   97

   98     // Return the list of objects linked to

   99     // the one passed in

  100     public ObjectIdCollection GetLinkedObjects(

  101       ObjectId from

  102     )

  103     {

  104       ObjectIdCollection existingList;

  105       m_dict.TryGetValue(from, out existingList);

  106       return existingList;

  107     }

  108

  109     // Check whether the dictionary contains

  110     // a particular key

  111     public bool Contains(ObjectId key)

  112     {

  113       return m_dict.ContainsKey(key);

  114     }

  115   }

  116   /// <summary>

  117   /// This class defines our commands and event callbacks.

  118   /// </summary>

  119   public class LinkingCommands

  120   {

  121     LinkedObjectManager m_linkManager;

  122     ObjectIdCollection m_entitiesToUpdate;

  123

  124     public LinkingCommands()

  125     {

  126       Document doc =

  127         Application.DocumentManager.MdiActiveDocument;

  128       Database db = doc.Database;

  129       db.ObjectModified +=

  130         new ObjectEventHandler(OnObjectModified);

  131       db.ObjectErased +=

  132         new ObjectErasedEventHandler(OnObjectErased);

  133       doc.CommandEnded +=

  134         new CommandEventHandler(OnCommandEnded);

  135

  136       m_linkManager = new LinkedObjectManager();

  137       m_entitiesToUpdate = new ObjectIdCollection();

  138     }

  139

  140     ~LinkingCommands()

  141     {

  142       try

  143       {

  144         Document doc =

  145           Application.DocumentManager.MdiActiveDocument;

  146         Database db = doc.Database;

  147         db.ObjectModified -=

  148           new ObjectEventHandler(OnObjectModified);

  149         db.ObjectErased -=

  150           new ObjectErasedEventHandler(OnObjectErased);

  151         doc.CommandEnded +=

  152           new CommandEventHandler(OnCommandEnded);

  153       }

  154       catch(System.Exception)

  155       {

  156         // The document or database may no longer

  157         // be available on unload

  158       }

  159     }

  160

  161     // Define "LINK" command

  162     [CommandMethod("LINK")]

  163     public void LinkEntities()

  164     {

  165       Document doc =

  166         Application.DocumentManager.MdiActiveDocument;

  167       Database db = doc.Database;

  168       Editor ed = doc.Editor;

  169

  170       PromptEntityOptions opts =

  171         new PromptEntityOptions(

  172           "\nSelect first circle to link: "

  173         );

  174       opts.AllowNone = true;

  175       opts.SetRejectMessage(

  176         "\nOnly circles can be selected."

  177       );

  178       opts.AddAllowedClass(typeof(Circle), false);

  179

  180       PromptEntityResult res = ed.GetEntity(opts);

  181       if (res.Status == PromptStatus.OK)

  182       {

  183         ObjectId from = res.ObjectId;

  184         opts.Message =

  185           "\nSelect second circle to link: ";

  186         res = ed.GetEntity(opts);

  187         if (res.Status == PromptStatus.OK)

  188         {

  189           ObjectId to = res.ObjectId;

  190           m_linkManager.LinkObjects(from, to);

  191           m_entitiesToUpdate.Add(from);

  192         }

  193       }

  194     }

  195

  196     // Define callback for Database.ObjectModified event

  197     private void OnObjectModified(

  198       object sender, ObjectEventArgs e)

  199     {

  200       ObjectId id = e.DBObject.ObjectId;

  201       if (m_linkManager.Contains(id) &&

  202           !m_entitiesToUpdate.Contains(id))

  203       {

  204         m_entitiesToUpdate.Add(id);

  205       }

  206     }

  207

  208     // Define callback for Database.ObjectErased event

  209     private void OnObjectErased(

  210       object sender, ObjectErasedEventArgs e)

  211     {

  212       if (e.Erased)

  213       {

  214         m_linkManager.RemoveLinks(e.DBObject.ObjectId);

  215       }

  216     }

  217

  218     // Define callback for Document.CommandEnded event

  219     private void OnCommandEnded(

  220       object sender, CommandEventArgs e)

  221     {

  222       foreach (ObjectId id in m_entitiesToUpdate)

  223       {

  224         UpdateLinkedEntities(id);

  225       }

  226       m_entitiesToUpdate.Clear();

  227     }

  228

  229     // Helper function for OnCommandEnded

  230     private void UpdateLinkedEntities(ObjectId from)

  231     {

  232       Document doc =

  233         Application.DocumentManager.MdiActiveDocument;

  234       Editor ed = doc.Editor;

  235       Database db = doc.Database;

  236

  237       ObjectIdCollection linked =

  238         m_linkManager.GetLinkedObjects(from);

  239

  240       Transaction tr =

  241         db.TransactionManager.StartTransaction();

  242       using (tr)

  243       {

  244         try

  245         {

  246           Point3d firstCenter;

  247           Point3d secondCenter;

  248           double firstRadius;

  249           double secondRadius;

  250

  251           Entity ent =

  252             (Entity)tr.GetObject(from, OpenMode.ForRead);

  253

  254           if (GetCenterAndRadius(

  255                 ent,

  256                 out firstCenter,

  257                 out firstRadius

  258               )

  259           )

  260           {

  261             foreach (ObjectId to in linked)

  262             {

  263               Entity ent2 =

  264                 (Entity)tr.GetObject(to, OpenMode.ForRead);

  265               if (GetCenterAndRadius(

  266                     ent2,

  267                     out secondCenter,

  268                     out secondRadius

  269                   )

  270               )

  271               {

  272                 Vector3d vec = firstCenter - secondCenter;

  273                 if (!vec.IsZeroLength())

  274                 {

  275                   // Only move the linked circle if it's not

  276                   // already near enough               

  277                   double apart =

  278                   vec.Length - (firstRadius + secondRadius);

  279                   if (apart < 0.0)

  280                     apart = -apart;

  281

  282                   if (apart > 0.00001)

  283                   {

  284                     ent2.UpgradeOpen();

  285                     ent2.TransformBy(

  286                       Matrix3d.Displacement(

  287                         vec.GetNormal() * apart

  288                       )

  289                     );

  290                   }

  291                 }

  292               }

  293             }

  294           }

  295         }

  296         catch (System.Exception ex)

  297         {

  298           Autodesk.AutoCAD.Runtime.Exception ex2 =

  299             ex as Autodesk.AutoCAD.Runtime.Exception;

  300           if (ex2 != null &&

  301               ex2.ErrorStatus != ErrorStatus.WasOpenForUndo)

  302           {

  303             ed.WriteMessage(

  304               "\nAutoCAD exception: {0}", ex2

  305             );

  306           }

  307           else if (ex2 == null)

  308           {

  309             ed.WriteMessage(

  310               "\nSystem exception: {0}", ex

  311             );

  312           }

  313         }

  314         tr.Commit();

  315       }

  316     }

  317

  318     // Helper function to get the center and radius

  319     // for all supported circular objects

  320     private bool GetCenterAndRadius(

  321       Entity ent,

  322       out Point3d center,

  323       out double radius

  324     )

  325     {

  326       // For circles it's easy...

  327       Circle circle = ent as Circle;

  328       if (circle != null)

  329       {

  330         center = circle.Center;

  331         radius = circle.Radius;

  332         return true;

  333       }

  334       else

  335       {

  336         // Throw in some empty values...

  337         // Returning false indicates the object

  338         // passed in was not useable

  339         center = Point3d.Origin;

  340         radius = 0.0;

  341         return false;

  342       }

  343     }

  344   }

  345 }

Here's what happens when you execute the LINK command on some circles you've drawn...

Some circles:

Linkedcircles_1_prelinking_1

After the LINK command has been used to link them together, two-by-two:

Linkedcircles_1_postlinking 

Now grip-move the head of the chain:

Linkedcircles_1_prestretch

And here's the result - the chain moves to remain attached to the head:

Linkedcircles_1_poststretch_1

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e200d834c9c94853ef

Listed below are links to weblogs that reference Linking Circles, Part 1: Using .NET events to relate AutoCAD geometry:

blog comments powered by Disqus

Feed/Share

10 Random Posts