明经CAD社区

 找回密码
 注册

QQ登录

只需一步,快速开始

搜索
查看: 3805|回复: 2

[Kean专集] Kean-An automatic numbering system for AutoCAD blocks using .NET

 关闭 [复制链接]
发表于 2009-5-15 15:05:00 | 显示全部楼层 |阅读模式
三、一个图块的自动计数系统
May 07, 2008
An automatic numbering system for AutoCAD blocks using .NET - Part 1
I had this interesting request come in by email:
I have a problem where I have a block I need to insert into several parts of a drawing. The block has three attributes, 1. Point number, 2. Level, and 3. Code.
I would like to do the following;
1. be able to select objects in a drawing and have it insert the block and autosnap to circle centres, and auto number each point number attribute.
2. be able to manually select points which are automatically numbered.
3. it should remember the last point number.
4. have the option to renumber points if changes are made i could renumber all the points.
5. be able to extract the points & attributes, point number, x position, y position, level, & code to a comma delimited file.
The request was more for guidance than for custom development services, but I thought the topic was of general-enough interest to be worth spending some time working on.
Over the next few posts I'm going to serialize the implementation I put together, in more-or-less the order in which I implemented it. If further related enhancement requests come in - as comments or by email - then I'll do what I can to incorporate them into the solution (no guarantees, though :-).
Today's post looks at the first two items: automatic numbering of selected circles and manual selection of points that also get numbered.
During these posts (and in the code), I'll refer to these numbered blocks as "bubbles", as this implementation could be used to implement automatically numbered bubble (or balloon) dimensions. As you'll see in the next few posts, though, I've tried to keep the core number management implementation as independent as possible, so it could be used to manage a numbered list of any type of AutoCAD entity.
So, here's the initial C# code:
  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. namespace AutoNumberedBubbles
  8. {
  9.   public class Commands : IExtensionApplication
  10.   {
  11.     // Strings identifying the block
  12.     // and the attribute name to use
  13.     const string blockName = "BUBBLE";
  14.     const string attbName = "NUMBER";
  15.     // In this version, just use a simple
  16.     // integer to take care of the numbering
  17.     private int m_bulletNumber = 0;
  18.     // Constructor
  19.     public Commands()
  20.     {
  21.     }
  22.     // Functions called on initialization & termination
  23.     public void Initialize()
  24.     {
  25.       try
  26.       {
  27.         Document doc =
  28.           Application.DocumentManager.MdiActiveDocument;
  29.         Editor ed = doc.Editor;
  30.         ed.WriteMessage(
  31.           "\nBAP  Create bubbles at points" +
  32.           "\nBIC  Create bubbles at the center of circles"
  33.         );
  34.       }
  35.       catch
  36.       { }
  37.     }
  38.     public void Terminate()
  39.     {
  40.     }
  41.     // Command to create bubbles at points selected
  42.     // by the user - loops until cancelled
  43.     [CommandMethod("BAP")]
  44.     public void BubblesAtPoints()
  45.     {
  46.       Document doc =
  47.         Application.DocumentManager.MdiActiveDocument;
  48.       Database db = doc.Database;
  49.       Editor ed = doc.Editor;
  50.       Autodesk.AutoCAD.ApplicationServices.
  51.       TransactionManager tm =
  52.         doc.TransactionManager;
  53.       Transaction tr =
  54.         tm.StartTransaction();
  55.       using (tr)
  56.       {
  57.         // Get the information about the block
  58.         // and attribute definitions we care about
  59.         BlockTableRecord ms;
  60.         ObjectId blockId;
  61.         AttributeDefinition ad;
  62.         List<AttributeDefinition> other;
  63.         if (GetBlock(
  64.               db, tr, out ms, out blockId
  65.           ))
  66.         {
  67.           GetBlockAttributes(
  68.             tr, blockId, out ad, out other
  69.           );
  70.           // By default the modelspace is returned to
  71.           // us in read-only state
  72.           ms.UpgradeOpen();
  73.           // Loop until cancelled
  74.           bool finished = false;
  75.           while (!finished)
  76.           {
  77.             PromptPointOptions ppo =
  78.               new PromptPointOptions("\nSelect point: ");
  79.             ppo.AllowNone = true;
  80.             PromptPointResult ppr =
  81.               ed.GetPoint(ppo);
  82.             if (ppr.Status != PromptStatus.OK)
  83.               finished = true;
  84.             else
  85.               // Call a function to create our bubble
  86.               CreateNumberedBubbleAtPoint(
  87.                 db, ms, tr, ppr.Value,
  88.                 blockId, ad, other
  89.               );
  90.             tm.QueueForGraphicsFlush();
  91.             tm.FlushGraphics();
  92.           }
  93.         }
  94.         tr.Commit();
  95.       }
  96.     }
  97.     // Command to create a bubble at the center of
  98.     // each of the selected circles
  99.     [CommandMethod("BIC")]
  100.     public void BubblesInCircles()
  101.     {
  102.       Document doc =
  103.         Application.DocumentManager.MdiActiveDocument;
  104.       Database db = doc.Database;
  105.       Editor ed = doc.Editor;
  106.       // Allow the user to select circles
  107.       TypedValue[] tvs =
  108.         new TypedValue[1] {
  109.             new TypedValue(
  110.               (int)DxfCode.Start,
  111.               "CIRCLE"
  112.             )
  113.           };
  114.       SelectionFilter sf =
  115.         new SelectionFilter(tvs);
  116.       PromptSelectionResult psr =
  117.         ed.GetSelection(sf);
  118.       if (psr.Status == PromptStatus.OK &&
  119.           psr.Value.Count > 0)
  120.       {
  121.         Transaction tr =
  122.           db.TransactionManager.StartTransaction();
  123.         using (tr)
  124.         {
  125.           // Get the information about the block
  126.           // and attribute definitions we care about
  127.           BlockTableRecord ms;
  128.           ObjectId blockId;
  129.           AttributeDefinition ad;
  130.           List<AttributeDefinition> other;
  131.           if (GetBlock(
  132.                 db, tr, out ms, out blockId
  133.             ))
  134.           {
  135.             GetBlockAttributes(
  136.               tr, blockId, out ad, out other
  137.             );
  138.             // By default the modelspace is returned to
  139.             // us in read-only state
  140.             ms.UpgradeOpen();
  141.             foreach (SelectedObject o in psr.Value)
  142.             {
  143.               // For each circle in the selected list...
  144.               DBObject obj =
  145.                 tr.GetObject(o.ObjectId, OpenMode.ForRead);
  146.               Circle c = obj as Circle;
  147.               if (c == null)
  148.                 ed.WriteMessage(
  149.                   "\nObject selected is not a circle."
  150.                 );
  151.               else
  152.                 // Call our numbering function, passing the
  153.                 // center of the circle
  154.                 CreateNumberedBubbleAtPoint(
  155.                   db, ms, tr, c.Center,
  156.                   blockId, ad, other
  157.                 );
  158.             }
  159.           }
  160.           tr.Commit();
  161.         }
  162.       }
  163.     }
  164.     // Internal helper function to open and retrieve
  165.     // the model-space and the block def we care about
  166.     private bool
  167.       GetBlock(
  168.         Database db,
  169.         Transaction tr,
  170.         out BlockTableRecord ms,
  171.         out ObjectId blockId
  172.       )
  173.     {
  174.       BlockTable bt =
  175.         (BlockTable)tr.GetObject(
  176.           db.BlockTableId,
  177.           OpenMode.ForRead
  178.         );
  179.       if (!bt.Has(blockName))
  180.       {
  181.         Document doc =
  182.           Application.DocumentManager.MdiActiveDocument;
  183.         Editor ed = doc.Editor;
  184.         ed.WriteMessage(
  185.           "\nCannot find block definition "" +
  186.           blockName +
  187.           "" in the current drawing."
  188.         );
  189.         blockId = ObjectId.Null;
  190.         ms = null;
  191.         return false;
  192.       }
  193.       ms =
  194.         (BlockTableRecord)tr.GetObject(
  195.           bt[BlockTableRecord.ModelSpace],
  196.           OpenMode.ForRead
  197.         );
  198.       blockId = bt[blockName];
  199.       return true;
  200.     }
  201.     // Internal helper function to retrieve
  202.     // attribute info from our block
  203.     // (we return the main attribute def
  204.     // and then all the "others")
  205.     private void
  206.       GetBlockAttributes(
  207.         Transaction tr,
  208.         ObjectId blockId,
  209.         out AttributeDefinition ad,
  210.         out List<AttributeDefinition> other
  211.       )
  212.     {
  213.       BlockTableRecord blk =
  214.         (BlockTableRecord)tr.GetObject(
  215.           blockId,
  216.           OpenMode.ForRead
  217.         );
  218.       ad = null;
  219.       other =
  220.         new List<AttributeDefinition>();
  221.       foreach (ObjectId attId in blk)
  222.       {
  223.         DBObject obj =
  224.           (DBObject)tr.GetObject(
  225.             attId,
  226.             OpenMode.ForRead
  227.           );
  228.         AttributeDefinition ad2 =
  229.           obj as AttributeDefinition;
  230.         if (ad2 != null)
  231.         {
  232.           if (ad2.Tag == attbName)
  233.           {
  234.             if (ad2.Constant)
  235.             {
  236.               Document doc =
  237.                 Application.DocumentManager.MdiActiveDocument;
  238.               Editor ed = doc.Editor;
  239.               ed.WriteMessage(
  240.                 "\nAttribute to change is constant!"
  241.               );
  242.             }
  243.             else
  244.               ad = ad2;
  245.           }
  246.           else
  247.             if (!ad2.Constant)
  248.               other.Add(ad2);
  249.         }
  250.       }
  251.     }
  252.     // Internal helper function to create a bubble
  253.     // at a particular point
  254.     private Entity
  255.       CreateNumberedBubbleAtPoint(
  256.         Database db,
  257.         BlockTableRecord btr,
  258.         Transaction tr,
  259.         Point3d pt,
  260.         ObjectId blockId,
  261.         AttributeDefinition ad,
  262.         List<AttributeDefinition> other
  263.       )
  264.     {
  265.       //  Create a new block reference
  266.       BlockReference br =
  267.         new BlockReference(pt, blockId);
  268.       // Add it to the database
  269.       br.SetDatabaseDefaults();
  270.       ObjectId blockRefId = btr.AppendEntity(br);
  271.       tr.AddNewlyCreatedDBObject(br, true);
  272.       // Create an attribute reference for our main
  273.       // attribute definition (where we'll put the
  274.       // bubble's number)
  275.       AttributeReference ar =
  276.         new AttributeReference();
  277.       // Add it to the database, and set its position, etc.
  278.       ar.SetDatabaseDefaults();
  279.       ar.SetAttributeFromBlock(ad, br.BlockTransform);
  280.       ar.Position =
  281.         ad.Position.TransformBy(br.BlockTransform);
  282.       ar.Tag = ad.Tag;
  283.       // Set the bubble's number
  284.       int bubbleNumber =
  285.         ++m_bulletNumber;
  286.       ar.TextString = bubbleNumber.ToString();
  287.       ar.AdjustAlignment(db);
  288.       // Add the attribute to the block reference
  289.       br.AttributeCollection.AppendAttribute(ar);
  290.       tr.AddNewlyCreatedDBObject(ar, true);
  291.       // Now we add attribute references for the
  292.       // other attribute definitions
  293.       foreach (AttributeDefinition ad2 in other)
  294.       {
  295.         AttributeReference ar2 =
  296.           new AttributeReference();
  297.         ar2.SetAttributeFromBlock(ad2, br.BlockTransform);
  298.         ar2.Position =
  299.           ad2.Position.TransformBy(br.BlockTransform);
  300.         ar2.Tag = ad2.Tag;
  301.         ar2.TextString = ad2.TextString;
  302.         ar2.AdjustAlignment(db);
  303.         br.AttributeCollection.AppendAttribute(ar2);
  304.         tr.AddNewlyCreatedDBObject(ar2, true);
  305.       }
  306.       return br;
  307.     }
  308.   }
  309. }
You can see only two commands have been implemented, for now: BAP (Bubbles At Points) and BIC (Bubbles In Circles). These commands will create bubbles in increasing order, starting at 1. For now there's nothing in the application to allow the numbering to continue when you reload the drawing - it will start again at 1, for now.
Here's a base drawing containing some circles (which you can download from here):
Here's what happens when we run the BIC command, selecting each of the circles, going from left to right:
And here's what happens after we run the BAP command, selecting a number of points at random:

In the next post we'll add some intelligence to the numbering system, by adding a class to take care of a lot of the hard work for us. We'll then implement some commands to use this class to get more control over the numbering.
 楼主| 发表于 2009-5-15 15:17:00 | 显示全部楼层
May 12, 2008
An automatic numbering system for AutoCAD blocks using .NET - Part 3
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:
  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. namespace AutoNumberedBubbles
  8. {
  9.   public class Commands : IExtensionApplication
  10.   {
  11.     // Strings identifying the block
  12.     // and the attribute name to use
  13.     const string blockName = "BUBBLE";
  14.     const string attbName = "NUMBER";
  15.     // We will use a separate object to
  16.     // manage our numbering, and maintain a
  17.     // "base" index (the start of the list)
  18.     private NumberedObjectManager m_nom;
  19.     private int m_baseNumber = 0;
  20.     // Constructor
  21.     public Commands()
  22.     {
  23.       m_nom = new NumberedObjectManager();
  24.     }
  25.     // Functions called on initialization & termination
  26.     public void Initialize()
  27.     {
  28.       try
  29.       {
  30.         Document doc =
  31.           Application.DocumentManager.MdiActiveDocument;
  32.         Editor ed = doc.Editor;
  33.         ed.WriteMessage(
  34.           "\nLNS  Load numbering settings by analyzing the current drawing" +
  35.           "\nDMP  Print internal numbering information" +
  36.           "\nBAP  Create bubbles at points" +
  37.           "\nBIC  Create bubbles at the center of circles" +
  38.           "\nMB   Move a bubble in the list" +
  39.           "\nDB   Delete a bubble" +
  40.           "\nRBS  Reorder the bubbles, to close gaps caused by deletion" +
  41.           "\nHLB  Highlight a particular bubble"
  42.         );
  43.       }
  44.       catch
  45.       { }
  46.     }
  47.     public void Terminate()
  48.     {
  49.     }
  50.     // Command to extract and display information
  51.     // about the internal numbering
  52.     [CommandMethod("DMP")]
  53.     public void DumpNumberingInformation()
  54.     {
  55.       Document doc =
  56.         Application.DocumentManager.MdiActiveDocument;
  57.       Editor ed = doc.Editor;
  58.       m_nom.DumpInfo(ed);
  59.     }
  60.     // Command to analyze the current document and
  61.     // understand which indeces have been used and
  62.     // which are currently free
  63.     [CommandMethod("LNS")]
  64.     public void LoadNumberingSettings()
  65.     {
  66.       Document doc =
  67.         Application.DocumentManager.MdiActiveDocument;
  68.       Database db = doc.Database;
  69.       Editor ed = doc.Editor;
  70.       // We need to clear any internal state
  71.       // already collected
  72.       m_nom.Clear();
  73.       m_baseNumber = 0;
  74.       // Select all the blocks in the current drawing
  75.       TypedValue[] tvs =
  76.         new TypedValue[1] {
  77.             new TypedValue(
  78.               (int)DxfCode.Start,
  79.               "INSERT"
  80.             )
  81.           };
  82.       SelectionFilter sf =
  83.         new SelectionFilter(tvs);
  84.       PromptSelectionResult psr =
  85.         ed.SelectAll(sf);
  86.       // If it succeeded and we have some blocks...
  87.       if (psr.Status == PromptStatus.OK &&
  88.           psr.Value.Count > 0)
  89.       {
  90.         Transaction tr =
  91.           db.TransactionManager.StartTransaction();
  92.         using (tr)
  93.         {
  94.           // First get the modelspace and the ID
  95.           // of the block for which we're searching
  96.           BlockTableRecord ms;
  97.           ObjectId blockId;
  98.           if (GetBlock(
  99.                 db, tr, out ms, out blockId
  100.              ))
  101.           {
  102.             // For each block reference in the drawing...
  103.             foreach (SelectedObject o in psr.Value)
  104.             {
  105.               DBObject obj =
  106.                 tr.GetObject(o.ObjectId, OpenMode.ForRead);
  107.               BlockReference br = obj as BlockReference;
  108.               if (br != null)
  109.               {
  110.                 // If it's the one we care about...
  111.                 if (br.BlockTableRecord == blockId)
  112.                 {
  113.                   // Check its attribute references...
  114.                   int pos = -1;
  115.                   AttributeCollection ac =
  116.                     br.AttributeCollection;
  117.                   foreach (ObjectId id in ac)
  118.                   {
  119.                     DBObject obj2 =
  120.                       tr.GetObject(id, OpenMode.ForRead);
  121.                     AttributeReference ar =
  122.                       obj2 as AttributeReference;
  123.                     // When we find the attribute
  124.                     // we care about...
  125.                     if (ar.Tag == attbName)
  126.                     {
  127.                       try
  128.                       {
  129.                         // Attempt to extract the number from
  130.                         // the text string property... use a
  131.                         // try-catch block just in case it is
  132.                         // non-numeric
  133.                         pos =
  134.                           int.Parse(ar.TextString);
  135.                         // Add the object at the appropriate
  136.                         // index
  137.                         m_nom.NumberObject(
  138.                           o.ObjectId, pos, false
  139.                         );
  140.                       }
  141.                       catch { }
  142.                     }
  143.                   }
  144.                 }
  145.               }
  146.             }
  147.           }
  148.           tr.Commit();
  149.         }
  150.         // Once we have analyzed all the block references...
  151.         int start = m_nom.GetLowerBound(true);
  152.         // If the first index is non-zero, ask the user if
  153.         // they want to rebase the list to begin at the
  154.         // current start position
  155.         if (start > 0)
  156.         {
  157.           ed.WriteMessage(
  158.             "\nLowest index is {0}. ",
  159.             start
  160.           );
  161.           PromptKeywordOptions pko =
  162.             new PromptKeywordOptions(
  163.               "Make this the start of the list?"
  164.             );
  165.           pko.AllowNone = true;
  166.           pko.Keywords.Add("Yes");
  167.           pko.Keywords.Add("No");
  168.           pko.Keywords.Default = "Yes";
  169.           PromptResult pkr =
  170.             ed.GetKeywords(pko);
  171.           if (pkr.Status == PromptStatus.OK)
  172.           {
  173.             if (pkr.StringResult == "Yes")
  174.             {
  175.               // We store our own base number
  176.               // (the object used to manage objects
  177.               // always uses zero-based indeces)
  178.               m_baseNumber = start;
  179.               m_nom.RebaseList(m_baseNumber);
  180.             }
  181.           }
  182.         }
  183.       }
  184.     }
  185.    
  186.     // Command to create bubbles at points selected
  187.     // by the user - loops until cancelled
  188.     [CommandMethod("BAP")]
  189.     public void BubblesAtPoints()
  190.     {
  191.       Document doc =
  192.         Application.DocumentManager.MdiActiveDocument;
  193.       Database db = doc.Database;
  194.       Editor ed = doc.Editor;
  195.       Autodesk.AutoCAD.ApplicationServices.
  196.       TransactionManager tm =
  197.         doc.TransactionManager;
  198.       Transaction tr =
  199.         tm.StartTransaction();
  200.       using (tr)
  201.       {
  202.         // Get the information about the block
  203.         // and attribute definitions we care about
  204.         BlockTableRecord ms;
  205.         ObjectId blockId;
  206.         AttributeDefinition ad;
  207.         List<AttributeDefinition> other;
  208.         if (GetBlock(
  209.               db, tr, out ms, out blockId
  210.            ))
  211.         {
  212.           GetBlockAttributes(
  213.             tr, blockId, out ad, out other
  214.           );
  215.           // By default the modelspace is returned to
  216.           // us in read-only state
  217.           ms.UpgradeOpen();
  218.           // Loop until cancelled
  219.           bool finished = false;
  220.           while (!finished)
  221.           {
  222.             PromptPointOptions ppo =
  223.               new PromptPointOptions("\nSelect point: ");
  224.             ppo.AllowNone = true;
  225.             PromptPointResult ppr =
  226.               ed.GetPoint(ppo);
  227.             if (ppr.Status != PromptStatus.OK)
  228.               finished = true;
  229.             else
  230.               // Call a function to create our bubble
  231.               CreateNumberedBubbleAtPoint(
  232.                 db, ms, tr, ppr.Value,
  233.                 blockId, ad, other
  234.               );
  235.             tm.QueueForGraphicsFlush();
  236.             tm.FlushGraphics();
  237.           }
  238.         }
  239.         tr.Commit();
  240.       }
  241.     }
  242.     // Command to create a bubble at the center of
  243.     // each of the selected circles
  244.     [CommandMethod("BIC")]
  245.     public void BubblesInCircles()
  246.     {
  247.       Document doc =
  248.         Application.DocumentManager.MdiActiveDocument;
  249.       Database db = doc.Database;
  250.       Editor ed = doc.Editor;
  251.       // Allow the user to select circles
  252.       TypedValue[] tvs =
  253.         new TypedValue[1] {
  254.             new TypedValue(
  255.               (int)DxfCode.Start,
  256.               "CIRCLE"
  257.             )
  258.           };
  259.       SelectionFilter sf =
  260.         new SelectionFilter(tvs);
  261.       PromptSelectionResult psr =
  262.         ed.GetSelection(sf);
  263.       if (psr.Status == PromptStatus.OK &&
  264.           psr.Value.Count > 0)
  265.       {
  266.         Transaction tr =
  267.           db.TransactionManager.StartTransaction();
  268.         using (tr)
  269.         {
  270.           // Get the information about the block
  271.           // and attribute definitions we care about
  272.           BlockTableRecord ms;
  273.           ObjectId blockId;
  274.           AttributeDefinition ad;
  275.           List<AttributeDefinition> other;
  276.           if (GetBlock(
  277.                 db, tr, out ms, out blockId
  278.              ))
  279.           {
  280.             GetBlockAttributes(
  281.               tr, blockId, out ad, out other
  282.             );
  283.             // By default the modelspace is returned to
  284.             // us in read-only state
  285.             ms.UpgradeOpen();
  286.             foreach (SelectedObject o in psr.Value)
  287.             {
  288.               // For each circle in the selected list...
  289.               DBObject obj =
  290.                 tr.GetObject(o.ObjectId, OpenMode.ForRead);
  291.               Circle c = obj as Circle;
  292.               if (c == null)
  293.                 ed.WriteMessage(
  294.                   "\nObject selected is not a circle."
  295.                 );
  296.               else
  297.                 // Call our numbering function, passing the
  298.                 // center of the circle
  299.                 CreateNumberedBubbleAtPoint(
  300.                   db, ms, tr, c.Center,
  301.                   blockId, ad, other
  302.                 );
  303.             }
  304.           }
  305.           tr.Commit();
  306.         }
  307.       }
  308.     }
  309.     // Command to delete a particular bubble
  310.     // selected by its index
  311.     [CommandMethod("MB")]
  312.     public void MoveBubble()
  313.     {
  314.       Document doc =
  315.         Application.DocumentManager.MdiActiveDocument;
  316.       Editor ed = doc.Editor;
  317.       // Use a helper function to select a valid bubble index
  318.       int pos =
  319.         GetBubbleNumber(
  320.           ed,
  321.           "\nEnter number of bubble to move: "
  322.         );
  323.       if (pos >= m_baseNumber)
  324.       {
  325.         int from = pos - m_baseNumber;
  326.         pos =
  327.           GetBubbleNumber(
  328.             ed,
  329.             "\nEnter destination position: "
  330.           );
  331.         if (pos >= m_baseNumber)
  332.         {
  333.           int to = pos - m_baseNumber;
  334.           ObjectIdCollection ids =
  335.             m_nom.MoveObject(from, to);
  336.           RenumberBubbles(doc.Database, ids);
  337.         }
  338.       }
  339.     }
  340.     // Command to delete a particular bubbler,
  341.     // selected by its index
  342.     [CommandMethod("DB")]
  343.     public void DeleteBubble()
  344.     {
  345.       Document doc =
  346.         Application.DocumentManager.MdiActiveDocument;
  347.       Database db = doc.Database;
  348.       Editor ed = doc.Editor;
  349.       // Use a helper function to select a valid bubble index
  350.       int pos =
  351.         GetBubbleNumber(
  352.           ed,
  353.           "\nEnter number of bubble to erase: "
  354.         );
  355.       if (pos >= m_baseNumber)
  356.       {
  357.         // Remove the object from the internal list
  358.         // (this returns the ObjectId stored for it,
  359.         // which we can then use to erase the entity)
  360.         ObjectId id =
  361.           m_nom.RemoveObject(pos - m_baseNumber);
  362.         Transaction tr =
  363.           db.TransactionManager.StartTransaction();
  364.         using (tr)
  365.         {
  366.           DBObject obj =
  367.             tr.GetObject(id, OpenMode.ForWrite);
  368.           obj.Erase();
  369.           tr.Commit();
  370.         }
  371.       }
  372.     }
  373.     // Command to reorder all the bubbles in the drawing,
  374.     // closing all the gaps between numbers but maintaining
  375.     // the current numbering order
  376.     [CommandMethod("RBS")]
  377.     public void ReorderBubbles()
  378.     {
  379.       Document doc =
  380.         Application.DocumentManager.MdiActiveDocument;
  381.       // Re-order the bubbles - the IDs returned are
  382.       // of the objects that need to be renumbered
  383.       ObjectIdCollection ids =
  384.         m_nom.ReorderObjects();
  385.       RenumberBubbles(doc.Database, ids);
  386.     }
  387.     // Command to highlight a particular bubble
  388.     [CommandMethod("HLB")]
  389.     public void HighlightBubble()
  390.     {
  391.       Document doc =
  392.         Application.DocumentManager.MdiActiveDocument;
  393.       Database db = doc.Database;
  394.       Editor ed = doc.Editor;
  395.       // Use our function to select a valid bubble index
  396.       int pos =
  397.         GetBubbleNumber(
  398.           ed,
  399.           "\nEnter number of bubble to highlight: "
  400.         );
  401.       if (pos >= m_baseNumber)
  402.       {
  403.         Transaction tr =
  404.           db.TransactionManager.StartTransaction();
  405.         using (tr)
  406.         {
  407.           // Get the ObjectId from the index...
  408.           ObjectId id =
  409.             m_nom.GetObjectId(pos - m_baseNumber);
  410.           if (id == ObjectId.Null)
  411.           {
  412.             ed.WriteMessage(
  413.               "\nNumber is not currently used -" +
  414.               " nothing to highlight."
  415.             );
  416.             return;
  417.           }
  418.           // And then open & highlight the entity
  419.           Entity ent =
  420.             (Entity)tr.GetObject(
  421.               id,
  422.               OpenMode.ForRead
  423.             );
  424.           ent.Highlight();
  425.           tr.Commit();
  426.         }
  427.       }
  428.     }
  429.     // Internal helper function to open and retrieve
  430.     // the model-space and the block def we care about
  431.     private bool
  432.       GetBlock(
  433.         Database db,
  434.         Transaction tr,
  435.         out BlockTableRecord ms,
  436.         out ObjectId blockId
  437.       )
  438.     {
  439.       BlockTable bt =
  440.         (BlockTable)tr.GetObject(
  441.           db.BlockTableId,
  442.           OpenMode.ForRead
  443.         );
  444.       if (!bt.Has(blockName))
  445.       {
  446.         Document doc =
  447.           Application.DocumentManager.MdiActiveDocument;
  448.         Editor ed = doc.Editor;
  449.         ed.WriteMessage(
  450.           "\nCannot find block definition "" +
  451.           blockName +
  452.           "" in the current drawing."
  453.         );
  454.         blockId = ObjectId.Null;
  455.         ms = null;
  456.         return false;
  457.       }
  458.       ms =
  459.         (BlockTableRecord)tr.GetObject(
  460.           bt[BlockTableRecord.ModelSpace],
  461.           OpenMode.ForRead
  462.         );
  463.       blockId = bt[blockName];
  464.       return true;
  465.     }
  466.     // Internal helper function to retrieve
  467.     // attribute info from our block
  468.     // (we return the main attribute def
  469.     // and then all the "others")
  470.     private void
  471.       GetBlockAttributes(
  472.         Transaction tr,
  473.         ObjectId blockId,
  474.         out AttributeDefinition ad,
  475.         out List<AttributeDefinition> other
  476.       )
  477.     {
  478.       BlockTableRecord blk =
  479.         (BlockTableRecord)tr.GetObject(
  480.           blockId,
  481.           OpenMode.ForRead
  482.         );
  483.       ad = null;
  484.       other =
  485.         new List<AttributeDefinition>();
  486.       foreach (ObjectId attId in blk)
  487.       {
  488.         DBObject obj =
  489.           (DBObject)tr.GetObject(
  490.             attId,
  491.             OpenMode.ForRead
  492.           );
  493.         AttributeDefinition ad2 =
  494.           obj as AttributeDefinition;
  495.         if (ad2 != null)
  496.         {
  497.           if (ad2.Tag == attbName)
  498.           {
  499.             if (ad2.Constant)
  500.             {
  501.               Document doc =
  502.                 Application.DocumentManager.MdiActiveDocument;
  503.               Editor ed = doc.Editor;
  504.               ed.WriteMessage(
  505.                 "\nAttribute to change is constant!"
  506.               );
  507.             }
  508.             else
  509.               ad = ad2;
  510.           }
  511.           else
  512.             if (!ad2.Constant)
  513.               other.Add(ad2);
  514.         }
  515.       }
  516.     }
  517.     // Internal helper function to create a bubble
  518.     // at a particular point
  519.     private Entity
  520.       CreateNumberedBubbleAtPoint(
  521.         Database db,
  522.         BlockTableRecord btr,
  523.         Transaction tr,
  524.         Point3d pt,
  525.         ObjectId blockId,
  526.         AttributeDefinition ad,
  527.         List<AttributeDefinition> other
  528.       )
  529.     {
  530.       //  Create a new block reference
  531.       BlockReference br =
  532.         new BlockReference(pt, blockId);
  533.       // Add it to the database
  534.       br.SetDatabaseDefaults();
  535.       ObjectId blockRefId = btr.AppendEntity(br);
  536.       tr.AddNewlyCreatedDBObject(br, true);
  537.       // Create an attribute reference for our main
  538.       // attribute definition (where we'll put the
  539.       // bubble's number)
  540.       AttributeReference ar =
  541.         new AttributeReference();
  542.       // Add it to the database, and set its position, etc.
  543.       ar.SetDatabaseDefaults();
  544.       ar.SetAttributeFromBlock(ad, br.BlockTransform);
  545.       ar.Position =
  546.         ad.Position.TransformBy(br.BlockTransform);
  547.       ar.Tag = ad.Tag;
  548.       // Set the bubble's number
  549.       int bubbleNumber =
  550.         m_baseNumber +
  551.         m_nom.NextObjectNumber(blockRefId);
  552.       ar.TextString = bubbleNumber.ToString();
  553.       ar.AdjustAlignment(db);
  554.       // Add the attribute to the block reference
  555.       br.AttributeCollection.AppendAttribute(ar);
  556.       tr.AddNewlyCreatedDBObject(ar, true);
  557.       // Now we add attribute references for the
  558.       // other attribute definitions
  559.       foreach (AttributeDefinition ad2 in other)
  560.       {
  561.         AttributeReference ar2 =
  562.           new AttributeReference();
  563.         ar2.SetAttributeFromBlock(ad2, br.BlockTransform);
  564.         ar2.Position =
  565.           ad2.Position.TransformBy(br.BlockTransform);
  566.         ar2.Tag = ad2.Tag;
  567.         ar2.TextString = ad2.TextString;
  568.         ar2.AdjustAlignment(db);
  569.         br.AttributeCollection.AppendAttribute(ar2);
  570.         tr.AddNewlyCreatedDBObject(ar2, true);
  571.       }
  572.       return br;
  573.     }
  574.     // Internal helper function to have the user
  575.     // select a valid bubble index
  576.     private int
  577.       GetBubbleNumber(
  578.         Editor ed,
  579.         string prompt
  580.       )
  581.     {
  582.       int upper = m_nom.GetUpperBound();
  583.       if (upper <= 0)
  584.       {
  585.         ed.WriteMessage(
  586.           "\nNo bubbles are currently being managed."
  587.         );
  588.         return upper;
  589.       }
  590.       PromptIntegerOptions pio =
  591.         new PromptIntegerOptions(prompt);
  592.       pio.AllowNone = false;
  593.       // Get the limits from our manager object
  594.       pio.LowerLimit =
  595.         m_baseNumber +
  596.         m_nom.GetLowerBound(false);
  597.       pio.UpperLimit =
  598.         m_baseNumber +
  599.         upper;
  600.       PromptIntegerResult pir =
  601.         ed.GetInteger(pio);
  602.       if (pir.Status == PromptStatus.OK)
  603.         return pir.Value;
  604.       else
  605.         return -1;
  606.     }
  607.     // Internal helper function to open up a list
  608.     // of bubbles and reset their number to the
  609.     // position in the list
  610.     private void RenumberBubbles(
  611.       Database db, ObjectIdCollection ids)
  612.     {
  613.       Transaction tr =
  614.         db.TransactionManager.StartTransaction();
  615.       using (tr)
  616.       {
  617.         // Get the block information
  618.         BlockTableRecord ms;
  619.         ObjectId blockId;
  620.         if (GetBlock(
  621.               db, tr, out ms, out blockId
  622.            ))
  623.         {
  624.           // Open each bubble to be renumbered
  625.           foreach (ObjectId bid in ids)
  626.           {
  627.             if (bid != ObjectId.Null)
  628.             {
  629.               DBObject obj =
  630.                 tr.GetObject(bid, OpenMode.ForRead);
  631.               BlockReference br = obj as BlockReference;
  632.               if (br != null)
  633.               {
  634.                 if (br.BlockTableRecord == blockId)
  635.                 {
  636.                   AttributeCollection ac =
  637.                     br.AttributeCollection;
  638.                   // Go through its attributes
  639.                   foreach (ObjectId aid in ac)
  640.                   {
  641.                     DBObject obj2 =
  642.                       tr.GetObject(aid, OpenMode.ForRead);
  643.                     AttributeReference ar =
  644.                       obj2 as AttributeReference;
  645.                     if (ar.Tag == attbName)
  646.                     {
  647.                       // Change the one we care about
  648.                       ar.UpgradeOpen();
  649.                       int bubbleNumber =
  650.                         m_baseNumber + m_nom.GetNumber(bid);
  651.                       ar.TextString =
  652.                         bubbleNumber.ToString();
  653.                       break;
  654.                     }
  655.                   }
  656.                 }
  657.               }
  658.             }
  659.           }
  660.         }
  661.         tr.Commit();
  662.       }
  663.     }
  664.   }
  665.   // A generic class for managing groups of
  666.   // numbered (and ordered) objects
  667.   public class NumberedObjectManager
  668.   {
  669.     // We need to store a list of object IDs, but
  670.     // also a list of free positions in the list
  671.     // (this allows numbering gaps)
  672.     private List<ObjectId> m_ids;
  673.     private List<int> m_free;
  674.     // Constructor
  675.     public NumberedObjectManager()
  676.     {
  677.       m_ids =
  678.         new List<ObjectId>();
  679.       m_free =
  680.         new List<int>();
  681.     }
  682.     // Clear the internal lists
  683.     public void Clear()
  684.     {
  685.       m_ids.Clear();
  686.       m_free.Clear();
  687.     }
  688.     // Return the first entry in the ObjectId list
  689.     // (specify "true" if you want to skip
  690.     // any null object IDs)
  691.     public int GetLowerBound(bool ignoreNull)
  692.     {
  693.       if (ignoreNull)
  694.         // Define an in-line predicate to check
  695.         // whether an ObjectId is null
  696.         return
  697.           m_ids.FindIndex(
  698.             delegate(ObjectId id)
  699.             {
  700.               return id != ObjectId.Null;
  701.             }
  702.           );
  703.       else
  704.         return 0;
  705.     }
  706.     // Return the last entry in the ObjectId list
  707.     public int GetUpperBound()
  708.     {
  709.       return m_ids.Count - 1;
  710.     }
  711.     // Store the specified ObjectId in the next
  712.     // available location in the list, and return
  713.     // what that is
  714.     public int NextObjectNumber(ObjectId id)
  715.     {
  716.       int pos;
  717.       if (m_free.Count > 0)
  718.       {
  719.         // Get the first free position, then remove
  720.         // it from the "free" list
  721.         pos = m_free[0];
  722.         m_free.RemoveAt(0);
  723.         m_ids[pos] = id;
  724.       }
  725.       else
  726.       {
  727.         // There are no free slots (gaps in the numbering)
  728.         // so we append it to the list
  729.         pos = m_ids.Count;
  730.         m_ids.Add(id);
  731.       }
  732.       return pos;
  733.     }
  734.     // Go through the list of objects and close any gaps
  735.     // by shuffling the list down (easy, as we're using a
  736.     // List<> rather than an array)
  737.     public ObjectIdCollection ReorderObjects()
  738.     {
  739.       // Create a collection of ObjectIds we'll return
  740.       // for the caller to go and update
  741.       // (so the renumbering will gets reflected
  742.       // in the objects themselves)
  743.       ObjectIdCollection ids =
  744.         new ObjectIdCollection();
  745.       // We'll go through the "free" list backwards,
  746.       // to allow any changes made to the list of
  747.       // objects to not affect what we're doing
  748.       List<int> rev =
  749.         new List<int>(m_free);
  750.       rev.Reverse();
  751.       foreach (int pos in rev)
  752.       {
  753.         // First we remove the object at the "free"
  754.         // position (in theory this should be set to
  755.         // ObjectId.Null, as the slot has been marked
  756.         // as blank)
  757.         m_ids.RemoveAt(pos);
  758.         // Now we go through and add the IDs of any
  759.         // affected objects to the list to return
  760.         for (int i = pos; i < m_ids.Count; i++)
  761.         {
  762.           ObjectId id = m_ids[pos];
  763.           // Only add non-null objects
  764.           // not already in the list
  765.          
  766.           if (!ids.Contains(id) &&
  767.               id != ObjectId.Null)
  768.             ids.Add(id);
  769.         }
  770.       }
  771.       
  772.       // Our free slots have been filled, so clear
  773.       // the list
  774.       
  775.       m_free.Clear();
  776.       return ids;
  777.     }
  778.     // Get the ID of an object at a particular position
  779.     public ObjectId GetObjectId(int pos)
  780.     {
  781.       if (pos < m_ids.Count)
  782.         return m_ids[pos];
  783.       else
  784.         return ObjectId.Null;
  785.     }
  786.     // Get the position of an ObjectId in the list
  787.     public int GetNumber(ObjectId id)
  788.     {
  789.       if (m_ids.Contains(id))
  790.         return m_ids.IndexOf(id);
  791.       else
  792.         return -1;
  793.     }
  794.     // Store an ObjectId in a particular position
  795.     // (shuffle == true will "insert" it, shuffling
  796.     // the remaining objects down,
  797.     // shuffle == false will replace the item in
  798.     // that slot)
  799.     public void NumberObject(
  800.       ObjectId id, int index, bool shuffle)
  801.     {
  802.       // If we're inserting into the list
  803.       if (index < m_ids.Count)
  804.       {
  805.         if (shuffle)
  806.           // Insert takes care of the shuffling
  807.           m_ids.Insert(index, id);
  808.         else
  809.         {
  810.           // If we're replacing the existing item, do
  811.           // so and then make sure the slot is removed
  812.           // from the "free" list, if applicable
  813.           m_ids[index] = id;
  814.           if (m_free.Contains(index))
  815.             m_free.Remove(index);
  816.         }
  817.       }
  818.       else
  819.       {
  820.         // If we're appending, shuffling is irrelevant,
  821.         // but we may need to add additional "free" slots
  822.         // if the position comes after the end
  823.         while (m_ids.Count < index)
  824.         {
  825.           m_ids.Add(ObjectId.Null);
  826.           m_free.Add(m_ids.LastIndexOf(ObjectId.Null));
  827.           m_free.Sort();
  828.         }
  829.         m_ids.Add(id);
  830.       }
  831.     }
  832.     // Move an ObjectId already in the list to a
  833.     // particular position
  834.     // (ObjectIds between the two positions will
  835.     // get shuffled down automatically)
  836.     public ObjectIdCollection MoveObject(
  837.       int from, int to)
  838.     {
  839.       ObjectIdCollection ids =
  840.         new ObjectIdCollection();
  841.       if (from < m_ids.Count &&
  842.           to < m_ids.Count)
  843.       {
  844.         if (from != to)
  845.         {
  846.           ObjectId id = m_ids[from];
  847.           m_ids.RemoveAt(from);
  848.           m_ids.Insert(to, id);
  849.           int start = (from < to ? from : to);
  850.           int end = (from < to ? to : from);
  851.           for (int i = start; i <= end; i++)
  852.           {
  853.             ids.Add(m_ids[i]);
  854.           }
  855.         }
  856.         // Now need to adjust/recreate "free" list
  857.         m_free.Clear();
  858.         for (int j = 0; j < m_ids.Count; j++)
  859.         {
  860.           if (m_ids[j] == ObjectId.Null)
  861.             m_free.Add(j);
  862.         }
  863.       }
  864.       return ids;
  865.     }
  866.     // Remove an ObjectId from the list
  867.     public int RemoveObject(ObjectId id)
  868.     {
  869.       // Check it's non-null and in the list
  870.       if (id != ObjectId.Null &&
  871.           m_ids.Contains(id))
  872.       {
  873.         int pos = m_ids.IndexOf(id);
  874.         RemoveObject(pos);
  875.         return pos;
  876.       }
  877.       return -1;
  878.     }
  879.     // Remove the ObjectId at a particular position
  880.     public ObjectId RemoveObject(int pos)
  881.     {
  882.       // Get the ObjectId in the specified position,
  883.       // making sure it's non-null
  884.       ObjectId id = m_ids[pos];
  885.       if (id != ObjectId.Null)
  886.       {
  887.         // Null out the position and add it to the
  888.         // "free" list
  889.         m_ids[pos] = ObjectId.Null;
  890.         m_free.Add(pos);
  891.         m_free.Sort();
  892.       }
  893.       return id;
  894.     }
  895.     // Dump out the object list information
  896.     // as well as the "free" slots
  897.     public void DumpInfo(Editor ed)
  898.     {
  899.       if (m_ids.Count > 0)
  900.       {
  901.         ed.WriteMessage("\nIdx ObjectId");
  902.         int index = 0;
  903.         foreach (ObjectId id in m_ids)
  904.           ed.WriteMessage("\n{0} {1}", index++, id);
  905.       }
  906.       if (m_free.Count > 0)
  907.       {
  908.         ed.WriteMessage("\n\nFree list: ");
  909.         foreach (int pos in m_free)
  910.           ed.WriteMessage("{0} ", pos);
  911.       }
  912.     }
  913.     // Remove the initial n items from the list
  914.     public void RebaseList(int start)
  915.     {
  916.       // First we remove the ObjectIds
  917.       for (int i=0; i < start; i++)
  918.         m_ids.RemoveAt(0);
  919.       // Then we go through the "free" list...
  920.       int idx = 0;
  921.       while (idx < m_free.Count)
  922.       {
  923.         if (m_free[idx] < start)
  924.           // Remove any that refer to the slots
  925.           // we've removed
  926.           m_free.RemoveAt(idx);
  927.         else
  928.         {
  929.           // Subtracting the number of slots
  930.           // we've removed from the other items
  931.           m_free[idx] -= start;
  932.           idx++;
  933.         }
  934.       }
  935.     }
  936.   }
  937. }
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:
  1. Command: LNS
  2. Lowest index is 1. Make this the start of the list? [Yes/No] <Yes>: Yes
  3. Command: DB
  4. Enter number of bubble to erase: 3
  5. Command: DB
  6. Enter number of bubble to erase: 5
  7. Command: DB
  8. Enter number of bubble to erase: 15
  9. Command: DB
  10. Enter number of bubble to erase: 16
  11. Command: DMP
  12. Idx ObjectId
  13. 0 (2129683752)
  14. 1 (2129683776)
  15. 2 (0)
  16. 3 (2129683824)
  17. 4 (0)
  18. 5 (2129683872)
  19. 6 (2129683896)
  20. 7 (2129683920)
  21. 8 (2129683944)
  22. 9 (2129683968)
  23. 10 (2129683992)
  24. 11 (2129684016)
  25. 12 (2129684040)
  26. 13 (2129684064)
  27. 14 (0)
  28. 15 (0)
  29. 16 (2129684136)
  30. 17 (2129684160)
  31. 18 (2129684184)
  32. 19 (2129684208)
  33. 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:
  1. Command: MB
  2. Enter number of bubble to move: 7
  3. Enter destination position: 2
  4. Command: DMP
  5. Idx ObjectId
  6. 0 (2129683752)
  7. 1 (2129683896)
  8. 2 (2129683776)
  9. 3 (0)
  10. 4 (2129683824)
  11. 5 (0)
  12. 6 (2129683872)
  13. 7 (2129683920)
  14. 8 (2129683944)
  15. 9 (2129683968)
  16. 10 (2129683992)
  17. 11 (2129684016)
  18. 12 (2129684040)
  19. 13 (2129684064)
  20. 14 (0)
  21. 15 (0)
  22. 16 (2129684136)
  23. 17 (2129684160)
  24. 18 (2129684184)
  25. 19 (2129684208)
  26. 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:
  1. Command: RBS
  2. Command: DMP
  3. Idx ObjectId
  4. 0 (2129683752)
  5. 1 (2129683896)
  6. 2 (2129683776)
  7. 3 (2129683824)
  8. 4 (2129683872)
  9. 5 (2129683920)
  10. 6 (2129683944)
  11. 7 (2129683968)
  12. 8 (2129683992)
  13. 9 (2129684016)
  14. 10 (2129684040)
  15. 11 (2129684064)
  16. 12 (2129684136)
  17. 13 (2129684160)
  18. 14 (2129684184)
  19. 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...?
 楼主| 发表于 2009-5-15 15:28:00 | 显示全部楼层
您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|CAD论坛|CAD教程|CAD下载|联系我们|关于明经|明经通道 ( 粤ICP备05003914号 )  
©2000-2023 明经通道 版权所有 本站代码,在未取得本站及作者授权的情况下,不得用于商业用途

GMT+8, 2024-11-23 12:58 , Processed in 0.186304 second(s), 23 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表