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 }
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.