In the last three posts, we've looked at how to link circles using .NET events, how to store the link data in the drawing database, and how to create links automatically as objects are created.
In this post we'll extend the code to support 3D. Yes, that's right, we're going to add support for creating chains of spheres, and with surprisingly little additional effort.
Here's the source code for this post as a download.
Notes on the changes:
Lines 522, 526 and 536 are simply changes to prompts to refer to spheres as well as circles. To actually support the selection of spheres, we now make an additional call to add Solid3d as an allowed class (line 529).
On lines 621-622 we change the criteria for auto-linking, to support objects of type Solid3d. It would be nice to also filter specifically on the solid type, but unfortunately this information is currently not exposed through .NET (you can get it from C++, should you really need to... this is a technique we used in the Component Technologies demo we've been presenting at our annual regional conferences). And in any case, you can get some really fun results if you select different types of solids... :-)
And finally, we have the code to get the centre and radius of spheres (lines 772-787 and line 794). We simply use the centroid of the solid for the centre (which is a fair guess for solids, but may be slightly off for pyramids, for instance), and we retrieve the radius by working backwards from the surface area, which for sphere is 4 x PI x r^2. Again - this isn't quite going to right for non-spherical solids, so be prepared.
OK, that's it... 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 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 or sphere to link: "
523 );
524 opts.AllowNone = true;
525 opts.SetRejectMessage(
526 "\nOnly circles or 3D solids can be selected."
527 );
528 opts.AddAllowedClass(typeof(Circle), false);
529 opts.AddAllowedClass(typeof(Solid3d), false);
530
531 PromptEntityResult res = ed.GetEntity(opts);
532 if (res.Status == PromptStatus.OK)
533 {
534 ObjectId from = res.ObjectId;
535 opts.Message =
536 "\nSelect second circle or sphere to link: ";
537 res = ed.GetEntity(opts);
538 if (res.Status == PromptStatus.OK)
539 {
540 ObjectId to = res.ObjectId;
541 m_linkManager.LinkObjects(from, to);
542 m_lastEntity = to;
543 m_entitiesToUpdate.Add(from);
544 }
545 }
546 }
547
548 // Define "AUTOLINK" command
549 [CommandMethod("AUTOLINK")]
550 public void ToggleAutoLink()
551 {
552 Document doc =
553 Application.DocumentManager.MdiActiveDocument;
554 Editor ed = doc.Editor;
555 m_autoLink = !m_autoLink;
556 if (m_autoLink)
557 {
558 ed.WriteMessage("\nAutomatic linking turned on.");
559 }
560 else
561 {
562 ed.WriteMessage("\nAutomatic linking turned off.");
563 }
564 }
565
566 // Define "LOADLINKS" command
567 [CommandMethod("LOADLINKS")]
568 public void LoadLinkSettings()
569 {
570 Document doc =
571 Application.DocumentManager.MdiActiveDocument;
572 Database db = doc.Database;
573 m_linkManager.LoadFromDatabase(db);
574 }
575
576 // Define "SAVELINKS" command
577 [CommandMethod("SAVELINKS")]
578 public void SaveLinkSettings()
579 {
580 Document doc =
581 Application.DocumentManager.MdiActiveDocument;
582 Database db = doc.Database;
583 m_linkManager.SaveToDatabase(db);
584 }
585
586 // Define callback for Database.ObjectModified event
587 private void OnObjectModified(
588 object sender, ObjectEventArgs e)
589 {
590 ObjectId id = e.DBObject.ObjectId;
591 if (m_linkManager.Contains(id) &&
592 !m_entitiesToUpdate.Contains(id))
593 {
594 m_entitiesToUpdate.Add(id);
595 }
596 }
597
598 // Define callback for Database.ObjectErased event
599 private void OnObjectErased(
600 object sender, ObjectErasedEventArgs e)
601 {
602 if (e.Erased)
603 {
604 ObjectId id = e.DBObject.ObjectId;
605 m_linkManager.RemoveLinks(id);
606 if (m_lastEntity == id)
607 {
608 m_lastEntity = ObjectId.Null;
609 }
610 }
611 }
612
613 // Define callback for Database.ObjectAppended event
614 void OnObjectAppended(object sender, ObjectEventArgs e)
615 {
616 Database db = sender as Database;
617 if (db != null)
618 {
619 if (m_autoLink)
620 {
621 if (e.DBObject.GetType() == typeof(Circle) ||
622 e.DBObject.GetType() == typeof(Solid3d))
623 {
624 ObjectId from = e.DBObject.ObjectId;
625 if (m_lastEntity == ObjectId.Null)
626 {
627 m_lastEntity = from;
628 }
629 else
630 {
631 m_linkManager.LinkObjects(from, m_lastEntity);
632 m_lastEntity = from;
633 m_entitiesToUpdate.Add(from);
634 }
635 }
636 }
637 }
638 }
639
640 // Define callback for Database.BeginSave event
641 void OnBeginSave(object sender, DatabaseIOEventArgs e)
642 {
643 Database db = sender as Database;
644 if (db != null)
645 {
646 m_linkManager.SaveToDatabase(db);
647 }
648 }
649
650 // Define callback for Document.CommandEnded event
651 private void OnCommandEnded(
652 object sender, CommandEventArgs e)
653 {
654 foreach (ObjectId id in m_entitiesToUpdate)
655 {
656 UpdateLinkedEntities(id);
657 }
658 m_entitiesToUpdate.Clear();
659 }
660
661 // Helper function for OnCommandEnded
662 private void UpdateLinkedEntities(ObjectId from)
663 {
664 Document doc =
665 Application.DocumentManager.MdiActiveDocument;
666 Editor ed = doc.Editor;
667 Database db = doc.Database;
668
669 ObjectIdCollection linked =
670 m_linkManager.GetLinkedObjects(from);
671
672 Transaction tr =
673 db.TransactionManager.StartTransaction();
674 using (tr)
675 {
676 try
677 {
678 Point3d firstCenter;
679 Point3d secondCenter;
680 double firstRadius;
681 double secondRadius;
682
683 Entity ent =
684 (Entity)tr.GetObject(from, OpenMode.ForRead);
685
686 if (GetCenterAndRadius(
687 ent,
688 out firstCenter,
689 out firstRadius
690 )
691 )
692 {
693 foreach (ObjectId to in linked)
694 {
695 Entity ent2 =
696 (Entity)tr.GetObject(to, OpenMode.ForRead);
697 if (GetCenterAndRadius(
698 ent2,
699 out secondCenter,
700 out secondRadius
701 )
702 )
703 {
704 Vector3d vec = firstCenter - secondCenter;
705 if (!vec.IsZeroLength())
706 {
707 // Only move the linked circle if it's not
708 // already near enough
709 double apart =
710 vec.Length - (firstRadius + secondRadius);
711 if (apart < 0.0)
712 apart = -apart;
713
714 if (apart > 0.00001)
715 {
716 ent2.UpgradeOpen();
717 ent2.TransformBy(
718 Matrix3d.Displacement(
719 vec.GetNormal() * apart
720 )
721 );
722 }
723 }
724 }
725 }
726 }
727 }
728 catch (System.Exception ex)
729 {
730 Autodesk.AutoCAD.Runtime.Exception ex2 =
731 ex as Autodesk.AutoCAD.Runtime.Exception;
732 if (ex2 != null &&
733 ex2.ErrorStatus !=
734 ErrorStatus.WasOpenForUndo &&
735 ex2.ErrorStatus !=
736 ErrorStatus.WasErased
737 )
738 {
739 ed.WriteMessage(
740 "\nAutoCAD exception: {0}", ex2
741 );
742 }
743 else if (ex2 == null)
744 {
745 ed.WriteMessage(
746 "\nSystem exception: {0}", ex
747 );
748 }
749 }
750 tr.Commit();
751 }
752 }
753
754 // Helper function to get the center and radius
755 // for all supported circular objects
756 private bool GetCenterAndRadius(
757 Entity ent,
758 out Point3d center,
759 out double radius
760 )
761 {
762 // For circles it's easy...
763 Circle circle = ent as Circle;
764 if (circle != null)
765 {
766 center = circle.Center;
767 radius = circle.Radius;
768 return true;
769 }
770 else
771 {
772 // For solids (spheres) return the centroid
773 // and the radius, derived from the area
774 Solid3d solid = ent as Solid3d;
775 if (solid != null)
776 {
777 center =
778 solid.MassProperties.Centroid;
779 // Surface area of a sphere is 4 * pi * r^2
780 radius =
781 System.Math.Sqrt(
782 solid.Area / System.Math.PI
783 ) / 2;
784 return true;
785 }
786 else
787 {
788 // Throw in some empty values...
789 // Returning false indicates the object
790 // passed in was not useable
791 center = Point3d.Origin;
792 radius = 0.0;
793 return false;
794 }
795 }
796 }
797 }
798 }
Just to prove it all works, here are a couple of screenshots of some linked spheres with pretty materials attached. By the way, the links will also work when the movement is in full 3D - not just planar to the current UCS:
That's almost it for the "Linking Circles" series, unless someone makes some interesting suggestions in the comments. I have an old LISP file I used to demo the original sample: it simply moves the "head of the snake" through a sine wave a number of times across the screen (just using the MOVE command - nothing very fancy). I'll post the code next time, in case anyone's interested.