In the previous posts we looked at some code to link AutoCAD entities via .NET events, and how to persist the link data in the drawing file.
This post extends the previous code to automatically link circles into the head of the chain, as circles are drawn by the user. The changes to the project are relatively modest compared to last time. Once again, the source is both available for download and listed below with changed line-numbers in red.
Some notes on the changes:
First we declare some new variables in our command class: a boolean (m_autolink - line 463) which tells us whether automatic linking is "on" or "off", and an ObjectId (m_lastEntity - line 464), which we will use to find the most recently created, linked object. We could have made this a setting in our LinkedObjectManager class, should we have wanted to make this a persistent setting, for instance, but for simplicity's sake we'll leave it in the command class for now.
We have to register (lines 475-476) and unregister (lines 497-498) a handler for another event - OnObjectAppended(). It's via this callback that we're informed that a new object has been added to the drawing.
Next we define our AUTOLINK command (lines 547-563). This simply toggles the m_autolink setting between true and false (or on and off). We might have chosen to display the current setting and ask whether the user wanted to change it to on or off, but frankly that seemed like overkill. If you don't like what you've set it to, you can just call the command again. :-)
A minor change was needed to OnObjectErased(), to set the value of m_lastEntity to Null, should the entity be erased. This also gets caught by a change in the code right at the end, but it's cleaner coding to make the behaviour right here, also.
Next we have the guts of our implementation (such that it is), which is the OnObjectAppended() callback definition (lines 612-636). Here we check whether the object added is a circle, and if so, we either link it to the last one added (stored in m_lastEntity), or - if the value of m_lastEntity is Null, for whatever reason - then we simply make it the next object to be linked in, and leave it at that.
And finally there's a minor change I added to more elegantly support UNDO (which also applies to the code in the first two posts, although I won't go and update them now). Because we're not persisting the state of our links synchronously in the drawing database, they don't automatically participate in the undo mechanism (e.g. if the user uses the UNDO command, we would have to do a little extra work to recreate the correct "last object to link to" settings). Rather than implement the equivalent of our own undo mechanism, I decided not to bother, and simply made sure that when a link is to an erased object, we simply give up (without any error message). This shouldn't happen very often - as we have our OnObjectErased() callback, but you never know. It does mean that the "bad" links might continue to exist in our LinkedObjectManager, they just won't work. The next time the data is saved and reloaded, though, these links effectively get purged. To really make this a production-ready app, I feel a little more attention is needed in this area... that said, the foundation is certainly there for you to work from (just please test thoroughly for your specific situation, of course).
Now for 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 and save links
22 /// between objects
23 /// </summary>
24 public class LinkedObjectManager
25 {
26 const string kCompanyDict =
27 "AsdkLinks";
28 const string kApplicationDict =
29 "AsdkLinkedObjects";
30 const string kXrecPrefix =
31 "LINKXREC";
32
33 Dictionary<ObjectId, ObjectIdCollection> m_dict;
34
35 // Constructor
36 public LinkedObjectManager()
37 {
38 m_dict =
39 new Dictionary<ObjectId,ObjectIdCollection>();
40 }
41
42 // Create a bi-directional link between two objects
43 public void LinkObjects(ObjectId from, ObjectId to)
44 {
45 CreateLink(from, to);
46 CreateLink(to, from);
47 }
48
49 // Helper function to create a one-way
50 // link between objects
51 private void CreateLink(ObjectId from, ObjectId to)
52 {
53 ObjectIdCollection existingList;
54 if (m_dict.TryGetValue(from, out existingList))
55 {
56 if (!existingList.Contains(to))
57 {
58 existingList.Add(to);
59 m_dict.Remove(from);
60 m_dict.Add(from, existingList);
61 }
62 }
63 else
64 {
65 ObjectIdCollection newList =
66 new ObjectIdCollection();
67 newList.Add(to);
68 m_dict.Add(from, newList);
69 }
70 }
71
72 // Remove bi-directional links from an object
73 public void RemoveLinks(ObjectId from)
74 {
75 ObjectIdCollection existingList;
76 if (m_dict.TryGetValue(from, out existingList))
77 {
78 m_dict.Remove(from);
79 foreach (ObjectId id in existingList)
80 {
81 RemoveFromList(id, from);
82 }
83 }
84 }
85
86 // Helper function to remove an object reference
87 // from a list (assumes the overall list should
88 // remain)
89 private void RemoveFromList(
90 ObjectId key,
91 ObjectId toremove
92 )
93 {
94 ObjectIdCollection existingList;
95 if (m_dict.TryGetValue(key, out existingList))
96 {
97 if (existingList.Contains(toremove))
98 {
99 existingList.Remove(toremove);
100 m_dict.Remove(key);
101 m_dict.Add(key, existingList);
102 }
103 }
104 }
105
106 // Return the list of objects linked to
107 // the one passed in
108 public ObjectIdCollection GetLinkedObjects(
109 ObjectId from
110 )
111 {
112 ObjectIdCollection existingList;
113 m_dict.TryGetValue(from, out existingList);
114 return existingList;
115 }
116
117 // Check whether the dictionary contains
118 // a particular key
119 public bool Contains(ObjectId key)
120 {
121 return m_dict.ContainsKey(key);
122 }
123
124 // Save the link information to a special
125 // dictionary in the database
126 public void SaveToDatabase(Database db)
127 {
128 Transaction tr =
129 db.TransactionManager.StartTransaction();
130 using (tr)
131 {
132 ObjectId dictId =
133 GetLinkDictionaryId(db, true);
134 DBDictionary dict =
135 (DBDictionary)tr.GetObject(
136 dictId,
137 OpenMode.ForWrite
138 );
139 int xrecCount = 0;
140
141 foreach (
142 KeyValuePair<ObjectId, ObjectIdCollection> kv
143 in m_dict
144 )
145 {
146 // Prepare the result buffer with our data
147 ResultBuffer rb =
148 new ResultBuffer(
149 new TypedValue(
150 (int)DxfCode.SoftPointerId,
151 kv.Key
152 )
153 );
154 int i = 1;
155 foreach (ObjectId id in kv.Value)
156 {
157 rb.Add(
158 new TypedValue(
159 (int)DxfCode.SoftPointerId + i,
160 id
161 )
162 );
163 i++;
164 }
165
166 // Update or create an xrecord to store the data
167 Xrecord xrec;
168 bool newXrec = false;
169 if (dict.Contains(
170 kXrecPrefix + xrecCount.ToString()
171 )
172 )
173 {
174 // Open the existing object
175 DBObject obj =
176 tr.GetObject(
177 dict.GetAt(
178 kXrecPrefix + xrecCount.ToString()
179 ),
180 OpenMode.ForWrite
181 );
182 // Check whether it's an xrecord
183 xrec = obj as Xrecord;
184 if (xrec == null)
185 {
186 // Should never happen
187 // We only store xrecords in this dict
188 obj.Erase();
189 xrec = new Xrecord();
190 newXrec = true;
191 }
192 }
193 // No object existed - create a new one
194 else
195 {
196 xrec = new Xrecord();
197 newXrec = true;
198 }
199 xrec.XlateReferences = true;
200 xrec.Data = (ResultBuffer)rb;
201 if (newXrec)
202 {
203 dict.SetAt(
204 kXrecPrefix + xrecCount.ToString(),
205 xrec
206 );
207 tr.AddNewlyCreatedDBObject(xrec, true);
208 }
209 xrecCount++;
210 }
211
212 // Now erase the left-over xrecords
213 bool finished = false;
214 do
215 {
216 if (dict.Contains(
217 kXrecPrefix + xrecCount.ToString()
218 )
219 )
220 {
221 DBObject obj =
222 tr.GetObject(
223 dict.GetAt(
224 kXrecPrefix + xrecCount.ToString()
225 ),
226 OpenMode.ForWrite
227 );
228 obj.Erase();
229 }
230 else
231 {
232 finished = true;
233 }
234 xrecCount++;
235 } while (!finished);
236 tr.Commit();
237 }
238 }
239
240 // Load the link information from a special
241 // dictionary in the database
242 public void LoadFromDatabase(Database db)
243 {
244 Document doc =
245 Application.DocumentManager.MdiActiveDocument;
246 Editor ed = doc.Editor;
247 Transaction tr =
248 db.TransactionManager.StartTransaction();
249 using (tr)
250 {
251 // Try to find the link dictionary, but
252 // do not create it if one isn't there
253 ObjectId dictId =
254 GetLinkDictionaryId(db, false);
255 if (dictId.IsNull)
256 {
257 ed.WriteMessage(
258 "\nCould not find link dictionary."
259 );
260 return;
261 }
262
263 // By this stage we can assume the dictionary exists
264 DBDictionary dict =
265 (DBDictionary)tr.GetObject(
266 dictId, OpenMode.ForRead
267 );
268 int xrecCount = 0;
269 bool done = false;
270
271 // Loop, reading the xrecords one-by-one
272 while (!done)
273 {
274 if (dict.Contains(
275 kXrecPrefix + xrecCount.ToString()
276 )
277 )
278 {
279 ObjectId recId =
280 dict.GetAt(
281 kXrecPrefix + xrecCount.ToString()
282 );
283 DBObject obj =
284 tr.GetObject(recId, OpenMode.ForRead);
285 Xrecord xrec = obj as Xrecord;
286 if (xrec == null)
287 {
288 ed.WriteMessage(
289 "\nDictionary contains non-xrecords."
290 );
291 return;
292 }
293 int i = 0;
294 ObjectId from = new ObjectId();
295 ObjectIdCollection to =
296 new ObjectIdCollection();
297 foreach (TypedValue val in xrec.Data)
298 {
299 if (i == 0)
300 from = (ObjectId)val.Value;
301 else
302 {
303 to.Add((ObjectId)val.Value);
304 }
305 i++;
306 }
307 // Validate the link info and add it to our
308 // internal data structure
309 AddValidatedLinks(db, from, to);
310 xrecCount++;
311 }
312 else
313 {
314 done = true;
315 }
316 }
317 tr.Commit();
318 }
319 }
320
321 // Helper function to validate links before adding
322 // them to the internal data structure
323 private void AddValidatedLinks(
324 Database db,
325 ObjectId from,
326 ObjectIdCollection to
327 )
328 {
329 Document doc =
330 Application.DocumentManager.MdiActiveDocument;
331 Editor ed = doc.Editor;
332 Transaction tr =
333 db.TransactionManager.StartTransaction();
334 using (tr)
335 {
336 try
337 {
338 ObjectIdCollection newList =
339 new ObjectIdCollection();
340
341 // Open the "from" object
342 DBObject obj =
343 tr.GetObject(from, OpenMode.ForRead, false);
344 if (obj != null)
345 {
346 // Open each of the "to" objects
347 foreach (ObjectId id in to)
348 {
349 DBObject obj2;
350 try
351 {
352 obj2 =
353 tr.GetObject(id, OpenMode.ForRead, false);
354 // Filter out the erased "to" objects
355 if (obj2 != null)
356 {
357 newList.Add(id);
358 }
359 }
360 catch (System.Exception)
361 {
362 ed.WriteMessage(
363 "\nFiltered out link to an erased object."
364 );
365 }
366 }
367 // Only if the "from" object and at least
368 // one "to" object exist (and are unerased)
369 // do we add an entry for them
370 if (newList.Count > 0)
371 {
372 m_dict.Add(from, newList);
373 }
374 }
375 }
376 catch (System.Exception)
377 {
378 ed.WriteMessage(
379 "\nFiltered out link from an erased object."
380 );
381 }
382 tr.Commit();
383 }
384 }
385
386 // Helper function to get (optionally create)
387 // the nested dictionary for our xrecord objects
388 private ObjectId GetLinkDictionaryId(
389 Database db,
390 bool createIfNotExisting
391 )
392 {
393 ObjectId appDictId = ObjectId.Null;
394
395 Transaction tr =
396 db.TransactionManager.StartTransaction();
397 using (tr)
398 {
399 DBDictionary nod =
400 (DBDictionary)tr.GetObject(
401 db.NamedObjectsDictionaryId,
402 OpenMode.ForRead
403 );
404 // Our outer level ("company") dictionary
405 // does not exist
406 if (!nod.Contains(kCompanyDict))
407 {
408 if (!createIfNotExisting)
409 return ObjectId.Null;
410
411 // Create both the "company" dictionary...
412 DBDictionary compDict = new DBDictionary();
413 nod.UpgradeOpen();
414 nod.SetAt(kCompanyDict, compDict);
415 tr.AddNewlyCreatedDBObject(compDict, true);
416
417 // ... and the inner "application" dictionary.
418 DBDictionary appDict = new DBDictionary();
419 appDictId =
420 compDict.SetAt(kApplicationDict, appDict);
421 tr.AddNewlyCreatedDBObject(appDict, true);
422 }
423 else
424 {
425 // Our "company" dictionary exists...
426 DBDictionary compDict =
427 (DBDictionary)tr.GetObject(
428 nod.GetAt(kCompanyDict),
429 OpenMode.ForRead
430 );
431 /// So check for our "application" dictionary
432 if (!compDict.Contains(kApplicationDict))
433 {
434 if (!createIfNotExisting)
435 return ObjectId.Null;
436
437 // Create the "application" dictionary
438 DBDictionary appDict = new DBDictionary();
439 compDict.UpgradeOpen();
440 appDictId =
441 compDict.SetAt(kApplicationDict, appDict);
442 tr.AddNewlyCreatedDBObject(appDict, true);
443 }
444 else
445 {
446 // Both dictionaries already exist...
447 appDictId = compDict.GetAt(kApplicationDict);
448 }
449 }
450 tr.Commit();
451 }
452 return appDictId;
453 }
454 }
455
456 /// <summary>
457 /// This class defines our commands and event callbacks.
458 /// </summary>
459 public class LinkingCommands
460 {
461 LinkedObjectManager m_linkManager;
462 ObjectIdCollection m_entitiesToUpdate;
463 bool m_autoLink = false;
464 ObjectId m_lastEntity = ObjectId.Null;
465
466 public LinkingCommands()
467 {
468 Document doc =
469 Application.DocumentManager.MdiActiveDocument;
470 Database db = doc.Database;
471 db.ObjectModified +=
472 new ObjectEventHandler(OnObjectModified);
473 db.ObjectErased +=
474 new ObjectErasedEventHandler(OnObjectErased);
475 db.ObjectAppended +=
476 new ObjectEventHandler(OnObjectAppended);
477 db.BeginSave +=
478 new DatabaseIOEventHandler(OnBeginSave);
479 doc.CommandEnded +=
480 new CommandEventHandler(OnCommandEnded);
481
482 m_linkManager = new LinkedObjectManager();
483 m_entitiesToUpdate = new ObjectIdCollection();
484 }
485
486 ~LinkingCommands()
487 {
488 try
489 {
490 Document doc =
491 Application.DocumentManager.MdiActiveDocument;
492 Database db = doc.Database;
493 db.ObjectModified -=
494 new ObjectEventHandler(OnObjectModified);
495 db.ObjectErased -=
496 new ObjectErasedEventHandler(OnObjectErased);
497 db.ObjectAppended -=
498 new ObjectEventHandler(OnObjectAppended);
499 db.BeginSave -=
500 new DatabaseIOEventHandler(OnBeginSave);
501 doc.CommandEnded +=
502 new CommandEventHandler(OnCommandEnded);
503 }
504 catch(System.Exception)
505 {
506 // The document or database may no longer
507 // be available on unload
508 }
509 }
510
511 // Define "LINK" command
512 [CommandMethod("LINK")]
513 public void LinkEntities()
514 {
515 Document doc =
516 Application.DocumentManager.MdiActiveDocument;
517 Database db = doc.Database;
518 Editor ed = doc.Editor;
519
520 PromptEntityOptions opts =
521 new PromptEntityOptions(
522 "\nSelect first circle to link: "
523 );
524 opts.AllowNone = true;
525 opts.SetRejectMessage(
526 "\nOnly circles can be selected."
527 );
528 opts.AddAllowedClass(typeof(Circle), false);
529
530 PromptEntityResult res = ed.GetEntity(opts);
531 if (res.Status == PromptStatus.OK)
532 {
533 ObjectId from = res.ObjectId;
534 opts.Message =
535 "\nSelect second circle to link: ";
536 res = ed.GetEntity(opts);
537 if (res.Status == PromptStatus.OK)
538 {
539 ObjectId to = res.ObjectId;
540 m_linkManager.LinkObjects(from, to);
541 m_lastEntity = to;
542 m_entitiesToUpdate.Add(from);
543 }
544 }
545 }
546
547 // Define "AUTOLINK" command
548 [CommandMethod("AUTOLINK")]
549 public void ToggleAutoLink()
550 {
551 Document doc =
552 Application.DocumentManager.MdiActiveDocument;
553 Editor ed = doc.Editor;
554 m_autoLink = !m_autoLink;
555 if (m_autoLink)
556 {
557 ed.WriteMessage("\nAutomatic linking turned on.");
558 }
559 else
560 {
561 ed.WriteMessage("\nAutomatic linking turned off.");
562 }
563 }
564
565 // Define "LOADLINKS" command
566 [CommandMethod("LOADLINKS")]
567 public void LoadLinkSettings()
568 {
569 Document doc =
570 Application.DocumentManager.MdiActiveDocument;
571 Database db = doc.Database;
572 m_linkManager.LoadFromDatabase(db);
573 }
574
575 // Define "SAVELINKS" command
576 [CommandMethod("SAVELINKS")]
577 public void SaveLinkSettings()
578 {
579 Document doc =
580 Application.DocumentManager.MdiActiveDocument;
581 Database db = doc.Database;
582 m_linkManager.SaveToDatabase(db);
583 }
584
585 // Define callback for Database.ObjectModified event
586 private void OnObjectModified(
587 object sender, ObjectEventArgs e)
588 {
589 ObjectId id = e.DBObject.ObjectId;
590 if (m_linkManager.Contains(id) &&
591 !m_entitiesToUpdate.Contains(id))
592 {
593 m_entitiesToUpdate.Add(id);
594 }
595 }
596
597 // Define callback for Database.ObjectErased event
598 private void OnObjectErased(
599 object sender, ObjectErasedEventArgs e)
600 {
601 if (e.Erased)
602 {
603 ObjectId id = e.DBObject.ObjectId;
604 m_linkManager.RemoveLinks(id);
605 if (m_lastEntity == id)
606 {
607 m_lastEntity = ObjectId.Null;
608 }
609 }
610 }
611
612 // Define callback for Database.ObjectAppended event
613 void OnObjectAppended(object sender, ObjectEventArgs e)
614 {
615 Database db = sender as Database;
616 if (db != null)
617 {
618 if (m_autoLink)
619 {
620 if (e.DBObject.GetType() == typeof(Circle))
621 {
622 ObjectId from = e.DBObject.ObjectId;
623 if (m_lastEntity == ObjectId.Null)
624 {
625 m_lastEntity = from;
626 }
627 else
628 {
629 m_linkManager.LinkObjects(from, m_lastEntity);
630 m_lastEntity = from;
631 m_entitiesToUpdate.Add(from);
632 }
633 }
634 }
635 }
636 }
637
638 // Define callback for Database.BeginSave event
639 void OnBeginSave(object sender, DatabaseIOEventArgs e)
640 {
641 Database db = sender as Database;
642 if (db != null)
643 {
644 m_linkManager.SaveToDatabase(db);
645 }
646 }
647
648 // Define callback for Document.CommandEnded event
649 private void OnCommandEnded(
650 object sender, CommandEventArgs e)
651 {
652 foreach (ObjectId id in m_entitiesToUpdate)
653 {
654 UpdateLinkedEntities(id);
655 }
656 m_entitiesToUpdate.Clear();
657 }
658
659 // Helper function for OnCommandEnded
660 private void UpdateLinkedEntities(ObjectId from)
661 {
662 Document doc =
663 Application.DocumentManager.MdiActiveDocument;
664 Editor ed = doc.Editor;
665 Database db = doc.Database;
666
667 ObjectIdCollection linked =
668 m_linkManager.GetLinkedObjects(from);
669
670 Transaction tr =
671 db.TransactionManager.StartTransaction();
672 using (tr)
673 {
674 try
675 {
676 Point3d firstCenter;
677 Point3d secondCenter;
678 double firstRadius;
679 double secondRadius;
680
681 Entity ent =
682 (Entity)tr.GetObject(from, OpenMode.ForRead);
683
684 if (GetCenterAndRadius(
685 ent,
686 out firstCenter,
687 out firstRadius
688 )
689 )
690 {
691 foreach (ObjectId to in linked)
692 {
693 Entity ent2 =
694 (Entity)tr.GetObject(to, OpenMode.ForRead);
695 if (GetCenterAndRadius(
696 ent2,
697 out secondCenter,
698 out secondRadius
699 )
700 )
701 {
702 Vector3d vec = firstCenter - secondCenter;
703 if (!vec.IsZeroLength())
704 {
705 // Only move the linked circle if it's not
706 // already near enough
707 double apart =
708 vec.Length - (firstRadius + secondRadius);
709 if (apart < 0.0)
710 apart = -apart;
711
712 if (apart > 0.00001)
713 {
714 ent2.UpgradeOpen();
715 ent2.TransformBy(
716 Matrix3d.Displacement(
717 vec.GetNormal() * apart
718 )
719 );
720 }
721 }
722 }
723 }
724 }
725 }
726 catch (System.Exception ex)
727 {
728 Autodesk.AutoCAD.Runtime.Exception ex2 =
729 ex as Autodesk.AutoCAD.Runtime.Exception;
730 if (ex2 != null &&
731 ex2.ErrorStatus !=
732 ErrorStatus.WasOpenForUndo &&
733 ex2.ErrorStatus !=
734 ErrorStatus.WasErased
735 )
736 {
737 ed.WriteMessage(
738 "\nAutoCAD exception: {0}", ex2
739 );
740 }
741 else if (ex2 == null)
742 {
743 ed.WriteMessage(
744 "\nSystem exception: {0}", ex
745 );
746 }
747 }
748 tr.Commit();
749 }
750 }
751
752 // Helper function to get the center and radius
753 // for all supported circular objects
754 private bool GetCenterAndRadius(
755 Entity ent,
756 out Point3d center,
757 out double radius
758 )
759 {
760 // For circles it's easy...
761 Circle circle = ent as Circle;
762 if (circle != null)
763 {
764 center = circle.Center;
765 radius = circle.Radius;
766 return true;
767 }
768 else
769 {
770 // Throw in some empty values...
771 // Returning false indicates the object
772 // passed in was not useable
773 center = Point3d.Origin;
774 radius = 0.0;
775 return false;
776 }
777 }
778 }
779 }
Let's take a quick look at this code running. Here's an existing chain that we've created using LINK. We then use AUTOLINK to toggle the automatic linking to on, and start creating circles:
And that's it for this post. Next time we'll look at adding support for other object types, including 3D (woohoo!).