In the last post we introduced some additional features to the original post in this series. In this post we take advantage of - and further extend - those features, by allowing deletion, movement and compaction of the numbered objects.
Here's the modified C# code, with changed/new lines in red, and here is the updated source file:
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
8 namespace AutoNumberedBubbles
9 {
10 public class Commands : IExtensionApplication
11 {
12 // Strings identifying the block
13 // and the attribute name to use
14
15 const string blockName = "BUBBLE";
16 const string attbName = "NUMBER";
17
18 // We will use a separate object to
19 // manage our numbering, and maintain a
20 // "base" index (the start of the list)
21
22 private NumberedObjectManager m_nom;
23 private int m_baseNumber = 0;
24
25 // Constructor
26
27 public Commands()
28 {
29 m_nom = new NumberedObjectManager();
30 }
31
32 // Functions called on initialization & termination
33
34 public void Initialize()
35 {
36 try
37 {
38 Document doc =
39 Application.DocumentManager.MdiActiveDocument;
40 Editor ed = doc.Editor;
41
42 ed.WriteMessage(
43 "\nLNS Load numbering settings by analyzing the current drawing" +
44 "\nDMP Print internal numbering information" +
45 "\nBAP Create bubbles at points" +
46 "\nBIC Create bubbles at the center of circles" +
47 "\nMB Move a bubble in the list" +
48 "\nDB Delete a bubble" +
49 "\nRBS Reorder the bubbles, to close gaps caused by deletion" +
50 "\nHLB Highlight a particular bubble"
51 );
52 }
53 catch
54 { }
55 }
56
57 public void Terminate()
58 {
59 }
60
61 // Command to extract and display information
62 // about the internal numbering
63
64 [CommandMethod("DMP")]
65 public void DumpNumberingInformation()
66 {
67 Document doc =
68 Application.DocumentManager.MdiActiveDocument;
69 Editor ed = doc.Editor;
70 m_nom.DumpInfo(ed);
71 }
72
73 // Command to analyze the current document and
74 // understand which indeces have been used and
75 // which are currently free
76
77 [CommandMethod("LNS")]
78 public void LoadNumberingSettings()
79 {
80 Document doc =
81 Application.DocumentManager.MdiActiveDocument;
82 Database db = doc.Database;
83 Editor ed = doc.Editor;
84
85 // We need to clear any internal state
86 // already collected
87
88 m_nom.Clear();
89 m_baseNumber = 0;
90
91 // Select all the blocks in the current drawing
92
93 TypedValue[] tvs =
94 new TypedValue[1] {
95 new TypedValue(
96 (int)DxfCode.Start,
97 "INSERT"
98 )
99 };
100 SelectionFilter sf =
101 new SelectionFilter(tvs);
102
103 PromptSelectionResult psr =
104 ed.SelectAll(sf);
105
106 // If it succeeded and we have some blocks...
107
108 if (psr.Status == PromptStatus.OK &&
109 psr.Value.Count > 0)
110 {
111 Transaction tr =
112 db.TransactionManager.StartTransaction();
113 using (tr)
114 {
115 // First get the modelspace and the ID
116 // of the block for which we're searching
117
118 BlockTableRecord ms;
119 ObjectId blockId;
120
121 if (GetBlock(
122 db, tr, out ms, out blockId
123 ))
124 {
125 // For each block reference in the drawing...
126
127 foreach (SelectedObject o in psr.Value)
128 {
129 DBObject obj =
130 tr.GetObject(o.ObjectId, OpenMode.ForRead);
131 BlockReference br = obj as BlockReference;
132 if (br != null)
133 {
134 // If it's the one we care about...
135
136 if (br.BlockTableRecord == blockId)
137 {
138 // Check its attribute references...
139
140 int pos = -1;
141 AttributeCollection ac =
142 br.AttributeCollection;
143
144 foreach (ObjectId id in ac)
145 {
146 DBObject obj2 =
147 tr.GetObject(id, OpenMode.ForRead);
148 AttributeReference ar =
149 obj2 as AttributeReference;
150
151 // When we find the attribute
152 // we care about...
153
154 if (ar.Tag == attbName)
155 {
156 try
157 {
158 // Attempt to extract the number from
159 // the text string property... use a
160 // try-catch block just in case it is
161 // non-numeric
162
163 pos =
164 int.Parse(ar.TextString);
165
166 // Add the object at the appropriate
167 // index
168
169 m_nom.NumberObject(
170 o.ObjectId, pos, false
171 );
172 }
173 catch { }
174 }
175 }
176 }
177 }
178 }
179 }
180 tr.Commit();
181 }
182
183 // Once we have analyzed all the block references...
184
185 int start = m_nom.GetLowerBound(true);
186
187 // If the first index is non-zero, ask the user if
188 // they want to rebase the list to begin at the
189 // current start position
190
191 if (start > 0)
192 {
193 ed.WriteMessage(
194 "\nLowest index is {0}. ",
195 start
196 );
197 PromptKeywordOptions pko =
198 new PromptKeywordOptions(
199 "Make this the start of the list?"
200 );
201 pko.AllowNone = true;
202 pko.Keywords.Add("Yes");
203 pko.Keywords.Add("No");
204 pko.Keywords.Default = "Yes";
205
206 PromptResult pkr =
207 ed.GetKeywords(pko);
208
209 if (pkr.Status == PromptStatus.OK)
210 {
211 if (pkr.StringResult == "Yes")
212 {
213 // We store our own base number
214 // (the object used to manage objects
215 // always uses zero-based indeces)
216
217 m_baseNumber = start;
218 m_nom.RebaseList(m_baseNumber);
219 }
220 }
221 }
222 }
223 }
224
225 // Command to create bubbles at points selected
226 // by the user - loops until cancelled
227
228 [CommandMethod("BAP")]
229 public void BubblesAtPoints()
230 {
231 Document doc =
232 Application.DocumentManager.MdiActiveDocument;
233 Database db = doc.Database;
234 Editor ed = doc.Editor;
235 Autodesk.AutoCAD.ApplicationServices.
236 TransactionManager tm =
237 doc.TransactionManager;
238
239 Transaction tr =
240 tm.StartTransaction();
241 using (tr)
242 {
243 // Get the information about the block
244 // and attribute definitions we care about
245
246 BlockTableRecord ms;
247 ObjectId blockId;
248 AttributeDefinition ad;
249 List<AttributeDefinition> other;
250
251 if (GetBlock(
252 db, tr, out ms, out blockId
253 ))
254 {
255 GetBlockAttributes(
256 tr, blockId, out ad, out other
257 );
258
259 // By default the modelspace is returned to
260 // us in read-only state
261
262 ms.UpgradeOpen();
263
264 // Loop until cancelled
265
266 bool finished = false;
267 while (!finished)
268 {
269 PromptPointOptions ppo =
270 new PromptPointOptions("\nSelect point: ");
271 ppo.AllowNone = true;
272
273 PromptPointResult ppr =
274 ed.GetPoint(ppo);
275 if (ppr.Status != PromptStatus.OK)
276 finished = true;
277 else
278 // Call a function to create our bubble
279 CreateNumberedBubbleAtPoint(
280 db, ms, tr, ppr.Value,
281 blockId, ad, other
282 );
283 tm.QueueForGraphicsFlush();
284 tm.FlushGraphics();
285 }
286 }
287 tr.Commit();
288 }
289 }
290
291 // Command to create a bubble at the center of
292 // each of the selected circles
293
294 [CommandMethod("BIC")]
295 public void BubblesInCircles()
296 {
297 Document doc =
298 Application.DocumentManager.MdiActiveDocument;
299 Database db = doc.Database;
300 Editor ed = doc.Editor;
301
302 // Allow the user to select circles
303
304 TypedValue[] tvs =
305 new TypedValue[1] {
306 new TypedValue(
307 (int)DxfCode.Start,
308 "CIRCLE"
309 )
310 };
311 SelectionFilter sf =
312 new SelectionFilter(tvs);
313
314 PromptSelectionResult psr =
315 ed.GetSelection(sf);
316
317 if (psr.Status == PromptStatus.OK &&
318 psr.Value.Count > 0)
319 {
320 Transaction tr =
321 db.TransactionManager.StartTransaction();
322 using (tr)
323 {
324 // Get the information about the block
325 // and attribute definitions we care about
326
327 BlockTableRecord ms;
328 ObjectId blockId;
329 AttributeDefinition ad;
330 List<AttributeDefinition> other;
331
332 if (GetBlock(
333 db, tr, out ms, out blockId
334 ))
335 {
336 GetBlockAttributes(
337 tr, blockId, out ad, out other
338 );
339
340 // By default the modelspace is returned to
341 // us in read-only state
342
343 ms.UpgradeOpen();
344
345 foreach (SelectedObject o in psr.Value)
346 {
347 // For each circle in the selected list...
348
349 DBObject obj =
350 tr.GetObject(o.ObjectId, OpenMode.ForRead);
351 Circle c = obj as Circle;
352 if (c == null)
353 ed.WriteMessage(
354 "\nObject selected is not a circle."
355 );
356 else
357 // Call our numbering function, passing the
358 // center of the circle
359 CreateNumberedBubbleAtPoint(
360 db, ms, tr, c.Center,
361 blockId, ad, other
362 );
363 }
364 }
365 tr.Commit();
366 }
367 }
368 }
369
370 // Command to delete a particular bubble
371 // selected by its index
372
373 [CommandMethod("MB")]
374 public void MoveBubble()
375 {
376 Document doc =
377 Application.DocumentManager.MdiActiveDocument;
378 Editor ed = doc.Editor;
379
380 // Use a helper function to select a valid bubble index
381
382 int pos =
383 GetBubbleNumber(
384 ed,
385 "\nEnter number of bubble to move: "
386 );
387
388 if (pos >= m_baseNumber)
389 {
390 int from = pos - m_baseNumber;
391
392 pos =
393 GetBubbleNumber(
394 ed,
395 "\nEnter destination position: "
396 );
397
398 if (pos >= m_baseNumber)
399 {
400 int to = pos - m_baseNumber;
401
402 ObjectIdCollection ids =
403 m_nom.MoveObject(from, to);
404
405 RenumberBubbles(doc.Database, ids);
406 }
407 }
408 }
409
410 // Command to delete a particular bubbler,
411 // selected by its index
412
413 [CommandMethod("DB")]
414 public void DeleteBubble()
415 {
416 Document doc =
417 Application.DocumentManager.MdiActiveDocument;
418 Database db = doc.Database;
419 Editor ed = doc.Editor;
420
421 // Use a helper function to select a valid bubble index
422
423 int pos =
424 GetBubbleNumber(
425 ed,
426 "\nEnter number of bubble to erase: "
427 );
428
429 if (pos >= m_baseNumber)
430 {
431 // Remove the object from the internal list
432 // (this returns the ObjectId stored for it,
433 // which we can then use to erase the entity)
434
435 ObjectId id =
436 m_nom.RemoveObject(pos - m_baseNumber);
437 Transaction tr =
438 db.TransactionManager.StartTransaction();
439 using (tr)
440 {
441 DBObject obj =
442 tr.GetObject(id, OpenMode.ForWrite);
443 obj.Erase();
444 tr.Commit();
445 }
446 }
447 }
448
449 // Command to reorder all the bubbles in the drawing,
450 // closing all the gaps between numbers but maintaining
451 // the current numbering order
452
453 [CommandMethod("RBS")]
454 public void ReorderBubbles()
455 {
456 Document doc =
457 Application.DocumentManager.MdiActiveDocument;
458
459 // Re-order the bubbles - the IDs returned are
460 // of the objects that need to be renumbered
461
462 ObjectIdCollection ids =
463 m_nom.ReorderObjects();
464
465 RenumberBubbles(doc.Database, ids);
466 }
467
468 // Command to highlight a particular bubble
469
470 [CommandMethod("HLB")]
471 public void HighlightBubble()
472 {
473 Document doc =
474 Application.DocumentManager.MdiActiveDocument;
475 Database db = doc.Database;
476 Editor ed = doc.Editor;
477
478 // Use our function to select a valid bubble index
479
480 int pos =
481 GetBubbleNumber(
482 ed,
483 "\nEnter number of bubble to highlight: "
484 );
485
486 if (pos >= m_baseNumber)
487 {
488 Transaction tr =
489 db.TransactionManager.StartTransaction();
490 using (tr)
491 {
492 // Get the ObjectId from the index...
493
494 ObjectId id =
495 m_nom.GetObjectId(pos - m_baseNumber);
496
497 if (id == ObjectId.Null)
498 {
499 ed.WriteMessage(
500 "\nNumber is not currently used -" +
501 " nothing to highlight."
502 );
503 return;
504 }
505
506 // And then open & highlight the entity
507
508 Entity ent =
509 (Entity)tr.GetObject(
510 id,
511 OpenMode.ForRead
512 );
513 ent.Highlight();
514 tr.Commit();
515 }
516 }
517 }
518
519 // Internal helper function to open and retrieve
520 // the model-space and the block def we care about
521
522 private bool
523 GetBlock(
524 Database db,
525 Transaction tr,
526 out BlockTableRecord ms,
527 out ObjectId blockId
528 )
529 {
530 BlockTable bt =
531 (BlockTable)tr.GetObject(
532 db.BlockTableId,
533 OpenMode.ForRead
534 );
535
536 if (!bt.Has(blockName))
537 {
538 Document doc =
539 Application.DocumentManager.MdiActiveDocument;
540 Editor ed = doc.Editor;
541 ed.WriteMessage(
542 "\nCannot find block definition \"" +
543 blockName +
544 "\" in the current drawing."
545 );
546
547 blockId = ObjectId.Null;
548 ms = null;
549 return false;
550 }
551
552 ms =
553 (BlockTableRecord)tr.GetObject(
554 bt[BlockTableRecord.ModelSpace],
555 OpenMode.ForRead
556 );
557
558 blockId = bt[blockName];
559
560 return true;
561 }
562
563 // Internal helper function to retrieve
564 // attribute info from our block
565 // (we return the main attribute def
566 // and then all the "others")
567
568 private void
569 GetBlockAttributes(
570 Transaction tr,
571 ObjectId blockId,
572 out AttributeDefinition ad,
573 out List<AttributeDefinition> other
574 )
575 {
576 BlockTableRecord blk =
577 (BlockTableRecord)tr.GetObject(
578 blockId,
579 OpenMode.ForRead
580 );
581
582 ad = null;
583 other =
584 new List<AttributeDefinition>();
585
586 foreach (ObjectId attId in blk)
587 {
588 DBObject obj =
589 (DBObject)tr.GetObject(
590 attId,
591 OpenMode.ForRead
592 );
593 AttributeDefinition ad2 =
594 obj as AttributeDefinition;
595
596 if (ad2 != null)
597 {
598 if (ad2.Tag == attbName)
599 {
600 if (ad2.Constant)
601 {
602 Document doc =
603 Application.DocumentManager.MdiActiveDocument;
604 Editor ed = doc.Editor;
605
606 ed.WriteMessage(
607 "\nAttribute to change is constant!"
608 );
609 }
610 else
611 ad = ad2;
612 }
613 else
614 if (!ad2.Constant)
615 other.Add(ad2);
616 }
617 }
618 }
619
620 // Internal helper function to create a bubble
621 // at a particular point
622
623 private Entity
624 CreateNumberedBubbleAtPoint(
625 Database db,
626 BlockTableRecord btr,
627 Transaction tr,
628 Point3d pt,
629 ObjectId blockId,
630 AttributeDefinition ad,
631 List<AttributeDefinition> other
632 )
633 {
634 // Create a new block reference
635
636 BlockReference br =
637 new BlockReference(pt, blockId);
638
639 // Add it to the database
640
641 br.SetDatabaseDefaults();
642 ObjectId blockRefId = btr.AppendEntity(br);
643 tr.AddNewlyCreatedDBObject(br, true);
644
645 // Create an attribute reference for our main
646 // attribute definition (where we'll put the
647 // bubble's number)
648
649 AttributeReference ar =
650 new AttributeReference();
651
652 // Add it to the database, and set its position, etc.
653
654 ar.SetDatabaseDefaults();
655 ar.SetAttributeFromBlock(ad, br.BlockTransform);
656 ar.Position =
657 ad.Position.TransformBy(br.BlockTransform);
658 ar.Tag = ad.Tag;
659
660 // Set the bubble's number
661
662 int bubbleNumber =
663 m_baseNumber +
664 m_nom.NextObjectNumber(blockRefId);
665
666 ar.TextString = bubbleNumber.ToString();
667 ar.AdjustAlignment(db);
668
669 // Add the attribute to the block reference
670
671 br.AttributeCollection.AppendAttribute(ar);
672 tr.AddNewlyCreatedDBObject(ar, true);
673
674 // Now we add attribute references for the
675 // other attribute definitions
676
677 foreach (AttributeDefinition ad2 in other)
678 {
679 AttributeReference ar2 =
680 new AttributeReference();
681
682 ar2.SetAttributeFromBlock(ad2, br.BlockTransform);
683 ar2.Position =
684 ad2.Position.TransformBy(br.BlockTransform);
685 ar2.Tag = ad2.Tag;
686 ar2.TextString = ad2.TextString;
687 ar2.AdjustAlignment(db);
688
689 br.AttributeCollection.AppendAttribute(ar2);
690 tr.AddNewlyCreatedDBObject(ar2, true);
691 }
692 return br;
693 }
694
695 // Internal helper function to have the user
696 // select a valid bubble index
697
698 private int
699 GetBubbleNumber(
700 Editor ed,
701 string prompt
702 )
703 {
704 int upper = m_nom.GetUpperBound();
705 if (upper <= 0)
706 {
707 ed.WriteMessage(
708 "\nNo bubbles are currently being managed."
709 );
710 return upper;
711 }
712
713 PromptIntegerOptions pio =
714 new PromptIntegerOptions(prompt);
715 pio.AllowNone = false;
716
717 // Get the limits from our manager object
718
719 pio.LowerLimit =
720 m_baseNumber +
721 m_nom.GetLowerBound(false);
722 pio.UpperLimit =
723 m_baseNumber +
724 upper;
725
726 PromptIntegerResult pir =
727 ed.GetInteger(pio);
728 if (pir.Status == PromptStatus.OK)
729 return pir.Value;
730 else
731 return -1;
732 }
733
734 // Internal helper function to open up a list
735 // of bubbles and reset their number to the
736 // position in the list
737
738 private void RenumberBubbles(
739 Database db, ObjectIdCollection ids)
740 {
741 Transaction tr =
742 db.TransactionManager.StartTransaction();
743 using (tr)
744 {
745 // Get the block information
746
747 BlockTableRecord ms;
748 ObjectId blockId;
749
750 if (GetBlock(
751 db, tr, out ms, out blockId
752 ))
753 {
754 // Open each bubble to be renumbered
755
756 foreach (ObjectId bid in ids)
757 {
758 if (bid != ObjectId.Null)
759 {
760 DBObject obj =
761 tr.GetObject(bid, OpenMode.ForRead);
762 BlockReference br = obj as BlockReference;
763 if (br != null)
764 {
765 if (br.BlockTableRecord == blockId)
766 {
767 AttributeCollection ac =
768 br.AttributeCollection;
769
770 // Go through its attributes
771
772 foreach (ObjectId aid in ac)
773 {
774 DBObject obj2 =
775 tr.GetObject(aid, OpenMode.ForRead);
776 AttributeReference ar =
777 obj2 as AttributeReference;
778
779 if (ar.Tag == attbName)
780 {
781 // Change the one we care about
782
783 ar.UpgradeOpen();
784
785 int bubbleNumber =
786 m_baseNumber + m_nom.GetNumber(bid);
787 ar.TextString =
788 bubbleNumber.ToString();
789
790 break;
791 }
792 }
793 }
794 }
795 }
796 }
797 }
798 tr.Commit();
799 }
800 }
801 }
802
803 // A generic class for managing groups of
804 // numbered (and ordered) objects
805
806 public class NumberedObjectManager
807 {
808 // We need to store a list of object IDs, but
809 // also a list of free positions in the list
810 // (this allows numbering gaps)
811
812 private List<ObjectId> m_ids;
813 private List<int> m_free;
814
815 // Constructor
816
817 public NumberedObjectManager()
818 {
819 m_ids =
820 new List<ObjectId>();
821
822 m_free =
823 new List<int>();
824 }
825
826 // Clear the internal lists
827
828 public void Clear()
829 {
830 m_ids.Clear();
831 m_free.Clear();
832 }
833
834 // Return the first entry in the ObjectId list
835 // (specify "true" if you want to skip
836 // any null object IDs)
837
838 public int GetLowerBound(bool ignoreNull)
839 {
840 if (ignoreNull)
841 // Define an in-line predicate to check
842 // whether an ObjectId is null
843 return
844 m_ids.FindIndex(
845 delegate(ObjectId id)
846 {
847 return id != ObjectId.Null;
848 }
849 );
850 else
851 return 0;
852 }
853
854 // Return the last entry in the ObjectId list
855
856 public int GetUpperBound()
857 {
858 return m_ids.Count - 1;
859 }
860
861 // Store the specified ObjectId in the next
862 // available location in the list, and return
863 // what that is
864
865 public int NextObjectNumber(ObjectId id)
866 {
867 int pos;
868 if (m_free.Count > 0)
869 {
870 // Get the first free position, then remove
871 // it from the "free" list
872
873 pos = m_free[0];
874 m_free.RemoveAt(0);
875 m_ids[pos] = id;
876 }
877 else
878 {
879 // There are no free slots (gaps in the numbering)
880 // so we append it to the list
881
882 pos = m_ids.Count;
883 m_ids.Add(id);
884 }
885 return pos;
886 }
887
888 // Go through the list of objects and close any gaps
889 // by shuffling the list down (easy, as we're using a
890 // List<> rather than an array)
891
892 public ObjectIdCollection ReorderObjects()
893 {
894 // Create a collection of ObjectIds we'll return
895 // for the caller to go and update
896 // (so the renumbering will gets reflected
897 // in the objects themselves)
898
899 ObjectIdCollection ids =
900 new ObjectIdCollection();
901
902 // We'll go through the "free" list backwards,
903 // to allow any changes made to the list of
904 // objects to not affect what we're doing
905
906 List<int> rev =
907 new List<int>(m_free);
908 rev.Reverse();
909
910 foreach (int pos in rev)
911 {
912 // First we remove the object at the "free"
913 // position (in theory this should be set to
914 // ObjectId.Null, as the slot has been marked
915 // as blank)
916
917 m_ids.RemoveAt(pos);
918
919 // Now we go through and add the IDs of any
920 // affected objects to the list to return
921
922 for (int i = pos; i < m_ids.Count; i++)
923 {
924 ObjectId id = m_ids[pos];
925
926 // Only add non-null objects
927 // not already in the list
928
929 if (!ids.Contains(id) &&
930 id != ObjectId.Null)
931 ids.Add(id);
932 }
933 }
934
935 // Our free slots have been filled, so clear
936 // the list
937
938 m_free.Clear();
939
940 return ids;
941 }
942
943 // Get the ID of an object at a particular position
944
945 public ObjectId GetObjectId(int pos)
946 {
947 if (pos < m_ids.Count)
948 return m_ids[pos];
949 else
950 return ObjectId.Null;
951 }
952
953 // Get the position of an ObjectId in the list
954
955 public int GetNumber(ObjectId id)
956 {
957 if (m_ids.Contains(id))
958 return m_ids.IndexOf(id);
959 else
960 return -1;
961 }
962
963 // Store an ObjectId in a particular position
964 // (shuffle == true will "insert" it, shuffling
965 // the remaining objects down,
966 // shuffle == false will replace the item in
967 // that slot)
968
969 public void NumberObject(
970 ObjectId id, int index, bool shuffle)
971 {
972 // If we're inserting into the list
973
974 if (index < m_ids.Count)
975 {
976 if (shuffle)
977 // Insert takes care of the shuffling
978 m_ids.Insert(index, id);
979 else
980 {
981 // If we're replacing the existing item, do
982 // so and then make sure the slot is removed
983 // from the "free" list, if applicable
984
985 m_ids[index] = id;
986 if (m_free.Contains(index))
987 m_free.Remove(index);
988 }
989 }
990 else
991 {
992 // If we're appending, shuffling is irrelevant,
993 // but we may need to add additional "free" slots
994 // if the position comes after the end
995
996 while (m_ids.Count < index)
997 {
998 m_ids.Add(ObjectId.Null);
999 m_free.Add(m_ids.LastIndexOf(ObjectId.Null));
1000 m_free.Sort();
1001 }
1002 m_ids.Add(id);
1003 }
1004 }
1005
1006 // Move an ObjectId already in the list to a
1007 // particular position
1008 // (ObjectIds between the two positions will
1009 // get shuffled down automatically)
1010
1011 public ObjectIdCollection MoveObject(
1012 int from, int to)
1013 {
1014 ObjectIdCollection ids =
1015 new ObjectIdCollection();
1016
1017 if (from < m_ids.Count &&
1018 to < m_ids.Count)
1019 {
1020 if (from != to)
1021 {
1022 ObjectId id = m_ids[from];
1023 m_ids.RemoveAt(from);
1024 m_ids.Insert(to, id);
1025
1026 int start = (from < to ? from : to);
1027 int end = (from < to ? to : from);
1028
1029 for (int i = start; i <= end; i++)
1030 {
1031 ids.Add(m_ids[i]);
1032 }
1033 }
1034 // Now need to adjust/recreate "free" list
1035 m_free.Clear();
1036 for (int j = 0; j < m_ids.Count; j++)
1037 {
1038 if (m_ids[j] == ObjectId.Null)
1039 m_free.Add(j);
1040 }
1041 }
1042 return ids;
1043 }
1044
1045 // Remove an ObjectId from the list
1046
1047 public int RemoveObject(ObjectId id)
1048 {
1049 // Check it's non-null and in the list
1050
1051 if (id != ObjectId.Null &&
1052 m_ids.Contains(id))
1053 {
1054 int pos = m_ids.IndexOf(id);
1055 RemoveObject(pos);
1056 return pos;
1057 }
1058 return -1;
1059 }
1060
1061 // Remove the ObjectId at a particular position
1062
1063 public ObjectId RemoveObject(int pos)
1064 {
1065 // Get the ObjectId in the specified position,
1066 // making sure it's non-null
1067
1068 ObjectId id = m_ids[pos];
1069 if (id != ObjectId.Null)
1070 {
1071 // Null out the position and add it to the
1072 // "free" list
1073
1074 m_ids[pos] = ObjectId.Null;
1075 m_free.Add(pos);
1076 m_free.Sort();
1077 }
1078 return id;
1079 }
1080
1081 // Dump out the object list information
1082 // as well as the "free" slots
1083
1084 public void DumpInfo(Editor ed)
1085 {
1086 if (m_ids.Count > 0)
1087 {
1088 ed.WriteMessage("\nIdx ObjectId");
1089
1090 int index = 0;
1091 foreach (ObjectId id in m_ids)
1092 ed.WriteMessage("\n{0} {1}", index++, id);
1093 }
1094
1095 if (m_free.Count > 0)
1096 {
1097 ed.WriteMessage("\n\nFree list: ");
1098
1099 foreach (int pos in m_free)
1100 ed.WriteMessage("{0} ", pos);
1101 }
1102 }
1103
1104 // Remove the initial n items from the list
1105
1106 public void RebaseList(int start)
1107 {
1108 // First we remove the ObjectIds
1109
1110 for (int i=0; i < start; i++)
1111 m_ids.RemoveAt(0);
1112
1113 // Then we go through the "free" list...
1114
1115 int idx = 0;
1116 while (idx < m_free.Count)
1117 {
1118 if (m_free[idx] < start)
1119 // Remove any that refer to the slots
1120 // we've removed
1121 m_free.RemoveAt(idx);
1122 else
1123 {
1124 // Subtracting the number of slots
1125 // we've removed from the other items
1126 m_free[idx] -= start;
1127 idx++;
1128 }
1129 }
1130 }
1131 }
1132 }
The above code defines four new commands which move, delete and highlight a bubble, and reorder the bubble list. I could probably have used better terminology for some of the command-names - the MB (Move Bubble) command does not move the physical position of the block in the drawing, it moves the bubble inside the list (i.e. it changes the bubble's number while maintaining the consistency of the list). Similarly, RBS (Reorder BubbleS) actually just compacts the list, removing unnecessary gaps in the list created by deletion. Anyway, the user is notified of the additional commands by lines 46-50, and the commands themselves are implemented by lines 370-517. MB, DB (Delete Bubble) and HLB (HighLight Bubble) all use a new helper function, GetBubbleNumber(), defined by lines 695-733, which asks the user to select a valid bubble from the list, which will then get moved, deleted or highlighted, as appropriate.
The other new helper function which is defined outside the NumberedObjectManager class (as the function depends on the specific implementation of our object numbering, i.e. with the value stored in an attribute in a block), is RenumberBubbles(), defined by lines 734-800. This function opens up a list of bubbles and sets their visible number to the one stored in the NamedObjectManager object. It is used by both MB and RBS.
To support these new commands, the NamedObjectManager class has also been extended in two new sections of the above code. The first new chunk of code (lines 888-961) implements new methods ReorderObjects() which again, is really a list compaction function and then GetObjectId() and GetNumber(), which - as you'd expect - return an ObjectId at a particular position and a position for a particular ObjectId. The next chunk (lines 1006-1079) implements MoveObject(), which moves an object from one place to another - shuffling the intermediate bubbles around, as needed - and two versions of RemoveObject(), depending on whether you wish to select the object by its ID or its position.
Something important to note about this implementation: so far we haven't dealt with what happens should the user choose to undo these commands: as the objects we're creating are not managed by AutoCAD (they are not stored in the drawing, for instance), their state is not captured in the undo filer, and so will not be affected by undo. But the geometry they refer to will, of course, so there is substantial potential for our list getting out of sync with reality. The easy (and arguably the best) way to get around this is to check for undo-related commands to be executed, and invalidate our list at that point (providing a suitable notification to the user, requesting that they run LNS again once done with their undoing & redoing). The current implementation does not do this.
Let's now take our new commands for a quick spin...
We're going to take our previously-created drawing, as a starting point, and use our new commands on it.
Let's start with DB:
Command: LNS
Lowest index is 1. Make this the start of the list? [Yes/No] <Yes>: Yes
Command: DB
Enter number of bubble to erase: 3
Command: DB
Enter number of bubble to erase: 5
Command: DB
Enter number of bubble to erase: 15
Command: DB
Enter number of bubble to erase: 16
Command: DMP
Idx ObjectId
0 (2129683752)
1 (2129683776)
2 (0)
3 (2129683824)
4 (0)
5 (2129683872)
6 (2129683896)
7 (2129683920)
8 (2129683944)
9 (2129683968)
10 (2129683992)
11 (2129684016)
12 (2129684040)
13 (2129684064)
14 (0)
15 (0)
16 (2129684136)
17 (2129684160)
18 (2129684184)
19 (2129684208)
Free list: 2 4 14 15
As you can see, we've ended up with a few free slots in our list (and you'll note you need to add our "base number" (1) to get to the visible number). Here's the state of the drawing at this point:
Now let's try MB:
Command: MB
Enter number of bubble to move: 7
Enter destination position: 2
Command: DMP
Idx ObjectId
0 (2129683752)
1 (2129683896)
2 (2129683776)
3 (0)
4 (2129683824)
5 (0)
6 (2129683872)
7 (2129683920)
8 (2129683944)
9 (2129683968)
10 (2129683992)
11 (2129684016)
12 (2129684040)
13 (2129684064)
14 (0)
15 (0)
16 (2129684136)
17 (2129684160)
18 (2129684184)
19 (2129684208)
Free list: 3 5 14 15
This results in the item in internal slot 6 being moved to internal slot 1 (remember that base number :-) and the objects between being shuffled along. Here's what's on the screen at this point:
And finally we'll compact the list - removing those four free slots - with our RBS command:
Command: RBS
Command: DMP
Idx ObjectId
0 (2129683752)
1 (2129683896)
2 (2129683776)
3 (2129683824)
4 (2129683872)
5 (2129683920)
6 (2129683944)
7 (2129683968)
8 (2129683992)
9 (2129684016)
10 (2129684040)
11 (2129684064)
12 (2129684136)
13 (2129684160)
14 (2129684184)
15 (2129684208)
And here's how that looks:
I don't currently have any further enhancements planned for this application. Feel free to post a comment or send me an email if there's a particular direction in which you'd like to see it go. For instance, is it interesting to see support for prefixes/suffixes...?