In the last post we saw some code to perform simple sequential numbering of blocks (reflected in a particular attribute contained in each block). In this next installment we'll extend the code by introducing a NumberedObjectManager class, which will manage the activities related to maintaining the sequence of numbers used by the various blocks. The main code will create an object of this class which will be used extensively in this and the next post by a number of new commands.
Here's the updated C# code, with changed & new lines marked with a red line-number. For your convenience here is the source file, to save you having to strip off the 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
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 );
48 }
49 catch
50 { }
51 }
52
53 public void Terminate()
54 {
55 }
56
57 // Command to extract and display information
58 // about the internal numbering
59
60 [CommandMethod("DMP")]
61 public void DumpNumberingInformation()
62 {
63 Document doc =
64 Application.DocumentManager.MdiActiveDocument;
65 Editor ed = doc.Editor;
66 m_nom.DumpInfo(ed);
67 }
68
69 // Command to analyze the current document and
70 // understand which indeces have been used and
71 // which are currently free
72
73 [CommandMethod("LNS")]
74 public void LoadNumberingSettings()
75 {
76 Document doc =
77 Application.DocumentManager.MdiActiveDocument;
78 Database db = doc.Database;
79 Editor ed = doc.Editor;
80
81 // We need to clear any internal state
82 // already collected
83
84 m_nom.Clear();
85 m_baseNumber = 0;
86
87 // Select all the blocks in the current drawing
88
89 TypedValue[] tvs =
90 new TypedValue[1] {
91 new TypedValue(
92 (int)DxfCode.Start,
93 "INSERT"
94 )
95 };
96 SelectionFilter sf =
97 new SelectionFilter(tvs);
98
99 PromptSelectionResult psr =
100 ed.SelectAll(sf);
101
102 // If it succeeded and we have some blocks...
103
104 if (psr.Status == PromptStatus.OK &&
105 psr.Value.Count > 0)
106 {
107 Transaction tr =
108 db.TransactionManager.StartTransaction();
109 using (tr)
110 {
111 // First get the modelspace and the ID
112 // of the block for which we're searching
113
114 BlockTableRecord ms;
115 ObjectId blockId;
116
117 if (GetBlock(
118 db, tr, out ms, out blockId
119 ))
120 {
121 // For each block reference in the drawing...
122
123 foreach (SelectedObject o in psr.Value)
124 {
125 DBObject obj =
126 tr.GetObject(o.ObjectId, OpenMode.ForRead);
127 BlockReference br = obj as BlockReference;
128 if (br != null)
129 {
130 // If it's the one we care about...
131
132 if (br.BlockTableRecord == blockId)
133 {
134 // Check its attribute references...
135
136 int pos = -1;
137 AttributeCollection ac =
138 br.AttributeCollection;
139
140 foreach (ObjectId id in ac)
141 {
142 DBObject obj2 =
143 tr.GetObject(id, OpenMode.ForRead);
144 AttributeReference ar =
145 obj2 as AttributeReference;
146
147 // When we find the attribute
148 // we care about...
149
150 if (ar.Tag == attbName)
151 {
152 try
153 {
154 // Attempt to extract the number from
155 // the text string property... use a
156 // try-catch block just in case it is
157 // non-numeric
158
159 pos =
160 int.Parse(ar.TextString);
161
162 // Add the object at the appropriate
163 // index
164
165 m_nom.NumberObject(
166 o.ObjectId, pos, false
167 );
168 }
169 catch { }
170 }
171 }
172 }
173 }
174 }
175 }
176 tr.Commit();
177 }
178
179 // Once we have analyzed all the block references...
180
181 int start = m_nom.GetLowerBound(true);
182
183 // If the first index is non-zero, ask the user if
184 // they want to rebase the list to begin at the
185 // current start position
186
187 if (start > 0)
188 {
189 ed.WriteMessage(
190 "\nLowest index is {0}. ",
191 start
192 );
193 PromptKeywordOptions pko =
194 new PromptKeywordOptions(
195 "Make this the start of the list?"
196 );
197 pko.AllowNone = true;
198 pko.Keywords.Add("Yes");
199 pko.Keywords.Add("No");
200 pko.Keywords.Default = "Yes";
201
202 PromptResult pkr =
203 ed.GetKeywords(pko);
204
205 if (pkr.Status == PromptStatus.OK)
206 {
207 if (pkr.StringResult == "Yes")
208 {
209 // We store our own base number
210 // (the object used to manage objects
211 // always uses zero-based indeces)
212
213 m_baseNumber = start;
214 m_nom.RebaseList(m_baseNumber);
215 }
216 }
217 }
218 }
219 }
220
221 // Command to create bubbles at points selected
222 // by the user - loops until cancelled
223
224 [CommandMethod("BAP")]
225 public void BubblesAtPoints()
226 {
227 Document doc =
228 Application.DocumentManager.MdiActiveDocument;
229 Database db = doc.Database;
230 Editor ed = doc.Editor;
231 Autodesk.AutoCAD.ApplicationServices.
232 TransactionManager tm =
233 doc.TransactionManager;
234
235 Transaction tr =
236 tm.StartTransaction();
237 using (tr)
238 {
239 // Get the information about the block
240 // and attribute definitions we care about
241
242 BlockTableRecord ms;
243 ObjectId blockId;
244 AttributeDefinition ad;
245 List<AttributeDefinition> other;
246
247 if (GetBlock(
248 db, tr, out ms, out blockId
249 ))
250 {
251 GetBlockAttributes(
252 tr, blockId, out ad, out other
253 );
254
255 // By default the modelspace is returned to
256 // us in read-only state
257
258 ms.UpgradeOpen();
259
260 // Loop until cancelled
261
262 bool finished = false;
263 while (!finished)
264 {
265 PromptPointOptions ppo =
266 new PromptPointOptions("\nSelect point: ");
267 ppo.AllowNone = true;
268
269 PromptPointResult ppr =
270 ed.GetPoint(ppo);
271 if (ppr.Status != PromptStatus.OK)
272 finished = true;
273 else
274 // Call a function to create our bubble
275 CreateNumberedBubbleAtPoint(
276 db, ms, tr, ppr.Value,
277 blockId, ad, other
278 );
279 tm.QueueForGraphicsFlush();
280 tm.FlushGraphics();
281 }
282 }
283 tr.Commit();
284 }
285 }
286
287 // Command to create a bubble at the center of
288 // each of the selected circles
289
290 [CommandMethod("BIC")]
291 public void BubblesInCircles()
292 {
293 Document doc =
294 Application.DocumentManager.MdiActiveDocument;
295 Database db = doc.Database;
296 Editor ed = doc.Editor;
297
298 // Allow the user to select circles
299
300 TypedValue[] tvs =
301 new TypedValue[1] {
302 new TypedValue(
303 (int)DxfCode.Start,
304 "CIRCLE"
305 )
306 };
307 SelectionFilter sf =
308 new SelectionFilter(tvs);
309
310 PromptSelectionResult psr =
311 ed.GetSelection(sf);
312
313 if (psr.Status == PromptStatus.OK &&
314 psr.Value.Count > 0)
315 {
316 Transaction tr =
317 db.TransactionManager.StartTransaction();
318 using (tr)
319 {
320 // Get the information about the block
321 // and attribute definitions we care about
322
323 BlockTableRecord ms;
324 ObjectId blockId;
325 AttributeDefinition ad;
326 List<AttributeDefinition> other;
327
328 if (GetBlock(
329 db, tr, out ms, out blockId
330 ))
331 {
332 GetBlockAttributes(
333 tr, blockId, out ad, out other
334 );
335
336 // By default the modelspace is returned to
337 // us in read-only state
338
339 ms.UpgradeOpen();
340
341 foreach (SelectedObject o in psr.Value)
342 {
343 // For each circle in the selected list...
344
345 DBObject obj =
346 tr.GetObject(o.ObjectId, OpenMode.ForRead);
347 Circle c = obj as Circle;
348 if (c == null)
349 ed.WriteMessage(
350 "\nObject selected is not a circle."
351 );
352 else
353 // Call our numbering function, passing the
354 // center of the circle
355 CreateNumberedBubbleAtPoint(
356 db, ms, tr, c.Center,
357 blockId, ad, other
358 );
359 }
360 }
361 tr.Commit();
362 }
363 }
364 }
365
366 // Internal helper function to open and retrieve
367 // the model-space and the block def we care about
368
369 private bool
370 GetBlock(
371 Database db,
372 Transaction tr,
373 out BlockTableRecord ms,
374 out ObjectId blockId
375 )
376 {
377 BlockTable bt =
378 (BlockTable)tr.GetObject(
379 db.BlockTableId,
380 OpenMode.ForRead
381 );
382
383 if (!bt.Has(blockName))
384 {
385 Document doc =
386 Application.DocumentManager.MdiActiveDocument;
387 Editor ed = doc.Editor;
388 ed.WriteMessage(
389 "\nCannot find block definition \"" +
390 blockName +
391 "\" in the current drawing."
392 );
393
394 blockId = ObjectId.Null;
395 ms = null;
396 return false;
397 }
398
399 ms =
400 (BlockTableRecord)tr.GetObject(
401 bt[BlockTableRecord.ModelSpace],
402 OpenMode.ForRead
403 );
404
405 blockId = bt[blockName];
406
407 return true;
408 }
409
410 // Internal helper function to retrieve
411 // attribute info from our block
412 // (we return the main attribute def
413 // and then all the "others")
414
415 private void
416 GetBlockAttributes(
417 Transaction tr,
418 ObjectId blockId,
419 out AttributeDefinition ad,
420 out List<AttributeDefinition> other
421 )
422 {
423 BlockTableRecord blk =
424 (BlockTableRecord)tr.GetObject(
425 blockId,
426 OpenMode.ForRead
427 );
428
429 ad = null;
430 other =
431 new List<AttributeDefinition>();
432
433 foreach (ObjectId attId in blk)
434 {
435 DBObject obj =
436 (DBObject)tr.GetObject(
437 attId,
438 OpenMode.ForRead
439 );
440 AttributeDefinition ad2 =
441 obj as AttributeDefinition;
442
443 if (ad2 != null)
444 {
445 if (ad2.Tag == attbName)
446 {
447 if (ad2.Constant)
448 {
449 Document doc =
450 Application.DocumentManager.MdiActiveDocument;
451 Editor ed = doc.Editor;
452
453 ed.WriteMessage(
454 "\nAttribute to change is constant!"
455 );
456 }
457 else
458 ad = ad2;
459 }
460 else
461 if (!ad2.Constant)
462 other.Add(ad2);
463 }
464 }
465 }
466
467 // Internal helper function to create a bubble
468 // at a particular point
469
470 private Entity
471 CreateNumberedBubbleAtPoint(
472 Database db,
473 BlockTableRecord btr,
474 Transaction tr,
475 Point3d pt,
476 ObjectId blockId,
477 AttributeDefinition ad,
478 List<AttributeDefinition> other
479 )
480 {
481 // Create a new block reference
482
483 BlockReference br =
484 new BlockReference(pt, blockId);
485
486 // Add it to the database
487
488 br.SetDatabaseDefaults();
489 ObjectId blockRefId = btr.AppendEntity(br);
490 tr.AddNewlyCreatedDBObject(br, true);
491
492 // Create an attribute reference for our main
493 // attribute definition (where we'll put the
494 // bubble's number)
495
496 AttributeReference ar =
497 new AttributeReference();
498
499 // Add it to the database, and set its position, etc.
500
501 ar.SetDatabaseDefaults();
502 ar.SetAttributeFromBlock(ad, br.BlockTransform);
503 ar.Position =
504 ad.Position.TransformBy(br.BlockTransform);
505 ar.Tag = ad.Tag;
506
507 // Set the bubble's number
508
509 int bubbleNumber =
510 m_baseNumber +
511 m_nom.NextObjectNumber(blockRefId);
512
513 ar.TextString = bubbleNumber.ToString();
514 ar.AdjustAlignment(db);
515
516 // Add the attribute to the block reference
517
518 br.AttributeCollection.AppendAttribute(ar);
519 tr.AddNewlyCreatedDBObject(ar, true);
520
521 // Now we add attribute references for the
522 // other attribute definitions
523
524 foreach (AttributeDefinition ad2 in other)
525 {
526 AttributeReference ar2 =
527 new AttributeReference();
528
529 ar2.SetAttributeFromBlock(ad2, br.BlockTransform);
530 ar2.Position =
531 ad2.Position.TransformBy(br.BlockTransform);
532 ar2.Tag = ad2.Tag;
533 ar2.TextString = ad2.TextString;
534 ar2.AdjustAlignment(db);
535
536 br.AttributeCollection.AppendAttribute(ar2);
537 tr.AddNewlyCreatedDBObject(ar2, true);
538 }
539 return br;
540 }
541 }
542
543 // A generic class for managing groups of
544 // numbered (and ordered) objects
545
546 public class NumberedObjectManager
547 {
548 // We need to store a list of object IDs, but
549 // also a list of free positions in the list
550 // (this allows numbering gaps)
551
552 private List<ObjectId> m_ids;
553 private List<int> m_free;
554
555 // Constructor
556
557 public NumberedObjectManager()
558 {
559 m_ids =
560 new List<ObjectId>();
561
562 m_free =
563 new List<int>();
564 }
565
566 // Clear the internal lists
567
568 public void Clear()
569 {
570 m_ids.Clear();
571 m_free.Clear();
572 }
573
574 // Return the first entry in the ObjectId list
575 // (specify "true" if you want to skip
576 // any null object IDs)
577
578 public int GetLowerBound(bool ignoreNull)
579 {
580 if (ignoreNull)
581 // Define an in-line predicate to check
582 // whether an ObjectId is null
583 return
584 m_ids.FindIndex(
585 delegate(ObjectId id)
586 {
587 return id != ObjectId.Null;
588 }
589 );
590 else
591 return 0;
592 }
593
594 // Return the last entry in the ObjectId list
595
596 public int GetUpperBound()
597 {
598 return m_ids.Count - 1;
599 }
600
601 // Store the specified ObjectId in the next
602 // available location in the list, and return
603 // what that is
604
605 public int NextObjectNumber(ObjectId id)
606 {
607 int pos;
608 if (m_free.Count > 0)
609 {
610 // Get the first free position, then remove
611 // it from the "free" list
612
613 pos = m_free[0];
614 m_free.RemoveAt(0);
615 m_ids[pos] = id;
616 }
617 else
618 {
619 // There are no free slots (gaps in the numbering)
620 // so we append it to the list
621
622 pos = m_ids.Count;
623 m_ids.Add(id);
624 }
625 return pos;
626 }
627
628 // Store an ObjectId in a particular position
629 // (shuffle == true will "insert" it, shuffling
630 // the remaining objects down,
631 // shuffle == false will replace the item in
632 // that slot)
633
634 public void NumberObject(
635 ObjectId id, int index, bool shuffle)
636 {
637 // If we're inserting into the list
638
639 if (index < m_ids.Count)
640 {
641 if (shuffle)
642 // Insert takes care of the shuffling
643 m_ids.Insert(index, id);
644 else
645 {
646 // If we're replacing the existing item, do
647 // so and then make sure the slot is removed
648 // from the "free" list, if applicable
649
650 m_ids[index] = id;
651 if (m_free.Contains(index))
652 m_free.Remove(index);
653 }
654 }
655 else
656 {
657 // If we're appending, shuffling is irrelevant,
658 // but we may need to add additional "free" slots
659 // if the position comes after the end
660
661 while (m_ids.Count < index)
662 {
663 m_ids.Add(ObjectId.Null);
664 m_free.Add(m_ids.LastIndexOf(ObjectId.Null));
665 m_free.Sort();
666 }
667 m_ids.Add(id);
668 }
669 }
670
671 // Dump out the object list information
672 // as well as the "free" slots
673
674 public void DumpInfo(Editor ed)
675 {
676 if (m_ids.Count > 0)
677 {
678 ed.WriteMessage("\nIdx ObjectId");
679
680 int index = 0;
681 foreach (ObjectId id in m_ids)
682 ed.WriteMessage("\n{0} {1}", index++, id);
683 }
684
685 if (m_free.Count > 0)
686 {
687 ed.WriteMessage("\n\nFree list: ");
688
689 foreach (int pos in m_free)
690 ed.WriteMessage("{0} ", pos);
691 }
692 }
693
694 // Remove the initial n items from the list
695
696 public void RebaseList(int start)
697 {
698 // First we remove the ObjectIds
699
700 for (int i=0; i < start; i++)
701 m_ids.RemoveAt(0);
702
703 // Then we go through the "free" list...
704
705 int idx = 0;
706 while (idx < m_free.Count)
707 {
708 if (m_free[idx] < start)
709 // Remove any that refer to the slots
710 // we've removed
711 m_free.RemoveAt(idx);
712 else
713 {
714 // Subtracting the number of slots
715 // we've removed from the other items
716 m_free[idx] -= start;
717 idx++;
718 }
719 }
720 }
721 }
722 }
Some information on the changes...
Rather than maintaining an integer for our "current number", we now have an instance of the NumberedObjectManager class, as well as a "base number" should we choose to start the numbering at something other than 0 (see lines 18-23 & 29). We use these variables when numbering the objects at lines 510-511.
We've added a couple of additional commands, LNS (Load Numbering System) and DMP (DuMP numbering system). These commands are announced to the user on load (lines 43-44) and implemented from lines 57-219. The DMP command is really an internal command to show the contents of the number list, while the LNS command analyses the current drawing and determines how the blocks inserted into it have been numbered. Should the numbering start at a higher number than 0, it asks the user whether to re-base the list, which essentially removes the initial entries (from 0 to the new start) and sets the "base number" variable. The function behind the LNS command could have been called automatically on drawing load, but I've chosen to keep it as a command, to allow more control (and to allow the user to re-analyze the drawing, should the numbering for some reason get out of sync).
So why did I choose to implement an analysis command, rather than storing the numbering information in the drawing? Mainly because the quantity of numbered blocks in any real-world (even highly complex) drawing is unlikely to be unmanageable (and therefore slow to analyze), so going to the effort of storing the list in XRecords in the DWG seems like overkill. We would - in any case - want to have something like the LNS command to allow existing drawings to be managed by this system. Seralizing to the drawing is always an option, of course, should your users provide feedback that this approach is innefficient.
The remainder of the changes are for the numbering system itself, the NumberedObjectManager (lines 543-721). As mentioned in the last post, this class has been kept as generic as possible, so it can be used for objects other than blocks, for instance. At the core of the class are two lists:
- m_ids - a list of ObjectIds, which is the list of numbered objects
- m_free - a list of free positions (integers) in the object list, which is useful for us to know the gaps in the list created by object deletion, etc.
An alternative implementation would have been to maintain a "map" between list positions and objects, but using two simple lists makes life easier in certain ways: if we want to delete a number we can simply set its value to ObjectId.Null and add its position to the "free" list, and if we want to move an item in the list we can remove it and insert it elsewhere - the other objects simply shift automatically (although they will need to be updated to reflect their new number, of course - more on this in the next post).
The current implementation fills gaps in the list when we number a new object, but we could very easily adjust the code to ignore those gaps and add numbers at the end.
So let's see what happens when we run the LNS command on the drawing we created in the last post. LNS does nothing very visible - although as we previously started the numbering at 1, it does ask us whether we want to rebase the list. To understand how the numbering system works, let's call LNS and DMP twice, and choosing a different re-basing option each time:
Command: LNS
Lowest index is 1. Make this the start of the list? [Yes/No] <Yes>: No
Command: DMP
Idx ObjectId
0 (0)
1 (2129683752)
2 (2129683776)
3 (2129683800)
4 (2129683824)
5 (2129683848)
6 (2129683872)
7 (2129683896)
8 (2129683920)
9 (2129683944)
10 (2129683968)
11 (2129683992)
12 (2129684016)
13 (2129684040)
14 (2129684064)
15 (2129684088)
16 (2129684112)
17 (2129684136)
18 (2129684160)
19 (2129684184)
20 (2129684208)
Free list: 0
Command: LNS
Lowest index is 1. Make this the start of the list? [Yes/No] <Yes>: Yes
Command: DMP
Idx ObjectId
0 (2129683752)
1 (2129683776)
2 (2129683800)
3 (2129683824)
4 (2129683848)
5 (2129683872)
6 (2129683896)
7 (2129683920)
8 (2129683944)
9 (2129683968)
10 (2129683992)
11 (2129684016)
12 (2129684040)
13 (2129684064)
14 (2129684088)
15 (2129684112)
16 (2129684136)
17 (2129684160)
18 (2129684184)
19 (2129684208)
We can see that the first time we run the LNS command, choosing not to re-base the list, we start the list at 0 and have a free slot at that position. If we start adding more numbered blocks, using the BIC or BAP commands, the system will start by numbering an item with 0 before carrying on at 21, 22, etc.
The second time we run the command, we do re-base the list, which means that our m_baseNumber variable will be set to 1 and will be added each time we map the internal list to what is numbered in the drawing.
That's it for today - in the next post we'll look at some more interesting usage of the numbering system, where we highlight, delete and move items in the list, as well as showing how to fill any gaps automatically.