- using Autodesk.AutoCAD.ApplicationServices;using Autodesk.AutoCAD.Runtime;using Autodesk.AutoCAD.DatabaseServices;using Autodesk.AutoCAD.EditorInput;using Autodesk.AutoCAD.Geometry;using System.Collections.Generic;using System.Collections;namespace AutoNumberedBubbles{ public class Commands : IExtensionApplication { // Strings identifying the block // and the attribute name to use const string blockName = "BUBBLE"; const string attbName = "NUMBER"; // A string to identify our application's // data in per-document UserData const string dataKey = "TTIFBubbles"; // Define a class for our custom data public class BubbleData { // A separate object to manage our numbering private NumberedObjectManager m_nom; public NumberedObjectManager Nom { get { return m_nom; } } // A "base" index (for the start of the list) private int m_baseNumber; public int BaseNumber { get { return m_baseNumber; } set { m_baseNumber = value; } } // A list of blocks added to the database // which we will then renumber private List m_blocksAdded; public List BlocksToRenumber { get { return m_blocksAdded; } } // Constructor public BubbleData() { m_baseNumber = 0; m_nom = new NumberedObjectManager(); m_blocksAdded = new List(); } // Method to clear the contents public void Reset() { m_baseNumber = 0; m_nom.Clear(); m_blocksAdded.Clear(); } } // Constructor public Commands() { } // Functions called on initialization & termination public void Initialize() { try { DocumentCollection dm = Application.DocumentManager; Document doc = dm.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; ed.WriteMessage( "\nLNS Load numbering settings by analyzing the current drawing" + "\nDMP Print internal numbering information" + "\nBAP Create bubbles at points" + "\nBIC Create bubbles at the center of circles" + "\nMB Move a bubble in the list" + "\nDB Delete a bubble" + "\nRBS Reorder the bubbles, to close gaps caused by deletion" + "\nHLB Highlight a particular bubble" ); // Hook into some events, to detect and renumber // blocks added to the database db.ObjectAppended += new ObjectEventHandler( db_ObjectAppended ); dm.DocumentCreated += new DocumentCollectionEventHandler( dm_DocumentCreated ); dm.DocumentLockModeWillChange += new DocumentLockModeWillChangeEventHandler( dm_DocumentLockModeWillChange ); doc.CommandEnded += delegate(object sender, CommandEventArgs e) { if (e.GlobalCommandName == "UNDO" || e.GlobalCommandName == "U") { ed.WriteMessage( "\nUndo invalidates bubble numbering: call" + " LNS to reload the numbers for this drawing" ); GetBubbleData((Document)sender).Reset(); } }; } catch { } } public void Terminate() { } // Method to retrieve (or create) the // BubbleData object for a particular // document private BubbleData GetBubbleData(Document doc) { Hashtable ud = doc.UserData; BubbleData bd = ud[dataKey] as BubbleData; if (bd == null) { object obj = ud[dataKey]; if (obj == null) { // Nothing there bd = new BubbleData(); ud.Add(dataKey, bd); } else { // Found something different instead Editor ed = doc.Editor; ed.WriteMessage( "Found an object of type "" + obj.GetType().ToString() + "" instead of BubbleData."); } } return bd; } // Do the same for a particular database private BubbleData GetBubbleData(Database db) { DocumentCollection dm = Application.DocumentManager; Document doc = dm.GetDocument(db); return GetBubbleData(doc); } // When a new document is created, attach our // ObjectAppended event handler to the new // database void dm_DocumentCreated( object sender, DocumentCollectionEventArgs e ) { e.Document.Database.ObjectAppended += new ObjectEventHandler( db_ObjectAppended ); } // When an object is appended to a database, // add it to a list we care about if it's a // BlockReference void db_ObjectAppended( object sender, ObjectEventArgs e ) { BlockReference br = e.DBObject as BlockReference; if (br != null) { BubbleData bd = GetBubbleData(e.DBObject.Database); bd.BlocksToRenumber.Add(br.ObjectId); } } // When the command (or action) is over, // take the list of blocks to renumber and // go through them, renumbering each one void dm_DocumentLockModeWillChange( object sender, DocumentLockModeWillChangeEventArgs e ) { Document doc = e.Document; BubbleData bd = GetBubbleData(doc); if (bd.BlocksToRenumber.Count > 0) { Database db = doc.Database; Transaction tr = db.TransactionManager.StartTransaction(); using (tr) { foreach (ObjectId bid in bd.BlocksToRenumber) { try { BlockReference br = tr.GetObject(bid, OpenMode.ForRead) as BlockReference; if (br != null) { BlockTableRecord btr = (BlockTableRecord)tr.GetObject( br.BlockTableRecord, OpenMode.ForRead ); if (btr.Name == blockName) { AttributeCollection ac = br.AttributeCollection; foreach (ObjectId aid in ac) { DBObject obj = tr.GetObject(aid, OpenMode.ForRead); AttributeReference ar = obj as AttributeReference; if (ar.Tag == attbName) { // Change the one we care about ar.UpgradeOpen(); int bubbleNumber = bd.BaseNumber + bd.Nom.NextObjectNumber(bid); ar.TextString = bubbleNumber.ToString(); break; } } } } } catch { } } tr.Commit(); bd.BlocksToRenumber.Clear(); } } } // Command to extract and display information // about the internal numbering [CommandMethod("DMP")] public void DumpNumberingInformation() { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; BubbleData bd = GetBubbleData(doc); bd.Nom.DumpInfo(ed); } // Command to analyze the current document and // understand which indeces have been used and // which are currently free [CommandMethod("LNS")] public void LoadNumberingSettings() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; BubbleData bd = GetBubbleData(doc); // We need to clear any internal state // already collected bd.Reset(); // Select all the blocks in the current drawing TypedValue[] tvs = new TypedValue[1] { new TypedValue( (int)DxfCode.Start, "INSERT" ) }; SelectionFilter sf = new SelectionFilter(tvs); PromptSelectionResult psr = ed.SelectAll(sf); // If it succeeded and we have some blocks... if (psr.Status == PromptStatus.OK && psr.Value.Count > 0) { Transaction tr = db.TransactionManager.StartTransaction(); using (tr) { // First get the modelspace and the ID // of the block for which we're searching BlockTableRecord ms; ObjectId blockId; if (GetBlock( db, tr, out ms, out blockId )) { // For each block reference in the drawing... foreach (SelectedObject o in psr.Value) { DBObject obj = tr.GetObject(o.ObjectId, OpenMode.ForRead); BlockReference br = obj as BlockReference; if (br != null) { // If it's the one we care about... if (br.BlockTableRecord == blockId) { // Check its attribute references... int pos = -1; AttributeCollection ac = br.AttributeCollection; foreach (ObjectId id in ac) { DBObject obj2 = tr.GetObject(id, OpenMode.ForRead); AttributeReference ar = obj2 as AttributeReference; // When we find the attribute // we care about... if (ar.Tag == attbName) { try { // Attempt to extract the number from // the text string property... use a // try-catch block just in case it is // non-numeric pos = int.Parse(ar.TextString); // Add the object at the appropriate // index bd.Nom.NumberObject( o.ObjectId, pos, false, true ); } catch { } } } } } } } tr.Commit(); } // Once we have analyzed all the block references... int start = bd.Nom.GetLowerBound(true); // If the first index is non-zero, ask the user if // they want to rebase the list to begin at the // current start position if (start > 0) { ed.WriteMessage( "\nLowest index is {0}. ", start ); PromptKeywordOptions pko = new PromptKeywordOptions( "Make this the start of the list?" ); pko.AllowNone = true; pko.Keywords.Add("Yes"); pko.Keywords.Add("No"); pko.Keywords.Default = "Yes"; PromptResult pkr = ed.GetKeywords(pko); if (pkr.Status != PromptStatus.OK) bd.Reset(); else { if (pkr.StringResult == "Yes") { // We store our own base number // (the object used to manage objects // always uses zero-based indeces) bd.BaseNumber = start; bd.Nom.RebaseList(bd.BaseNumber); } } } // We found duplicates in the numbering... if (bd.Nom.HasDuplicates()) { // Ask how to fix the duplicates PromptKeywordOptions pko = new PromptKeywordOptions( "Blocks contain duplicate numbers. " + "How do you want to renumber?" ); pko.AllowNone = true; pko.Keywords.Add("Automatically"); pko.Keywords.Add("Individually"); pko.Keywords.Add("Not"); pko.Keywords.Default = "Automatically"; PromptResult pkr = ed.GetKeywords(pko); bool bAuto = false; bool bManual = false; if (pkr.Status != PromptStatus.OK) bd.Reset(); else { if (pkr.StringResult == "Automatically") bAuto = true; else if (pkr.StringResult == "Individually") bManual = true; // Whether fixing automatically or manually // we will iterate through the duplicate list if (bAuto || bManual) { ObjectIdCollection idc = new ObjectIdCollection(); // Get each entry in the duplicate list SortedDictionary[i]> dups = bd.Nom.Duplicates; foreach ( KeyValuePair[i]> dup in dups ) { // The position is the key in the entry // and the list of IDs is the value // (we take a copy, so we can modify it // without affecting the original) int pos = dup.Key; List ids = new List(dup.Value); // For automatic renumbering there's no // user interaction if (bAuto) { foreach (ObjectId id in ids) { bd.Nom.NextObjectNumber(id); idc.Add(id); } } else // bManual { // For manual renumbering we ask the user // to select the block to keep, then // we renumber the rest automatically ed.UpdateScreen(); ids.Add(bd.Nom.GetObjectId(pos)); HighlightBubbles(db, ids, true); ed.WriteMessage( "\n\nHighlighted blocks " + "with number {0}. ", pos + bd.BaseNumber ); bool finished = false; while (!finished) { PromptEntityOptions peo = new PromptEntityOptions( "Select block to keep (others " + "will be renumbered automatically): " ); peo.SetRejectMessage( "\nEntity must be a block." ); peo.AddAllowedClass( typeof(BlockReference), false); PromptEntityResult per = ed.GetEntity(peo); if (per.Status != PromptStatus.OK) { bd.Reset(); return; } else { // A block has been selected, so we // make sure it is one of the ones // we highlighted for the user if (ids.Contains(per.ObjectId)) { // Leave the selected block alone // by removing it from the list ids.Remove(per.ObjectId); // We then renumber each block in // the list foreach (ObjectId id in ids) { bd.Nom.NextObjectNumber(id); idc.Add(id); } RenumberBubbles(db, idc); idc.Clear(); // Let's unhighlight our selected // block (renumbering will do this // for the others) List redraw = new List(1); redraw.Add(per.ObjectId); HighlightBubbles(db, redraw, false); finished = true; } else { ed.WriteMessage( "\nBlock selected is not " + "numbered with {0}. ", pos + bd.BaseNumber ); } } } } } RenumberBubbles(db, idc); } bd.Nom.Duplicates.Clear(); ed.UpdateScreen(); } } } } // Take a list of objects and either highlight // or unhighlight them, depending on the flag private void HighlightBubbles( Database db, List ids, bool highlight) { Transaction tr = db.TransactionManager.StartTransaction(); using (tr) { foreach (ObjectId id in ids) { Entity ent = (Entity)tr.GetObject( id, OpenMode.ForRead ); if (highlight) ent.Highlight(); else ent.Draw(); } tr.Commit(); } } // Command to create bubbles at points selected // by the user - loops until cancelled [CommandMethod("BAP")] public void BubblesAtPoints() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; Autodesk.AutoCAD.ApplicationServices. TransactionManager tm = doc.TransactionManager; Transaction tr = tm.StartTransaction(); using (tr) { // Get the information about the block // and attribute definitions we care about BlockTableRecord ms; ObjectId blockId; AttributeDefinition ad; List other; if (GetBlock( db, tr, out ms, out blockId )) { GetBlockAttributes( tr, blockId, out ad, out other ); // By default the modelspace is returned to // us in read-only state ms.UpgradeOpen(); // Loop until cancelled bool finished = false; while (!finished) { PromptPointOptions ppo = new PromptPointOptions("\nSelect point: "); ppo.AllowNone = true; PromptPointResult ppr = ed.GetPoint(ppo); if (ppr.Status != PromptStatus.OK) finished = true; else // Call a function to create our bubble CreateNumberedBubbleAtPoint( db, ms, tr, ppr.Value, blockId, ad, other ); tm.QueueForGraphicsFlush(); tm.FlushGraphics(); } } tr.Commit(); } } // Command to create a bubble at the center of // each of the selected circles [CommandMethod("BIC")] public void BubblesInCircles() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; // Allow the user to select circles TypedValue[] tvs = new TypedValue[1] { new TypedValue( (int)DxfCode.Start, "CIRCLE" ) }; SelectionFilter sf = new SelectionFilter(tvs); PromptSelectionResult psr = ed.GetSelection(sf); if (psr.Status == PromptStatus.OK && psr.Value.Count > 0) { Transaction tr = db.TransactionManager.StartTransaction(); using (tr) { // Get the information about the block // and attribute definitions we care about BlockTableRecord ms; ObjectId blockId; AttributeDefinition ad; List other; if (GetBlock( db, tr, out ms, out blockId )) { GetBlockAttributes( tr, blockId, out ad, out other ); // By default the modelspace is returned to // us in read-only state ms.UpgradeOpen(); foreach (SelectedObject o in psr.Value) { // For each circle in the selected list... DBObject obj = tr.GetObject(o.ObjectId, OpenMode.ForRead); Circle c = obj as Circle; if (c == null) ed.WriteMessage( "\nObject selected is not a circle." ); else // Call our numbering function, passing the // center of the circle CreateNumberedBubbleAtPoint( db, ms, tr, c.Center, blockId, ad, other ); } } tr.Commit(); } } } // Command to delete a particular bubble // selected by its index [CommandMethod("MB")] public void MoveBubble() { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; BubbleData bd = GetBubbleData(doc); // Use a helper function to select a valid bubble index int pos = GetBubbleNumber( ed, bd, "\nEnter number of bubble to move: " ); if (pos >= bd.BaseNumber) { int from = pos - bd.BaseNumber; pos = GetBubbleNumber( ed, bd, "\nEnter destination position: " ); if (pos >= bd.BaseNumber) { int to = pos - bd.BaseNumber; ObjectIdCollection ids = bd.Nom.MoveObject(from, to); RenumberBubbles(doc.Database, ids); } } } // Command to delete a particular bubbler, // selected by its index [CommandMethod("DB")] public void DeleteBubble() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; BubbleData bd = GetBubbleData(doc); // Use a helper function to select a valid bubble index int pos = GetBubbleNumber( ed, bd, "\nEnter number of bubble to erase: " ); if (pos >= bd.BaseNumber) { // Remove the object from the internal list // (this returns the ObjectId stored for it, // which we can then use to erase the entity) ObjectId id = bd.Nom.RemoveObject(pos - bd.BaseNumber); Transaction tr = db.TransactionManager.StartTransaction(); using (tr) { DBObject obj = tr.GetObject(id, OpenMode.ForWrite); obj.Erase(); tr.Commit(); } } } // Command to reorder all the bubbles in the drawing, // closing all the gaps between numbers but maintaining // the current numbering order [CommandMethod("RBS")] public void ReorderBubbles() { Document doc = Application.DocumentManager.MdiActiveDocument; BubbleData bd = GetBubbleData(doc); // Re-order the bubbles - the IDs returned are // of the objects that need to be renumbered ObjectIdCollection ids = bd.Nom.ReorderObjects(); RenumberBubbles(doc.Database, ids); } // Command to highlight a particular bubble [CommandMethod("HLB")] public void HighlightBubble() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; BubbleData bd = GetBubbleData(doc); // Use our function to select a valid bubble index int pos = GetBubbleNumber( ed, bd, "\nEnter number of bubble to highlight: " ); if (pos >= bd.BaseNumber) { ObjectId id = bd.Nom.GetObjectId(pos - bd.BaseNumber); if (id == ObjectId.Null) { ed.WriteMessage( "\nNumber is not currently used -" + " nothing to highlight." ); return; } List ids = new List(1); ids.Add(id); HighlightBubbles(db, ids, true); } } // Internal helper function to open and retrieve // the model-space and the block def we care about private bool GetBlock( Database db, Transaction tr, out BlockTableRecord ms, out ObjectId blockId ) { BlockTable bt = (BlockTable)tr.GetObject( db.BlockTableId, OpenMode.ForRead ); if (!bt.Has(blockName)) { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; ed.WriteMessage( "\nCannot find block definition "" + blockName + "" in the current drawing." ); blockId = ObjectId.Null; ms = null; return false; } ms = (BlockTableRecord)tr.GetObject( bt[BlockTableRecord.ModelSpace], OpenMode.ForRead ); blockId = bt[blockName]; return true; } // Internal helper function to retrieve // attribute info from our block // (we return the main attribute def // and then all the "others") private void GetBlockAttributes( Transaction tr, ObjectId blockId, out AttributeDefinition ad, out List other ) { BlockTableRecord blk = (BlockTableRecord)tr.GetObject( blockId, OpenMode.ForRead ); ad = null; other = new List(); foreach (ObjectId attId in blk) { DBObject obj = (DBObject)tr.GetObject( attId, OpenMode.ForRead ); AttributeDefinition ad2 = obj as AttributeDefinition; if (ad2 != null) { if (ad2.Tag == attbName) { if (ad2.Constant) { Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; ed.WriteMessage( "\nAttribute to change is constant!" ); } else ad = ad2; } else if (!ad2.Constant) other.Add(ad2); } } } // Internal helper function to create a bubble // at a particular point private Entity CreateNumberedBubbleAtPoint( Database db, BlockTableRecord btr, Transaction tr, Point3d pt, ObjectId blockId, AttributeDefinition ad, List other ) { BubbleData bd = GetBubbleData(db); // Create a new block reference BlockReference br = new BlockReference(pt, blockId); // Add it to the database br.SetDatabaseDefaults(); ObjectId blockRefId = btr.AppendEntity(br); tr.AddNewlyCreatedDBObject(br, true); // Create an attribute reference for our main // attribute definition (where we'll put the // bubble's number) AttributeReference ar = new AttributeReference(); // Add it to the database, and set its position, etc. ar.SetDatabaseDefaults(); ar.SetAttributeFromBlock(ad, br.BlockTransform); ar.Position = ad.Position.TransformBy(br.BlockTransform); ar.Tag = ad.Tag; // Set the bubble's number int bubbleNumber = bd.BaseNumber + bd.Nom.NextObjectNumber(blockRefId); ar.TextString = bubbleNumber.ToString(); ar.AdjustAlignment(db); // Add the attribute to the block reference br.AttributeCollection.AppendAttribute(ar); tr.AddNewlyCreatedDBObject(ar, true); // Now we add attribute references for the // other attribute definitions foreach (AttributeDefinition ad2 in other) { AttributeReference ar2 = new AttributeReference(); ar2.SetAttributeFromBlock(ad2, br.BlockTransform); ar2.Position = ad2.Position.TransformBy(br.BlockTransform); ar2.Tag = ad2.Tag; ar2.TextString = ad2.TextString; ar2.AdjustAlignment(db); br.AttributeCollection.AppendAttribute(ar2); tr.AddNewlyCreatedDBObject(ar2, true); } return br; } // Internal helper function to have the user // select a valid bubble index private int GetBubbleNumber( Editor ed, BubbleData bd, string prompt ) { int upper = bd.Nom.GetUpperBound(); if (upper m_ids; // A list of free positions in the above list // (allows numbering gaps) private List[i] m_free; // A map of duplicates - blocks detected with // the number of an existing block private SortedDictionary[i]> m_dups; public SortedDictionary[i]> Duplicates { get { return m_dups; } } // Constructor public NumberedObjectManager() { m_ids = new List(); m_free = new List[i](); m_dups = new SortedDictionary[i]>(); } // Clear the internal lists public void Clear() { m_ids.Clear(); m_free.Clear(); m_dups.Clear(); } // Does the duplicate list contain anything? public bool HasDuplicates() { return m_dups.Count > 0; } // Return the first entry in the ObjectId list // (specify "true" if you want to skip // any null object IDs) public int GetLowerBound(bool ignoreNull) { if (ignoreNull) // Define an in-line predicate to check // whether an ObjectId is null return m_ids.FindIndex( delegate(ObjectId id) { return id != ObjectId.Null; } ); else return 0; } // Return the last entry in the ObjectId list public int GetUpperBound() { return m_ids.Count - 1; } // Store the specified ObjectId in the next // available location in the list, and return // what that is public int NextObjectNumber(ObjectId id) { int pos; if (m_free.Count > 0) { // Get the first free position, then remove // it from the "free" list pos = m_free[0]; m_free.RemoveAt(0); m_ids[pos] = id; } else { // There are no free slots (gaps in the numbering) // so we append it to the list pos = m_ids.Count; m_ids.Add(id); } return pos; } // Go through the list of objects and close any gaps // by shuffling the list down (easy, as we're using a // List rather than an array) public ObjectIdCollection ReorderObjects() { // Create a collection of ObjectIds we'll return // for the caller to go and update // (so the renumbering will gets reflected // in the objects themselves) ObjectIdCollection ids = new ObjectIdCollection(); // We'll go through the "free" list backwards, // to allow any changes made to the list of // objects to not affect what we're doing List[i] rev = new List[i](m_free); rev.Reverse(); foreach (int pos in rev) { // First we remove the object at the "free" // position (in theory this should be set to // ObjectId.Null, as the slot has been marked // as blank) m_ids.RemoveAt(pos); // Now we go through and add the IDs of any // affected objects to the list to return for (int i = pos; i ids; if (m_dups.ContainsKey(index)) { ids = m_dups[index]; m_dups.Remove(index); } else ids = new List(); ids.Add(id); m_dups.Add(index, ids); } } } } else { // If we're appending, shuffling is irrelevant, // but we may need to add additional "free" slots // if the position comes after the end while (m_ids.Count 0) { ed.WriteMessage("\nIdx ObjectId"); int index = 0; foreach (ObjectId id in m_ids) ed.WriteMessage("\n{0} {1}", index++, id); } if (m_free.Count > 0) { ed.WriteMessage("\n\nFree list: "); foreach (int pos in m_free) ed.WriteMessage("{0} ", pos); } if (HasDuplicates()) { ed.WriteMessage("\n\nDuplicate list: "); foreach ( KeyValuePair[i]> dup in m_dups ) { int pos = dup.Key; List ids = dup.Value; ed.WriteMessage("\n{0} ", pos); foreach (ObjectId id in ids) { ed.WriteMessage("{0} ", id); } } } } // Remove the initial n items from the list public void RebaseList(int start) { // First we remove the ObjectIds for (int i=0; i . KeyCollection kc = m_dups.Keys; // Copy the KeyCollection into a list of ints, // to make it easier to iterate // (this also allows us to modify the m_dups // object while we're iterating) List[i] idxs = new List[i](kc.Count); foreach (int pos in kc) idxs.Add(pos); foreach (int pos in idxs) { List ids; m_dups.TryGetValue(pos, out ids); if (m_dups.ContainsKey(pos - start)) throw new Exception( ErrorStatus.DuplicateKey, "\nClash detected - " + "duplicate list may be corrupted." ); else { // Remove the old entry and add the new one m_dups.Remove(pos); m_dups.Add(pos - start, ids); } } } } }}
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. :-)