An automatic numbering system for AutoCAD blocks using .NET - Part 4
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<

Atom