Wednesday, November 20, 2013

A Block with Auto-Pointing Leader

In drafting process, often user needs to create something like a label with a leader and points it to something in the drawing. The picture below is an example.




Also more often than not, depending on how crowded the drawing content is, user needs to move the label around. It would be better that when the label (be it consists of separate entities, or a block) moves, its pointing leader's tip can remain where it was, so that user would not have to move the label and then re-points the leader.

When I ran into a similar requirement from one of my AutoCAD programming project, the first idea jump out from my head is Overrule. For example, the label can be just a block without a leader line. Then I could use DrawableOverrule to draw a leader as the visual effect.

The problem with this approach is that the Overrule code must be loaded and enabled all the time. Or the label would not appear to have a leader pointing the point in interest.

In my real project, we settled with using a dynamic block for the label, which has a stretchable leader. Following is an example of a similar block:

Here is the video clip that shows how its leader can be stretched freely. The advantage of using this kind of block is that even if the code to auto-points the leader is not loaded and/does not works, the user can always manually drag the leader to make it point to where it is supposed to.

With this block, I figured, as long as I know where the point of interest, to which the leader points, I can always set the dynamic property's value (the X/Y value of the point parameter, named as "LeaderPoint" in this particular block).

So, the logic of setting the point parameter value is really simple: When the label block is inserted, the point of interest is saved with this block as XData, and then whenever the label block is changed (moved, stretched, rotated, scaled), the point parameter's value is recalculated and reset, thus the leader will remain pointing to the point of interest.

I went ahead trying to implement a TransformOverrule, thinking it would be fairly easy to do with just creating the overridden TransformBy() virtual method.

However, it turned out that the TransformOverrule was not the solution for 2 reasons:

1. The TransformBy() method seems not being triggered when user moves the block by select the block and drag the block by its grips;

2. AutoCAD crashes when the code is trying to set block reference's dynamic property value in the TransformBy() method. The crash is not not catch-able in my "try...catch..." code block, and if the code does not try to set the dynamic property value, then no crash. I just could not get over with this crashing issue and tend to think it is something inside the API, be it a bug or not.

The other drawback with using Overrule is, as I stated previously, the Overrule code has to be loaded all the time.

Therefore I gave up the Overrule idea and turned to handling events, like CommandWillStart, CommandEnded, and ObjectModified. The idea is that during the period from CommandWillStart to CommandEnded, the code will watch ObjectModified event if the command results in changes to the specific block references. The changed block reference's ObjectId is saved in a collection. After CommandEnded event, if the collection has ObjectId in it, extra code is executed to reset the block reference's dynamic property value (the point parameter that stretches the leader).

Here are the 2 helper classes going first:

Class XDataHelper, which embeds a Point3d value in the label block and read out the embedded Point3d value from the label block:

    1 using Autodesk.AutoCAD.DatabaseServices;
    2 using Autodesk.AutoCAD.Geometry;
    3 
    4 namespace BlockWithAutoPointingLeader
    5 {
    6     public static class XDataHelper
    7     {
    8         public static void SetPointToEntityXData(
    9             ObjectId entId, string appName, Point3d point)
   10         {
   11             RegisterXDataApp(appName, entId.Database);
   12 
   13             Database db = HostApplicationServices.WorkingDatabase;
   14 
   15             using (Transaction tran =
   16                 db.TransactionManager.StartOpenCloseTransaction())
   17             {
   18                 Entity ent = (Entity)tran.GetObject(entId, OpenMode.ForWrite);
   19 
   20                 ResultBuffer buffer = new ResultBuffer(
   21                             new TypedValue(
   22                                 (int)DxfCode.ExtendedDataRegAppName, appName),
   23                             new TypedValue(
   24                                 (int)DxfCode.ExtendedDataXCoordinate, point)
   25                             );
   26 
   27                 ent.XData = buffer;
   28 
   29                 tran.Commit();
   30             }
   31         }
   32 
   33         public static Point3d GetPointFromEntityXData(
   34             ObjectId entId, string appName)
   35         {
   36             Point3d point = new Point3d(
   37                 double.MinValue, double.MinValue, double.MinValue);
   38 
   39             Database db = HostApplicationServices.WorkingDatabase;
   40 
   41             using (Transaction tran =
   42                 db.TransactionManager.StartOpenCloseTransaction())
   43             {
   44                 Entity ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
   45 
   46                 point = GetPointFromEntityXData(ent, appName);
   47 
   48                 tran.Commit();
   49             }
   50 
   51             return point;
   52         }
   53 
   54         public static Point3d GetPointFromEntityXData(
   55             Entity entity, string appName)
   56         {
   57             Point3d point = new Point3d(
   58                 double.MinValue, double.MinValue, double.MinValue);
   59 
   60             ResultBuffer buffer = entity.GetXDataForApplication(appName);
   61 
   62             if (buffer != null)
   63             {
   64                 foreach (TypedValue tv in buffer)
   65                 {
   66                     if (tv.TypeCode ==
   67                         (short)DxfCode.ExtendedDataXCoordinate)
   68                     {
   69                         point = (Point3d)tv.Value;
   70                     }
   71                 }
   72             }
   73 
   74             return point;
   75         }
   76 
   77         private static void RegisterXDataApp(string appName, Database db)
   78         {
   79             using (Transaction tran = db.TransactionManager.StartTransaction())
   80             {
   81                 RegAppTable tbl = (RegAppTable)tran.GetObject(
   82                     db.RegAppTableId, OpenMode.ForRead);
   83 
   84                 if (!tbl.Has(appName))
   85                 {
   86                     tbl.UpgradeOpen();
   87                     RegAppTableRecord app = new RegAppTableRecord();
   88                     app.Name = appName;
   89 
   90                     tbl.Add(app);
   91                     tran.AddNewlyCreatedDBObject(app, true);
   92                 }
   93 
   94                 tran.Commit();
   95             }
   96         }
   97     }
   98 }

Class LeaderPointingHelper, which rests point parameter's value in the dynamic block reference:

    1 using System;
    2 using Autodesk.AutoCAD.DatabaseServices;
    3 using Autodesk.AutoCAD.Geometry;
    4 
    5 namespace BlockWithAutoPointingLeader
    6 {
    7     static class LeaderPointingHelper
    8     {
    9         public static void SnapLeaderToPoint(
   10             ObjectId blkId, string xDataAppName, string leaderPropName)
   11         {
   12             Database db = HostApplicationServices.WorkingDatabase;
   13 
   14             using (Transaction tran =
   15                 db.TransactionManager.StartTransaction())
   16             {
   17                 BlockReference bref = (BlockReference)
   18                     tran.GetObject(blkId, OpenMode.ForWrite);
   19                 SnapLeaderToPoint(bref, xDataAppName, leaderPropName);
   20                 tran.Commit();
   21             }
   22         }
   23 
   24         public static void SnapLeaderToPoint(
   25             BlockReference bref, string xDataAppName, string leaderPropName)
   26         {
   27             //Get point embedded with the block reference
   28             Point3d point = XDataHelper.GetPointFromEntityXData(
   29                 bref, xDataAppName);
   30             if (point.X == double.MinValue &&
   31                 point.Y == double.MinValue &&
   32                 point.Z == double.MinValue) return;
   33 
   34             //Get the pointing location's coordinate in terms of
   35             //the block's insertion point (e.g. set the block's
   36             //insertion point as the origin of a UCS and get the
   37             //coordinate of the pointing location in this UCS
   38             Point3d coordPoint = GetUscCoordinate(
   39                 point, bref.Rotation, bref.Position);
   40 
   41             //Set block's dynamic property - the strechable leader
   42             foreach (DynamicBlockReferenceProperty prop in
   43                 bref.DynamicBlockReferencePropertyCollection)
   44             {
   45                 if (prop.PropertyName ==
   46                     leaderPropName + " X")
   47                 {
   48                     prop.Value = coordPoint.X;
   49                 }
   50 
   51                 if (prop.PropertyName ==
   52                     leaderPropName + " Y")
   53                 {
   54                     prop.Value = coordPoint.Y;
   55                 }
   56             }
   57         }
   58 
   59         #region private methods
   60 
   61         private static Point3d GetUscCoordinate(
   62             Point3d inPoint, double ucsAngle, Point3d ucsOrigin)
   63         {
   64             double x, y, z;
   65 
   66             x = Math.Cos(ucsAngle);
   67             y = Math.Sin(ucsAngle);
   68             z = 0.0;
   69 
   70             Vector3d xVec = new Vector3d(x, y, z);
   71 
   72             x = 0.0 - Math.Sin(ucsAngle);
   73             y = Math.Cos(ucsAngle);
   74             z = 0.0;
   75 
   76             Vector3d yVec = new Vector3d(x, y, z);
   77             CoordinateSystem3d ucs =
   78                 new CoordinateSystem3d(ucsOrigin, xVec, yVec);
   79 
   80             Matrix3d mt = Matrix3d.AlignCoordinateSystem(
   81                 Point3d.Origin, Vector3d.XAxis,
   82                 Vector3d.YAxis, Vector3d.ZAxis,
   83                 ucs.Origin, ucs.Xaxis,
   84                 ucs.Yaxis, ucs.Zaxis);
   85 
   86             Point3d p = inPoint.TransformBy(mt.Inverse());
   87 
   88             return p;
   89         }
   90 
   91         #endregion
   92     }
   93 }

Here is the code that does the real work: class AutoPointingHandler:

    1 using System.Collections.Generic;
    2 using Autodesk.AutoCAD.ApplicationServices;
    3 using Autodesk.AutoCAD.DatabaseServices;
    4 
    5 namespace BlockWithAutoPointingLeader
    6 {
    7     public class AutoPointingHandler
    8     {
    9         private string _xDataAppName = "";
   10         private string _leaderPropName = "";
   11         private string _blockName = "";
   12         private bool _enabled = false;
   13         private bool _updating = false;
   14         private bool _handlerAttached = false;
   15         private bool _isMove = false;
   16 
   17         private List<ObjectId> _targetBlockIds = null;
   18 
   19         private Document _dwg;
   20 
   21         public AutoPointingHandler(
   22             string xDataAppName, string blockName, string leaderPropName)
   23         {
   24             _dwg = Application.DocumentManager.MdiActiveDocument;
   25             _xDataAppName = xDataAppName;
   26             _blockName = blockName;
   27             _leaderPropName = leaderPropName;
   28         }
   29 
   30         public bool Enabled
   31         {
   32             get { return _enabled; }
   33         }
   34 
   35         public void EnableAutoPointing(bool enable)
   36         {
   37             if (enable)
   38             {
   39                 if (!_enabled)
   40                 {
   41                     _dwg.CommandWillStart += dwg_CommandWillStart;
   42                     _enabled = true;
   43                 }
   44             }
   45             else
   46             {
   47                 if (_enabled)
   48                 {
   49                     _dwg.CommandWillStart -= dwg_CommandWillStart;
   50                     _enabled = false;
   51                 }
   52             }
   53         }
   54 
   55         private void dwg_CommandWillStart(object sender, CommandEventArgs e)
   56         {
   57             if (!_enabled) return;
   58             if (_updating) return;
   59 
   60             string cmdName = e.GlobalCommandName.ToUpper();
   61             if (cmdName.Contains("GRIP_POPUP") ||
   62                 cmdName.Contains("GRIP_STRETCH") ||
   63                 cmdName.Contains("MOVE") ||
   64                 cmdName.Contains("ROTATE") ||
   65                 cmdName.Contains("SCALE") ||
   66                 cmdName.Contains("STRETCH"))
   67             {
   68                 try
   69                 {
   70                     _dwg.Database.ObjectModified -= database_ObjectModified;
   71                     _dwg.CommandEnded -= dwg_CommandEnded;
   72                 }
   73                 catch { }
   74 
   75                 _dwg.Database.ObjectModified += database_ObjectModified;
   76                 _dwg.CommandEnded += dwg_CommandEnded;
   77                 _handlerAttached = true;
   78                 _targetBlockIds = new List<ObjectId>();
   79             }
   80         }
   81 
   82         private void dwg_CommandEnded(object sender, CommandEventArgs e)
   83         {
   84             if (_handlerAttached)
   85             {
   86                 _dwg.Database.ObjectModified -= database_ObjectModified;
   87                 _dwg.CommandEnded -= dwg_CommandEnded;
   88             }
   89 
   90             if (!_enabled) return;
   91             if (_updating) return;
   92 
   93             string cmdName = e.GlobalCommandName.ToUpper();
   94 
   95             if (cmdName.Contains("GRIP_POPUP") ||
   96                 cmdName.Contains("GRIP_STRETCH") ||
   97                 cmdName.Contains("MOVE") ||
   98                 cmdName.Contains("ROTATE") ||
   99                 cmdName.Contains("SCALE") ||
  100                 cmdName.Contains("STRETCH"))
  101             {
  102                 if (_targetBlockIds.Count>0)
  103                 {
  104                     _isMove = !cmdName.Contains("GRIP_POPUP");
  105 
  106                     //Since station(s) block has/have been modified
  107                     //recalculate/reset its dynamic property "LeaderPoint"
  108                     _updating = true;
  109                     foreach (var id in _targetBlockIds)
  110                     {
  111                         LeaderPointingHelper.SnapLeaderToPoint(
  112                             id, _xDataAppName, _leaderPropName);
  113                     }             
  114                     _updating = false;
  115 
  116                     _dwg.Editor.UpdateScreen();
  117                     _targetBlockIds = null;
  118                 }
  119             }
  120         }
  121 
  122         private void database_ObjectModified(object sender, ObjectEventArgs e)
  123         {
  124             if (!_enabled) return;
  125             if (_updating) return;
  126 
  127             if (!(e.DBObject is BlockReference)) return;
  128             if (e.DBObject.ObjectId.IsErased ||
  129                 e.DBObject.ObjectId.IsEffectivelyErased) return;
  130             if (_targetBlockIds == null) return;
  131 
  132             //Determine if the changed block refernce is
  133             //the target blockreference
  134             BlockReference bref = e.DBObject as BlockReference;
  135             if (bref.IsDynamicBlock)
  136             {
  137                 string blkName = "";
  138                 using (Transaction tran =
  139                     _dwg.TransactionManager.StartOpenCloseTransaction())
  140                 {
  141                     BlockTableRecord br = (BlockTableRecord)tran.GetObject(
  142                             bref.DynamicBlockTableRecord, OpenMode.ForRead);
  143                     blkName = br.Name;
  144                 }
  145 
  146                 if (blkName.ToUpper() == _blockName.ToUpper())
  147                 {
  148                     if (!_targetBlockIds.Contains(e.DBObject.ObjectId))
  149                     {
  150                         _targetBlockIds.Add(e.DBObject.ObjectId);
  151                     }
  152                 }
  153             }
  154         } 
  155     }
  156 }
 
 
Below is the code that insert the label block and enable the AutoPointingHandler. Note the the command methods are NOT static, so the command class is instantiated for each document when the commands is executed. This way, the AutoPointingHandler instance only hooks to events of its own drawing document.

    1 using System;
    2 using Autodesk.AutoCAD.ApplicationServices;
    3 using Autodesk.AutoCAD.DatabaseServices;
    4 using Autodesk.AutoCAD.EditorInput;
    5 using Autodesk.AutoCAD.Geometry;
    6 using Autodesk.AutoCAD.Runtime;
    7 
    8 [assembly: CommandClass(typeof(BlockWithAutoPointingLeader.MyCommands))]
    9 
   10 namespace BlockWithAutoPointingLeader
   11 {
   12     public class MyCommands
   13     {
   14         private const string XDATA_APPNAME = "AutoPointingBlock";
   15         private const string BLOCK_LEADER_PROPNAME = "LeaderPoint";
   16         private const string BLOCK_NAME = "PartNumber";
   17         private AutoPointingHandler _autoPointer = null;
   18 
   19         [CommandMethod("InsMyBlk")]
   20         public void RunMyCommand()
   21         {
   22             Document dwg = Application.DocumentManager.MdiActiveDocument;
   23             Editor ed = dwg.Editor;
   24 
   25             try
   26             {
   27                 int blkInserted = InsertAutoPointingBlock(dwg);
   28                 if (blkInserted > 0)
   29                 {
   30                     ed.WriteMessage("\n{0} block{1} inserted.",
   31                         blkInserted, blkInserted > 1 ? "s" : "");
   32 
   33                     EnsureAutoPoiningHandler();
   34                     _autoPointer.EnableAutoPointing(true);
   35                 }
   36             }
   37             catch (System.Exception ex)
   38             {
   39                 ed.WriteMessage("\nError: {0}", ex.Message);
   40             }
   41 
   42             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   43         }
   44 
   45         [CommandMethod("EnableAutoPointing")]
   46         public void EnableAutoPointingHandler()
   47         {
   48             Document dwg = Application.DocumentManager.MdiActiveDocument;
   49             Editor ed = dwg.Editor;
   50 
   51             EnsureAutoPoiningHandler();
   52             bool enabled = _autoPointer.Enabled;
   53 
   54             PromptKeywordOptions opt = new PromptKeywordOptions(
   55                 "\nAutoPointinghandler is currently " +
   56                 (enabled ? "enabled" : "disabled."));
   57             if (enabled)
   58             {
   59                 opt.Keywords.Add("oFf");
   60                 opt.Keywords.Default = "oFf";
   61             }
   62             else
   63             {
   64                 opt.Keywords.Add("oN");
   65                 opt.Keywords.Default = "oN";
   66             }
   67             opt.AppendKeywordsToMessage = true;
   68 
   69             PromptResult res = ed.GetKeywords(opt);
   70             if (res.Status == PromptStatus.OK)
   71             {
   72                 if (res.StringResult.ToUpper() == "ON")
   73                 {
   74                     _autoPointer.EnableAutoPointing(true);
   75                     ed.WriteMessage("\nAutoPointingHandler is now enabled.");
   76                 }
   77                 else
   78                 {
   79                     _autoPointer.EnableAutoPointing(false);
   80                     ed.WriteMessage("\nAutoPointingHandler is now disabled.");
   81                 }
   82             }
   83             else
   84             {
   85                 ed.WriteMessage("\n*Cancel*");
   86             }
   87 
   88             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   89         }
   90 
   91         private void EnsureAutoPoiningHandler()
   92         {
   93             if (_autoPointer == null)
   94             {
   95                 _autoPointer = new AutoPointingHandler(
   96                     XDATA_APPNAME, BLOCK_NAME, BLOCK_LEADER_PROPNAME);
   97             }
   98         }
   99 
  100         private int InsertAutoPointingBlock(Document dwg)
  101         {
  102             string blkLayer = "Layer1";
  103             int count = 0;
  104             while (true)
  105             {
  106                 Point3d lblPt;
  107                 Point3d blkPt;
  108                 if (PickLabelPoint(dwg.Editor,
  109                     "Pick point to be labelled:", out lblPt))
  110                 {
  111                     if (PickBlockPoint(dwg.Editor,
  112                         "Pick point for the label block", lblPt, out blkPt))
  113                     {
  114                         count++;
  115                         //Insert block
  116                         ObjectId brefId = InsertBlock(
  117                             dwg.Database, BLOCK_NAME, blkLayer, blkPt, count);
  118 
  119                         //Attach XData to the block refernce
  120                         XDataHelper.SetPointToEntityXData(
  121                             brefId, XDATA_APPNAME, lblPt);
  122 
  123                         //Snap block leader to point
  124                         LeaderPointingHelper.SnapLeaderToPoint(
  125                             brefId, XDATA_APPNAME, BLOCK_LEADER_PROPNAME);
  126 
  127                         dwg.Editor.UpdateScreen();
  128                     }
  129                     else
  130                     {
  131                         break;
  132                     }
  133                 }
  134                 else
  135                 {
  136                     break;
  137                 }
  138             }
  139 
  140             return count;
  141         }
  142 
  143         private bool PickLabelPoint(
  144             Editor ed, string msg, out Point3d point)
  145         {
  146             point = new Point3d();
  147             PromptPointOptions opt = new PromptPointOptions("\n" + msg);
  148             PromptPointResult res = ed.GetPoint(opt);
  149             if (res.Status == PromptStatus.OK)
  150             {
  151                 point = res.Value;
  152                 return true;
  153             }
  154             else
  155             {
  156                 return false;
  157             }
  158         }
  159 
  160         private bool PickBlockPoint(
  161             Editor ed, string msg, Point3d basePoint, out Point3d point)
  162         {
  163             point = new Point3d();
  164             PromptPointOptions opt = new PromptPointOptions("\n" + msg);
  165             opt.UseBasePoint = true;
  166             opt.BasePoint = basePoint;
  167             opt.UseDashedLine = true;
  168             PromptPointResult res = ed.GetPoint(opt);
  169             if (res.Status == PromptStatus.OK)
  170             {
  171                 point = res.Value;
  172                 return true;
  173             }
  174             else
  175             {
  176                 return false;
  177             }
  178         }
  179 
  180         private ObjectId InsertBlock(
  181             Database db, string blkName,
  182             string layerName, Point3d insPoint, int count)
  183         {
  184             ObjectId brefId = ObjectId.Null;
  185 
  186             using (Transaction tran =
  187                 db.TransactionManager.StartTransaction())
  188             {
  189                 BlockTable bt = (BlockTable)tran.GetObject(
  190                     db.BlockTableId, OpenMode.ForRead);
  191                 if (bt.Has(blkName))
  192                 {
  193                     BlockTableRecord bdef = (BlockTableRecord)
  194                         tran.GetObject(bt[blkName], OpenMode.ForRead);
  195 
  196                     BlockTableRecord model = (BlockTableRecord)tran.GetObject(
  197                         SymbolUtilityServices.GetBlockModelSpaceId(db),
  198                         OpenMode.ForWrite);
  199 
  200                     //Insert block
  201                     BlockReference bref = new BlockReference(
  202                         insPoint, bdef.ObjectId);
  203                     bref.SetDatabaseDefaults(db);
  204                     bref.Layer = layerName;
  205 
  206                     brefId = model.AppendEntity(bref);
  207                     tran.AddNewlyCreatedDBObject(bref, true);
  208 
  209                     //Add attribute
  210                     foreach (ObjectId id in bdef)
  211                     {
  212                         AttributeDefinition adef = tran.GetObject(
  213                             id, OpenMode.ForRead) as AttributeDefinition;
  214                         if (adef != null)
  215                         {
  216                             AttributeReference aref =
  217                                 new AttributeReference();
  218                             aref.SetAttributeFromBlock(
  219                                 adef, bref.BlockTransform);
  220 
  221                             if (adef.Tag.ToUpper() == "NO")
  222                             {
  223                                 aref.TextString =
  224                                     count.ToString().PadLeft(3, '0');
  225                             }
  226                             else
  227                             {
  228                                 aref.TextString = " ";
  229                             }
  230 
  231                             bref.AttributeCollection.AppendAttribute(aref);
  232                             tran.AddNewlyCreatedDBObject(aref, true);
  233                         }
  234                     }
  235                 }
  236                 else
  237                 {
  238                     throw new InvalidOperationException("Block \"" +
  239                     blkName + "\" not defined.");
  240                 }
  241 
  242                 tran.Commit();
  243             }
  244 
  245             return brefId;
  246         }
  247     }
  248 }

Watch this video clip to see the code in action.

A few things to note:

1. During the select-first operation of moving/rotating/scaling, the grip for dragging the leader's end point is not updated until the label block is deselected. I tried in code with Editor.Regen() without success. I could have try to remove it from the implied SelectionSet, but figured it is not a big deal.

2. While the AutoPointingHandler is enabled, when user drags the dynamic block's leader to point it to somewhere else, the leader will always go back. This could be good thing or bad thing. But in my case, this is by design, a good thing.

3. The key factor for this to work is to embed the point of interest with the block reference. If the point is changed, the block has to be re-inserted in order to have the changed point to be embedded with the block as XData.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.