In the original post in this series, we introduced a basic application to number AutoCAD objects, specifically blocks with attributes. In the second post we extended this to make use of a generic numbering system for drawing-resident AutoCAD objects, and in the third post we implemented additional commands to take advantage of this new "kernel".
In this post we're going to extend the application in a few ways: firstly we're going to support duplicates, so that the LNS command which parses the current drawing to understand its numbers will support automatic and semi-automatic renumbering of objects with duplicate numbers. In addition there are a number of new event handlers that have been introduced to automatically renumber objects on creation/insertion/copy, and also to clear the numbering system when a user undoes any action in the drawing (just to be safe :-).
While introducing these event handlers I decide to switch the approach for associating data with a drawing: rather than declaring the variables at a class level and assuming they would be duplicated instantiated appropriately per-document, as shown in this previous post, I decided to encapsulate the variables in a class and specifically instantiate that class and store it per-document, as shown in this previous post.
Here's the updated C# code, with the changed & new lines in red, and here is the complete source file to save you having to strip line numbers:
1 using Autodesk.AutoCAD.ApplicationServices;
2 using Autodesk.AutoCAD.Runtime;
3 using Autodesk.AutoCAD.DatabaseServices;
4 using Autodesk.AutoCAD.EditorInput;
5 using Autodesk.AutoCAD.Geometry;
6 using System.Collections.Generic;
7 using System.Collections;
8
9 namespace AutoNumberedBubbles
10 {
11 public class Commands : IExtensionApplication
12 {
13 // Strings identifying the block
14 // and the attribute name to use
15
16 const string blockName = "BUBBLE";
17 const string attbName = "NUMBER";
18
19 // A string to identify our application's
20 // data in per-document UserData
21
22 const string dataKey = "TTIFBubbles";
23
24 // Define a class for our custom data
25
26 public class BubbleData
27 {
28 // A separate object to manage our numbering
29
30 private NumberedObjectManager m_nom;
31 public NumberedObjectManager Nom
32 {
33 get { return m_nom; }
34 }
35
36 // A "base" index (for the start of the list)
37
38 private int m_baseNumber;
39 public int BaseNumber
40 {
41 get { return m_baseNumber; }
42 set { m_baseNumber = value; }
43 }
44
45 // A list of blocks added to the database
46 // which we will then renumber
47
48 private List<ObjectId> m_blocksAdded;
49 public List<ObjectId> BlocksToRenumber
50 {
51 get { return m_blocksAdded; }
52 }
53
54 // Constructor
55
56 public BubbleData()
57 {
58 m_baseNumber = 0;
59 m_nom = new NumberedObjectManager();
60 m_blocksAdded = new List<ObjectId>();
61 }
62
63 // Method to clear the contents
64
65 public void Reset()
66 {
67 m_baseNumber = 0;
68 m_nom.Clear();
69 m_blocksAdded.Clear();
70 }
71 }
72
73 // Constructor
74
75 public Commands()
76 {
77 }
78
79 // Functions called on initialization & termination
80
81 public void Initialize()
82 {
83 try
84 {
85 DocumentCollection dm =
86 Application.DocumentManager;
87 Document doc = dm.MdiActiveDocument;
88 Database db = doc.Database;
89 Editor ed = doc.Editor;
90
91 ed.WriteMessage(
92 "\nLNS Load numbering settings by analyzing the current drawing" +
93 "\nDMP Print internal numbering information" +
94 "\nBAP Create bubbles at points" +
95 "\nBIC Create bubbles at the center of circles" +
96 "\nMB Move a bubble in the list" +
97 "\nDB Delete a bubble" +
98 "\nRBS Reorder the bubbles, to close gaps caused by deletion" +
99 "\nHLB Highlight a particular bubble"
100 );
101
102 // Hook into some events, to detect and renumber
103 // blocks added to the database
104
105 db.ObjectAppended +=
106 new ObjectEventHandler(
107 db_ObjectAppended
108 );
109 dm.DocumentCreated +=
110 new DocumentCollectionEventHandler(
111 dm_DocumentCreated
112 );
113 dm.DocumentLockModeWillChange +=
114 new DocumentLockModeWillChangeEventHandler(
115 dm_DocumentLockModeWillChange
116 );
117
118 doc.CommandEnded +=
119 delegate(object sender, CommandEventArgs e)
120 {
121 if (e.GlobalCommandName == "UNDO" ||
122 e.GlobalCommandName == "U")
123 {
124 ed.WriteMessage(
125 "\nUndo invalidates bubble numbering: call" +
126 " LNS to reload the numbers for this drawing"
127 );
128 GetBubbleData((Document)sender).Reset();
129 }
130 };
131 }
132 catch
133 { }
134 }
135
136 public void Terminate()
137 {
138 }
139
140 // Method to retrieve (or create) the
141 // BubbleData object for a particular
142 // document
143
144 private BubbleData GetBubbleData(Document doc)
145 {
146 Hashtable ud = doc.UserData;
147 BubbleData bd =
148 ud[dataKey] as BubbleData;
149
150 if (bd == null)
151 {
152 object obj = ud[dataKey];
153 if (obj == null)
154 {
155 // Nothing there
156
157 bd = new BubbleData();
158 ud.Add(dataKey, bd);
159 }
160 else
161 {
162 // Found something different instead
163
164 Editor ed = doc.Editor;
165 ed.WriteMessage(
166 "Found an object of type \"" +
167 obj.GetType().ToString() +
168 "\" instead of BubbleData.");
169 }
170 }
171 return bd;
172 }
173
174 // Do the same for a particular database
175
176 private BubbleData GetBubbleData(Database db)
177 {
178 DocumentCollection dm =
179 Application.DocumentManager;
180 Document doc =
181 dm.GetDocument(db);
182 return GetBubbleData(doc);
183 }
184
185 // When a new document is created, attach our
186 // ObjectAppended event handler to the new
187 // database
188
189 void dm_DocumentCreated(
190 object sender,
191 DocumentCollectionEventArgs e
192 )
193 {
194 e.Document.Database.ObjectAppended +=
195 new ObjectEventHandler(
196 db_ObjectAppended
197 );
198 }
199
200 // When an object is appended to a database,
201 // add it to a list we care about if it's a
202 // BlockReference
203
204 void db_ObjectAppended(
205 object sender,
206 ObjectEventArgs e
207 )
208 {
209 BlockReference br =
210 e.DBObject as BlockReference;
211 if (br != null)
212 {
213 BubbleData bd =
214 GetBubbleData(e.DBObject.Database);
215 bd.BlocksToRenumber.Add(br.ObjectId);
216 }
217 }
218
219 // When the command (or action) is over,
220 // take the list of blocks to renumber and
221 // go through them, renumbering each one
222
223 void dm_DocumentLockModeWillChange(
224 object sender,
225 DocumentLockModeWillChangeEventArgs e
226 )
227 {
228 Document doc = e.Document;
229 BubbleData bd =
230 GetBubbleData(doc);
231
232 if (bd.BlocksToRenumber.Count > 0)
233 {
234 Database db = doc.Database;
235 Transaction tr =
236 db.TransactionManager.StartTransaction();
237 using (tr)
238 {
239 foreach (ObjectId bid in bd.BlocksToRenumber)
240 {
241 try
242 {
243 BlockReference br =
244 tr.GetObject(bid, OpenMode.ForRead)
245 as BlockReference;
246 if (br != null)
247 {
248 BlockTableRecord btr =
249 (BlockTableRecord)tr.GetObject(
250 br.BlockTableRecord,
251 OpenMode.ForRead
252 );
253 if (btr.Name == blockName)
254 {
255 AttributeCollection ac =
256 br.AttributeCollection;
257
258 foreach (ObjectId aid in ac)
259 {
260 DBObject obj =
261 tr.GetObject(aid, OpenMode.ForRead);
262 AttributeReference ar =
263 obj as AttributeReference;
264
265 if (ar.Tag == attbName)
266 {
267 // Change the one we care about
268
269 ar.UpgradeOpen();
270
271 int bubbleNumber =
272 bd.BaseNumber +
273 bd.Nom.NextObjectNumber(bid);
274 ar.TextString =
275 bubbleNumber.ToString();
276
277 break;
278 }
279 }
280 }
281 }
282 }
283 catch { }
284 }
285 tr.Commit();
286 bd.BlocksToRenumber.Clear();
287 }
288 }
289 }
290
291 // Command to extract and display information
292 // about the internal numbering
293
294 [CommandMethod("DMP")]
295 public void DumpNumberingInformation()
296 {
297 Document doc =
298 Application.DocumentManager.MdiActiveDocument;
299 Editor ed = doc.Editor;
300 BubbleData bd =
301 GetBubbleData(doc);
302 bd.Nom.DumpInfo(ed);
303 }
304
305 // Command to analyze the current document and
306 // understand which indeces have been used and
307 // which are currently free
308
309 [CommandMethod("LNS")]
310 public void LoadNumberingSettings()
311 {
312 Document doc =
313 Application.DocumentManager.MdiActiveDocument;
314 Database db = doc.Database;
315 Editor ed = doc.Editor;
316 BubbleData bd =
317 GetBubbleData(doc);
318
319 // We need to clear any internal state
320 // already collected
321
322 bd.Reset();
323
324 // Select all the blocks in the current drawing
325
326 TypedValue[] tvs =
327 new TypedValue[1] {
328 new TypedValue(
329 (int)DxfCode.Start,
330 "INSERT"
331 )
332 };
333 SelectionFilter sf =
334 new SelectionFilter(tvs);
335
336 PromptSelectionResult psr =
337 ed.SelectAll(sf);
338
339 // If it succeeded and we have some blocks...
340
341 if (psr.Status == PromptStatus.OK &&
342 psr.Value.Count > 0)
343 {
344 Transaction tr =
345 db.TransactionManager.StartTransaction();
346 using (tr)
347 {
348 // First get the modelspace and the ID
349 // of the block for which we're searching
350
351 BlockTableRecord ms;
352 ObjectId blockId;
353
354 if (GetBlock(
355 db, tr, out ms, out blockId
356 ))
357 {
358 // For each block reference in the drawing...
359
360 foreach (SelectedObject o in psr.Value)
361 {
362 DBObject obj =
363 tr.GetObject(o.ObjectId, OpenMode.ForRead);
364 BlockReference br = obj as BlockReference;
365 if (br != null)
366 {
367 // If it's the one we care about...
368
369 if (br.BlockTableRecord == blockId)
370 {
371 // Check its attribute references...
372
373 int pos = -1;
374 AttributeCollection ac =
375 br.AttributeCollection;
376
377 foreach (ObjectId id in ac)
378 {
379 DBObject obj2 =
380 tr.GetObject(id, OpenMode.ForRead);
381 AttributeReference ar =
382 obj2 as AttributeReference;
383
384 // When we find the attribute
385 // we care about...
386
387 if (ar.Tag == attbName)
388 {
389 try
390 {
391 // Attempt to extract the number from
392 // the text string property... use a
393 // try-catch block just in case it is
394 // non-numeric
395
396 pos =
397 int.Parse(ar.TextString);
398
399 // Add the object at the appropriate
400 // index
401
402 bd.Nom.NumberObject(
403 o.ObjectId, pos, false, true
404 );
405 }
406 catch { }
407 }
408 }
409 }
410 }
411 }
412 }
413 tr.Commit();
414 }
415
416 // Once we have analyzed all the block references...
417
418 int start = bd.Nom.GetLowerBound(true);
419
420 // If the first index is non-zero, ask the user if
421 // they want to rebase the list to begin at the
422 // current start position
423
424 if (start > 0)
425 {
426 ed.WriteMessage(
427 "\nLowest index is {0}. ",
428 start
429 );
430 PromptKeywordOptions pko =
431 new PromptKeywordOptions(
432 "Make this the start of the list?"
433 );
434 pko.AllowNone = true;
435 pko.Keywords.Add("Yes");
436 pko.Keywords.Add("No");
437 pko.Keywords.Default = "Yes";
438
439 PromptResult pkr =
440 ed.GetKeywords(pko);
441
442 if (pkr.Status != PromptStatus.OK)
443 bd.Reset();
444 else
445 {
446 if (pkr.StringResult == "Yes")
447 {
448 // We store our own base number
449 // (the object used to manage objects
450 // always uses zero-based indeces)
451
452 bd.BaseNumber = start;
453 bd.Nom.RebaseList(bd.BaseNumber);
454 }
455 }
456 }
457
458 // We found duplicates in the numbering...
459
460 if (bd.Nom.HasDuplicates())
461 {
462 // Ask how to fix the duplicates
463
464 PromptKeywordOptions pko =
465 new PromptKeywordOptions(
466 "Blocks contain duplicate numbers. " +
467 "How do you want to renumber?"
468 );
469 pko.AllowNone = true;
470 pko.Keywords.Add("Automatically");
471 pko.Keywords.Add("Individually");
472 pko.Keywords.Add("Not");
473 pko.Keywords.Default = "Automatically";
474
475 PromptResult pkr =
476 ed.GetKeywords(pko);
477
478 bool bAuto = false;
479 bool bManual = false;
480
481 if (pkr.Status != PromptStatus.OK)
482 bd.Reset();
483 else
484 {
485 if (pkr.StringResult == "Automatically")
486 bAuto = true;
487 else if (pkr.StringResult == "Individually")
488 bManual = true;
489
490 // Whether fixing automatically or manually
491 // we will iterate through the duplicate list
492
493 if (bAuto || bManual)
494 {
495 ObjectIdCollection idc =
496 new ObjectIdCollection();
497
498 // Get each entry in the duplicate list
499
500 SortedDictionary<int,List<ObjectId>> dups =
501 bd.Nom.Duplicates;
502 foreach (
503 KeyValuePair<int,List<ObjectId>> dup in dups
504 )
505 {
506 // The position is the key in the entry
507 // and the list of IDs is the value
508 // (we take a copy, so we can modify it
509 // without affecting the original)
510
511 int pos = dup.Key;
512 List<ObjectId> ids =
513 new List<ObjectId>(dup.Value);
514
515 // For automatic renumbering there's no
516 // user interaction
517
518 if (bAuto)
519 {
520 foreach (ObjectId id in ids)
521 {
522 bd.Nom.NextObjectNumber(id);
523 idc.Add(id);
524 }
525 }
526 else // bManual
527 {
528 // For manual renumbering we ask the user
529 // to select the block to keep, then
530 // we renumber the rest automatically
531
532 ed.UpdateScreen();
533
534 ids.Add(bd.Nom.GetObjectId(pos));
535 HighlightBubbles(db, ids, true);
536
537 ed.WriteMessage(
538 "\n\nHighlighted blocks " +
539 "with number {0}. ",
540 pos + bd.BaseNumber
541 );
542
543 bool finished = false;
544 while (!finished)
545 {
546 PromptEntityOptions peo =
547 new PromptEntityOptions(
548 "Select block to keep (others " +
549 "will be renumbered automatically): "
550 );
551 peo.SetRejectMessage(
552 "\nEntity must be a block."
553 );
554 peo.AddAllowedClass(
555 typeof(BlockReference), false);
556 PromptEntityResult per =
557 ed.GetEntity(peo);
558
559 if (per.Status != PromptStatus.OK)
560 {
561 bd.Reset();
562 return;
563 }
564 else
565 {
566 // A block has been selected, so we
567 // make sure it is one of the ones
568 // we highlighted for the user
569
570 if (ids.Contains(per.ObjectId))
571 {
572 // Leave the selected block alone
573 // by removing it from the list
574
575 ids.Remove(per.ObjectId);
576
577 // We then renumber each block in
578 // the list
579
580 foreach (ObjectId id in ids)
581 {
582 bd.Nom.NextObjectNumber(id);
583 idc.Add(id);
584 }
585 RenumberBubbles(db, idc);
586 idc.Clear();
587
588 // Let's unhighlight our selected
589 // block (renumbering will do this
590 // for the others)
591
592 List<ObjectId> redraw =
593 new List<ObjectId>(1);
594 redraw.Add(per.ObjectId);
595 HighlightBubbles(db, redraw, false);
596
597 finished = true;
598 }
599 else
600 {
601 ed.WriteMessage(
602 "\nBlock selected is not " +
603 "numbered with {0}. ",
604 pos + bd.BaseNumber
605 );
606 }
607 }
608 }
609 }
610 }
611 RenumberBubbles(db, idc);
612 }
613 bd.Nom.Duplicates.Clear();
614 ed.UpdateScreen();
615 }
616 }
617 }
618 }
619
620 // Take a list of objects and either highlight
621 // or unhighlight them, depending on the flag
622
623 private void HighlightBubbles(
624 Database db, List<ObjectId> ids, bool highlight)
625 {
626 Transaction tr =
627 db.TransactionManager.StartTransaction();
628 using (tr)
629 {
630 foreach (ObjectId id in ids)
631 {
632 Entity ent =
633 (Entity)tr.GetObject(
634 id,
635 OpenMode.ForRead
636 );
637 if (highlight)
638 ent.Highlight();
639 else
640 ent.Draw();
641 }
642 tr.Commit();
643 }
644 }
645
646 // Command to create bubbles at points selected
647 // by the user - loops until cancelled
648
649 [CommandMethod("BAP")]
650 public void BubblesAtPoints()
651 {
652 Document doc =
653 Application.DocumentManager.MdiActiveDocument;
654 Database db = doc.Database;
655 Editor ed = doc.Editor;
656 Autodesk.AutoCAD.ApplicationServices.
657 TransactionManager tm =
658 doc.TransactionManager;
659
660 Transaction tr =
661 tm.StartTransaction();
662 using (tr)
663 {
664 // Get the information about the block
665 // and attribute definitions we care about
666
667 BlockTableRecord ms;
668 ObjectId blockId;
669 AttributeDefinition ad;
670 List<AttributeDefinition> other;
671
672 if (GetBlock(
673 db, tr, out ms, out blockId
674 ))
675 {
676 GetBlockAttributes(
677 tr, blockId, out ad, out other
678 );
679
680 // By default the modelspace is returned to
681 // us in read-only state
682
683 ms.UpgradeOpen();
684
685 // Loop until cancelled
686
687 bool finished = false;
688 while (!finished)
689 {
690 PromptPointOptions ppo =
691 new PromptPointOptions("\nSelect point: ");
692 ppo.AllowNone = true;
693
694 PromptPointResult ppr =
695 ed.GetPoint(ppo);
696 if (ppr.Status != PromptStatus.OK)
697 finished = true;
698 else
699 // Call a function to create our bubble
700 CreateNumberedBubbleAtPoint(
701 db, ms, tr, ppr.Value,
702 blockId, ad, other
703 );
704 tm.QueueForGraphicsFlush();
705 tm.FlushGraphics();
706 }
707 }
708 tr.Commit();
709 }
710 }
711
712 // Command to create a bubble at the center of
713 // each of the selected circles
714
715 [CommandMethod("BIC")]
716 public void BubblesInCircles()
717 {
718 Document doc =
719 Application.DocumentManager.MdiActiveDocument;
720 Database db = doc.Database;
721 Editor ed = doc.Editor;
722
723 // Allow the user to select circles
724
725 TypedValue[] tvs =
726 new TypedValue[1] {
727 new TypedValue(
728 (int)DxfCode.Start,
729 "CIRCLE"
730 )
731 };
732 SelectionFilter sf =
733 new SelectionFilter(tvs);
734
735 PromptSelectionResult psr =
736 ed.GetSelection(sf);
737
738 if (psr.Status == PromptStatus.OK &&
739 psr.Value.Count > 0)
740 {
741 Transaction tr =
742 db.TransactionManager.StartTransaction();
743 using (tr)
744 {
745 // Get the information about the block
746 // and attribute definitions we care about
747
748 BlockTableRecord ms;
749 ObjectId blockId;
750 AttributeDefinition ad;
751 List<AttributeDefinition> other;
752
753 if (GetBlock(
754 db, tr, out ms, out blockId
755 ))
756 {
757 GetBlockAttributes(
758 tr, blockId, out ad, out other
759 );
760
761 // By default the modelspace is returned to
762 // us in read-only state
763
764 ms.UpgradeOpen();
765
766 foreach (SelectedObject o in psr.Value)
767 {
768 // For each circle in the selected list...
769
770 DBObject obj =
771 tr.GetObject(o.ObjectId, OpenMode.ForRead);
772 Circle c = obj as Circle;
773 if (c == null)
774 ed.WriteMessage(
775 "\nObject selected is not a circle."
776 );
777 else
778 // Call our numbering function, passing the
779 // center of the circle
780 CreateNumberedBubbleAtPoint(
781 db, ms, tr, c.Center,
782 blockId, ad, other
783 );
784 }
785 }
786 tr.Commit();
787 }
788 }
789 }
790
791 // Command to delete a particular bubble
792 // selected by its index
793
794 [CommandMethod("MB")]
795 public void MoveBubble()
796 {
797 Document doc =
798 Application.DocumentManager.MdiActiveDocument;
799 Editor ed = doc.Editor;
800 BubbleData bd =
801 GetBubbleData(doc);
802
803 // Use a helper function to select a valid bubble index
804
805 int pos =
806 GetBubbleNumber(
807 ed, bd,
808 "\nEnter number of bubble to move: "
809 );
810
811 if (pos >= bd.BaseNumber)
812 {
813 int from = pos - bd.BaseNumber;
814
815 pos =
816 GetBubbleNumber(
817 ed, bd,
818 "\nEnter destination position: "
819 );
820
821 if (pos >= bd.BaseNumber)
822 {
823 int to = pos - bd.BaseNumber;
824
825 ObjectIdCollection ids =
826 bd.Nom.MoveObject(from, to);
827
828 RenumberBubbles(doc.Database, ids);
829 }
830 }
831 }
832
833 // Command to delete a particular bubbler,
834 // selected by its index
835
836 [CommandMethod("DB")]
837 public void DeleteBubble()
838 {
839 Document doc =
840 Application.DocumentManager.MdiActiveDocument;
841 Database db = doc.Database;
842 Editor ed = doc.Editor;
843 BubbleData bd =
844 GetBubbleData(doc);
845
846 // Use a helper function to select a valid bubble index
847
848 int pos =
849 GetBubbleNumber(
850 ed, bd,
851 "\nEnter number of bubble to erase: "
852 );
853
854 if (pos >= bd.BaseNumber)
855 {
856 // Remove the object from the internal list
857 // (this returns the ObjectId stored for it,
858 // which we can then use to erase the entity)
859
860 ObjectId id =
861 bd.Nom.RemoveObject(pos - bd.BaseNumber);
862 Transaction tr =
863 db.TransactionManager.StartTransaction();
864 using (tr)
865 {
866 DBObject obj =
867 tr.GetObject(id, OpenMode.ForWrite);
868 obj.Erase();
869 tr.Commit();
870 }
871 }
872 }
873
874 // Command to reorder all the bubbles in the drawing,
875 // closing all the gaps between numbers but maintaining
876 // the current numbering order
877
878 [CommandMethod("RBS")]
879 public void ReorderBubbles()
880 {
881 Document doc =
882 Application.DocumentManager.MdiActiveDocument;
883 BubbleData bd =
884 GetBubbleData(doc);
885
886 // Re-order the bubbles - the IDs returned are
887 // of the objects that need to be renumbered
888
889 ObjectIdCollection ids =
890 bd.Nom.ReorderObjects();
891
892 RenumberBubbles(doc.Database, ids);
893 }
894
895 // Command to highlight a particular bubble
896
897 [CommandMethod("HLB")]
898 public void HighlightBubble()
899 {
900 Document doc =
901 Application.DocumentManager.MdiActiveDocument;
902 Database db = doc.Database;
903 Editor ed = doc.Editor;
904 BubbleData bd =
905 GetBubbleData(doc);
906
907 // Use our function to select a valid bubble index
908
909 int pos =
910 GetBubbleNumber(
911 ed, bd,
912 "\nEnter number of bubble to highlight: "
913 );
914
915 if (pos >= bd.BaseNumber)
916 {
917 ObjectId id =
918 bd.Nom.GetObjectId(pos - bd.BaseNumber);
919
920 if (id == ObjectId.Null)
921 {
922 ed.WriteMessage(
923 "\nNumber is not currently used -" +
924 " nothing to highlight."
925 );
926 return;
927 }
928 List<ObjectId> ids =
929 new List<ObjectId>(1);
930 ids.Add(id);
931 HighlightBubbles(db, ids, true);
932 }
933 }
934
935 // Internal helper function to open and retrieve
936 // the model-space and the block def we care about
937
938 private bool
939 GetBlock(
940 Database db,
941 Transaction tr,
942 out BlockTableRecord ms,
943 out ObjectId blockId
944 )
945 {
946 BlockTable bt =
947 (BlockTable)tr.GetObject(
948 db.BlockTableId,
949 OpenMode.ForRead
950 );
951
952 if (!bt.Has(blockName))
953 {
954 Document doc =
955 Application.DocumentManager.MdiActiveDocument;
956 Editor ed = doc.Editor;
957 ed.WriteMessage(
958 "\nCannot find block definition \"" +
959 blockName +
960 "\" in the current drawing."
961 );
962
963 blockId = ObjectId.Null;
964 ms = null;
965 return false;
966 }
967
968 ms =
969 (BlockTableRecord)tr.GetObject(
970 bt[BlockTableRecord.ModelSpace],
971 OpenMode.ForRead
972 );
973
974 blockId = bt[blockName];
975
976 return true;
977 }
978
979 // Internal helper function to retrieve
980 // attribute info from our block
981 // (we return the main attribute def
982 // and then all the "others")
983
984 private void
985 GetBlockAttributes(
986 Transaction tr,
987 ObjectId blockId,
988 out AttributeDefinition ad,
989 out List<AttributeDefinition> other
990 )
991 {
992 BlockTableRecord blk =
993 (BlockTableRecord)tr.GetObject(
994 blockId,
995 OpenMode.ForRead
996 );
997
998 ad = null;
999 other =
1000 new List<AttributeDefinition>();
1001
1002 foreach (ObjectId attId in blk)
1003 {
1004 DBObject obj =
1005 (DBObject)tr.GetObject(
1006 attId,
1007 OpenMode.ForRead
1008 );
1009 AttributeDefinition ad2 =
1010 obj as AttributeDefinition;
1011
1012 if (ad2 != null)
1013 {
1014 if (ad2.Tag == attbName)
1015 {
1016 if (ad2.Constant)
1017 {
1018 Document doc =
1019 Application.DocumentManager.MdiActiveDocument;
1020 Editor ed = doc.Editor;
1021
1022 ed.WriteMessage(
1023 "\nAttribute to change is constant!"
1024 );
1025 }
1026 else
1027 ad = ad2;
1028 }
1029 else
1030 if (!ad2.Constant)
1031 other.Add(ad2);
1032 }
1033 }
1034 }
1035
1036 // Internal helper function to create a bubble
1037 // at a particular point
1038
1039 private Entity
1040 CreateNumberedBubbleAtPoint(
1041 Database db,
1042 BlockTableRecord btr,
1043 Transaction tr,
1044 Point3d pt,
1045 ObjectId blockId,
1046 AttributeDefinition ad,
1047 List<AttributeDefinition> other
1048 )
1049 {
1050 BubbleData bd =
1051 GetBubbleData(db);
1052
1053 // Create a new block reference
1054
1055 BlockReference br =
1056 new BlockReference(pt, blockId);
1057
1058 // Add it to the database
1059
1060 br.SetDatabaseDefaults();
1061 ObjectId blockRefId = btr.AppendEntity(br);
1062 tr.AddNewlyCreatedDBObject(br, true);
1063
1064 // Create an attribute reference for our main
1065 // attribute definition (where we'll put the
1066 // bubble's number)
1067
1068 AttributeReference ar =
1069 new AttributeReference();
1070
1071 // Add it to the database, and set its position, etc.
1072
1073 ar.SetDatabaseDefaults();
1074 ar.SetAttributeFromBlock(ad, br.BlockTransform);
1075 ar.Position =
1076 ad.Position.TransformBy(br.BlockTransform);
1077 ar.Tag = ad.Tag;
1078
1079 // Set the bubble's number
1080
1081 int bubbleNumber =
1082 bd.BaseNumber +
1083 bd.Nom.NextObjectNumber(blockRefId);
1084
1085 ar.TextString = bubbleNumber.ToString();
1086 ar.AdjustAlignment(db);
1087
1088 // Add the attribute to the block reference
1089
1090 br.AttributeCollection.AppendAttribute(ar);
1091 tr.AddNewlyCreatedDBObject(ar, true);
1092
1093 // Now we add attribute references for the
1094 // other attribute definitions
1095
1096 foreach (AttributeDefinition ad2 in other)
1097 {
1098 AttributeReference ar2 =
1099 new AttributeReference();
1100
1101 ar2.SetAttributeFromBlock(ad2, br.BlockTransform);
1102 ar2.Position =
1103 ad2.Position.TransformBy(br.BlockTransform);
1104 ar2.Tag = ad2.Tag;
1105 ar2.TextString = ad2.TextString;
1106 ar2.AdjustAlignment(db);
1107
1108 br.AttributeCollection.AppendAttribute(ar2);
1109 tr.AddNewlyCreatedDBObject(ar2, true);
1110 }
1111 return br;
1112 }
1113
1114 // Internal helper function to have the user
1115 // select a valid bubble index
1116
1117 private int
1118 GetBubbleNumber(
1119 Editor ed,
1120 BubbleData bd,
1121 string prompt
1122 )
1123 {
1124 int upper = bd.Nom.GetUpperBound();
1125 if (upper <= 0)
1126 {
1127 ed.WriteMessage(
1128 "\nNo bubbles are currently being managed."
1129 );
1130 return upper;
1131 }
1132
1133 PromptIntegerOptions pio =
1134 new PromptIntegerOptions(prompt);
1135 pio.AllowNone = false;
1136
1137 // Get the limits from our manager object
1138
1139 pio.LowerLimit =
1140 bd.BaseNumber +
1141 bd.Nom.GetLowerBound(false);
1142 pio.UpperLimit =
1143 bd.BaseNumber +
1144 upper;
1145
1146 PromptIntegerResult pir =
1147 ed.GetInteger(pio);
1148 if (pir.Status == PromptStatus.OK)
1149 return pir.Value;
1150 else
1151 return -1;
1152 }
1153
1154 // Internal helper function to open up a list
1155 // of bubbles and reset their number to the
1156 // position in the list
1157
1158 private void RenumberBubbles(
1159 Database db, ObjectIdCollection ids)
1160 {
1161 BubbleData bd =
1162 GetBubbleData(db);
1163 Transaction tr =
1164 db.TransactionManager.StartTransaction();
1165 using (tr)
1166 {
1167 // Get the block information
1168
1169 BlockTableRecord ms;
1170 ObjectId blockId;
1171
1172 if (GetBlock(
1173 db, tr, out ms, out blockId
1174 ))
1175 {
1176 // Open each bubble to be renumbered
1177
1178 foreach (ObjectId bid in ids)
1179 {
1180 if (bid != ObjectId.Null)
1181 {
1182 DBObject obj =
1183 tr.GetObject(bid, OpenMode.ForRead);
1184 BlockReference br = obj as BlockReference;
1185 if (br != null)
1186 {
1187 if (br.BlockTableRecord == blockId)
1188 {
1189 AttributeCollection ac =
1190 br.AttributeCollection;
1191
1192 // Go through its attributes
1193
1194 foreach (ObjectId aid in ac)
1195 {
1196 DBObject obj2 =
1197 tr.GetObject(aid, OpenMode.ForRead);
1198 AttributeReference ar =
1199 obj2 as AttributeReference;
1200
1201 if (ar.Tag == attbName)
1202 {
1203 // Change the one we care about
1204
1205 ar.UpgradeOpen();
1206
1207 int bubbleNumber =
1208 bd.BaseNumber +
1209 bd.Nom.GetNumber(bid);
1210 ar.TextString =
1211 bubbleNumber.ToString();
1212
1213 break;
1214 }
1215 }
1216 }
1217 }
1218 }
1219 }
1220 }
1221 tr.Commit();
1222 }
1223 }
1224 }
1225
1226 // A generic class for managing groups of
1227 // numbered (and ordered) objects
1228
1229 public class NumberedObjectManager
1230 {
1231 // Store the IDs of the objects we're managing
1232
1233 private List<ObjectId> m_ids;
1234
1235 // A list of free positions in the above list
1236 // (allows numbering gaps)
1237
1238 private List<int> m_free;
1239
1240 // A map of duplicates - blocks detected with
1241 // the number of an existing block
1242
1243 private SortedDictionary<int,List<ObjectId>> m_dups;
1244 public SortedDictionary<int, List<ObjectId>> Duplicates
1245 {
1246 get { return m_dups; }
1247 }
1248
1249 // Constructor
1250
1251 public NumberedObjectManager()
1252 {
1253 m_ids =
1254 new List<ObjectId>();
1255
1256 m_free =
1257 new List<int>();
1258
1259 m_dups =
1260 new SortedDictionary<int, List<ObjectId>>();
1261 }
1262
1263 // Clear the internal lists
1264
1265 public void Clear()
1266 {
1267 m_ids.Clear();
1268 m_free.Clear();
1269 m_dups.Clear();
1270 }
1271
1272 // Does the duplicate list contain anything?
1273
1274 public bool HasDuplicates()
1275 {
1276 return m_dups.Count > 0;
1277 }
1278
1279 // Return the first entry in the ObjectId list
1280 // (specify "true" if you want to skip
1281 // any null object IDs)
1282
1283 public int GetLowerBound(bool ignoreNull)
1284 {
1285 if (ignoreNull)
1286 // Define an in-line predicate to check
1287 // whether an ObjectId is null
1288 return
1289 m_ids.FindIndex(
1290 delegate(ObjectId id)
1291 {
1292 return id != ObjectId.Null;
1293 }
1294 );
1295 else
1296 return 0;
1297 }
1298
1299 // Return the last entry in the ObjectId list
1300
1301 public int GetUpperBound()
1302 {
1303 return m_ids.Count - 1;
1304 }
1305
1306 // Store the specified ObjectId in the next
1307 // available location in the list, and return
1308 // what that is
1309
1310 public int NextObjectNumber(ObjectId id)
1311 {
1312 int pos;
1313 if (m_free.Count > 0)
1314 {
1315 // Get the first free position, then remove
1316 // it from the "free" list
1317
1318 pos = m_free[0];
1319 m_free.RemoveAt(0);
1320 m_ids[pos] = id;
1321 }
1322 else
1323 {
1324 // There are no free slots (gaps in the numbering)
1325 // so we append it to the list
1326
1327 pos = m_ids.Count;
1328 m_ids.Add(id);
1329 }
1330 return pos;
1331 }
1332
1333 // Go through the list of objects and close any gaps
1334 // by shuffling the list down (easy, as we're using a
1335 // List<> rather than an array)
1336
1337 public ObjectIdCollection ReorderObjects()
1338 {
1339 // Create a collection of ObjectIds we'll return
1340 // for the caller to go and update
1341 // (so the renumbering will gets reflected
1342 // in the objects themselves)
1343
1344 ObjectIdCollection ids =
1345 new ObjectIdCollection();
1346
1347 // We'll go through the "free" list backwards,
1348 // to allow any changes made to the list of
1349 // objects to not affect what we're doing
1350
1351 List<int> rev =
1352 new List<int>(m_free);
1353 rev.Reverse();
1354
1355 foreach (int pos in rev)
1356 {
1357 // First we remove the object at the "free"
1358 // position (in theory this should be set to
1359 // ObjectId.Null, as the slot has been marked
1360 // as blank)
1361
1362 m_ids.RemoveAt(pos);
1363
1364 // Now we go through and add the IDs of any
1365 // affected objects to the list to return
1366
1367 for (int i = pos; i < m_ids.Count; i++)
1368 {
1369 ObjectId id = m_ids[pos];
1370
1371 // Only add non-null objects
1372 // not already in the list
1373
1374 if (!ids.Contains(id) &&
1375 id != ObjectId.Null)
1376 ids.Add(id);
1377 }
1378 }
1379
1380 // Our free slots have been filled, so clear
1381 // the list
1382
1383 m_free.Clear();
1384
1385 return ids;
1386 }
1387
1388 // Get the ID of an object at a particular position
1389
1390 public ObjectId GetObjectId(int pos)
1391 {
1392 if (pos < m_ids.Count)
1393 return m_ids[pos];
1394 else
1395 return ObjectId.Null;
1396 }
1397
1398 // Get the position of an ObjectId in the list
1399
1400 public int GetNumber(ObjectId id)
1401 {
1402 if (m_ids.Contains(id))
1403 return m_ids.IndexOf(id);
1404 else
1405 return -1;
1406 }
1407
1408 // Store an ObjectId in a particular position
1409 // (shuffle == true will "insert" it, shuffling
1410 // the remaining objects down,
1411 // shuffle == false will replace the item in
1412 // that slot)
1413
1414 public void NumberObject(
1415 ObjectId id, int index, bool shuffle, bool dups)
1416 {
1417 // If we're inserting into the list
1418
1419 if (index < m_ids.Count)
1420 {
1421 if (shuffle)
1422 // Insert takes care of the shuffling
1423 m_ids.Insert(index, id);
1424 else
1425 {
1426 // If we're replacing the existing item, do
1427 // so and then make sure the slot is removed
1428 // from the "free" list, if applicable
1429
1430 if (!dups ||
1431 m_ids[index] == ObjectId.Null)
1432 {
1433 m_ids[index] = id;
1434 if (m_free.Contains(index))
1435 m_free.Remove(index);
1436 }
1437 else
1438 {
1439 // If we're tracking duplicates, add our new
1440 // object to the duplicate list for that index
1441
1442 if (dups)
1443 {
1444 List<ObjectId> ids;
1445 if (m_dups.ContainsKey(index))
1446 {
1447 ids = m_dups[index];
1448 m_dups.Remove(index);
1449 }
1450 else
1451 ids = new List<ObjectId>();
1452
1453 ids.Add(id);
1454 m_dups.Add(index, ids);
1455 }
1456 }
1457 }
1458 }
1459 else
1460 {
1461 // If we're appending, shuffling is irrelevant,
1462 // but we may need to add additional "free" slots
1463 // if the position comes after the end
1464
1465 while (m_ids.Count < index)
1466 {
1467 m_ids.Add(ObjectId.Null);
1468 m_free.Add(m_ids.LastIndexOf(ObjectId.Null));
1469 m_free.Sort();
1470 }
1471 m_ids.Add(id);
1472 }
1473 }
1474
1475 // Move an ObjectId already in the list to a
1476 // particular position
1477 // (ObjectIds between the two positions will
1478 // get shuffled down automatically)
1479
1480 public ObjectIdCollection MoveObject(
1481 int from, int to)
1482 {
1483 ObjectIdCollection ids =
1484 new ObjectIdCollection();
1485
1486 if (from < m_ids.Count &&
1487 to < m_ids.Count)
1488 {
1489 if (from != to)
1490 {
1491 ObjectId id = m_ids[from];
1492 m_ids.RemoveAt(from);
1493 m_ids.Insert(to, id);
1494
1495 int start = (from < to ? from : to);
1496 int end = (from < to ? to : from);
1497
1498 for (int i = start; i <= end; i++)
1499 {
1500 ids.Add(m_ids[i]);
1501 }
1502 }
1503 // Now need to adjust/recreate "free" list
1504 m_free.Clear();
1505 for (int j = 0; j < m_ids.Count; j++)
1506 {
1507 if (m_ids[j] == ObjectId.Null)
1508 m_free.Add(j);
1509 }
1510 }
1511 return ids;
1512 }
1513
1514 // Remove an ObjectId from the list
1515
1516 public int RemoveObject(ObjectId id)
1517 {
1518 // Check it's non-null and in the list
1519
1520 if (id != ObjectId.Null &&
1521 m_ids.Contains(id))
1522 {
1523 int pos = m_ids.IndexOf(id);
1524 RemoveObject(pos);
1525 return pos;
1526 }
1527 return -1;
1528 }
1529
1530 // Remove the ObjectId at a particular position
1531
1532 public ObjectId RemoveObject(int pos)
1533 {
1534 // Get the ObjectId in the specified position,
1535 // making sure it's non-null
1536
1537 ObjectId id = m_ids[pos];
1538 if (id != ObjectId.Null)
1539 {
1540 // Null out the position and add it to the
1541 // "free" list
1542
1543 m_ids[pos] = ObjectId.Null;
1544 m_free.Add(pos);
1545 m_free.Sort();
1546 }
1547 return id;
1548 }
1549
1550 // Dump out the object list information
1551 // as well as the "free" slots
1552
1553 public void DumpInfo(Editor ed)
1554 {
1555 if (m_ids.Count > 0)
1556 {
1557 ed.WriteMessage("\nIdx ObjectId");
1558
1559 int index = 0;
1560 foreach (ObjectId id in m_ids)
1561 ed.WriteMessage("\n{0} {1}", index++, id);
1562 }
1563
1564 if (m_free.Count > 0)
1565 {
1566 ed.WriteMessage("\n\nFree list: ");
1567
1568 foreach (int pos in m_free)
1569 ed.WriteMessage("{0} ", pos);
1570 }
1571 if (HasDuplicates())
1572 {
1573 ed.WriteMessage("\n\nDuplicate list: ");
1574
1575 foreach (
1576 KeyValuePair<int, List<ObjectId>> dup
1577 in m_dups
1578 )
1579 {
1580 int pos = dup.Key;
1581 List<ObjectId> ids = dup.Value;
1582
1583 ed.WriteMessage("\n{0} ", pos);
1584
1585 foreach (ObjectId id in ids)
1586 {
1587 ed.WriteMessage("{0} ", id);
1588 }
1589 }
1590 }
1591 }
1592
1593 // Remove the initial n items from the list
1594
1595 public void RebaseList(int start)
1596 {
1597 // First we remove the ObjectIds
1598
1599 for (int i=0; i < start; i++)
1600 m_ids.RemoveAt(0);
1601
1602 // Then we go through the "free" list...
1603
1604 int idx = 0;
1605 while (idx < m_free.Count)
1606 {
1607 if (m_free[idx] < start)
1608 // Remove any that refer to the slots
1609 // we've removed
1610 m_free.RemoveAt(idx);
1611 else
1612 {
1613 // Subtracting the number of slots
1614 // we've removed from the other items
1615 m_free[idx] -= start;
1616 idx++;
1617 }
1618 }
1619
1620 // The duplicate list is more tricky, as we store
1621 // our list of objects against the index
1622 // (we need to remove and re-add each entry)
1623
1624 if (HasDuplicates())
1625 {
1626 // First we get all the indeces (which we use
1627 // to iterate through the list)
1628
1629 SortedDictionary<int, List<ObjectId>>.
1630 KeyCollection kc =
1631 m_dups.Keys;
1632
1633 // Copy the KeyCollection into a list of ints,
1634 // to make it easier to iterate
1635 // (this also allows us to modify the m_dups
1636 // object while we're iterating)
1637
1638 List<int> idxs = new List<int>(kc.Count);
1639 foreach (int pos in kc)
1640 idxs.Add(pos);
1641
1642 foreach (int pos in idxs)
1643 {
1644 List<ObjectId> ids;
1645 m_dups.TryGetValue(pos, out ids);
1646
1647 if (m_dups.ContainsKey(pos - start))
1648 throw new Exception(
1649 ErrorStatus.DuplicateKey,
1650 "\nClash detected - " +
1651 "duplicate list may be corrupted."
1652 );
1653 else
1654 {
1655 // Remove the old entry and add the new one
1656
1657 m_dups.Remove(pos);
1658 m_dups.Add(pos - start, ids);
1659 }
1660 }
1661 }
1662 }
1663 }
1664 }
As for the specific changes...
Lines 19-71 encapsulate the information we need to store per-document, with lines 140-183 allowing retrieval of this data.
Line 102-130 and 185-289 add event handlers to the application. Note that we watch Database.ObjectAppended event to find when numbered objects are added to a drawing, but we do the actually work of renumbering the objects during DocumentCollection.DocumentLockWillChange - the safe place to do so.
A lot of the additional line changes are simply to access the new per-document data via the BubbleData class: 300-302, 316-317, 322, 402, 418, 452-453, 800-801, 811, 813, 821, 823, 826, 843-844, 854,861, 883-884, 890, 904-905, 915, 1050-1051, 1083-1084, 1124, 1140-1141, 1143, 1161-1162, 1208-1209.
Lines 458-616 add the ability to renumber bubbles while scanning the drawing, whether automatically (with no user intervention) or semi-automatically (allowing the user to choose a specific bubble not to renumber). This latter process uses a new HighlightBubbles() function (lines 620-644) to highlight a list of bubbles (it's generic, so could be called HighlightObjects(), thinking about it). This is then also used by the HLB command, replacing the previous implementation (lines 917-931).
We now pass the BubbleData class through to the GetBubbleNumber() function, in line 1120. This is then used in lines 807, 817, 850 & 911.
The NumberedObjectManager class has required infrastructure changes to support duplicates: 1240-1247, 1259-1260, 1269 & 1272-1277. We're using a SortedDictionary to maintain a list of ObjectIds per position. This is a standard "overflow" data structure, and is only added to when a duplicate is found.
DumpInfo() now displays duplicate data in the DMP command (lines 1571-1590).
RebaseList() has been changed to move entries in the duplicate list. This ended up being quite complicated, as we're mapping a list of objects against a position (the dictionary key) and so it's the key that changes when we move the list's base.
To try out this additional functionality, try this:
- Open a drawing with a bunch of bubbles, or create new ones.
- Copy & paste these bubbles one or more times, to seed the drawing with duplicate-numbered objects.
- Load the application and run the LNS command to understand the numbers in the drawing. Try the different options for renumbering duplicates (Automatically, Individually or Not).
- Next try copying and pasting again, once the numbering system is active, and see how the copied numbers are changed.
- INSERT a few bubbles: new blocks will also be renumbered - even if the user selects a particular number via the attribute editing dialog - but that's somewhat inevitable with this approach.
OK - now that we're up at nearly 1700 lines of code, it's getting time to bring this series to a close... (or at least to stop including the entire code in each post. :-)