While running the code to see the effect of the custom Object Snap, I realize that it might be even better, once the user selects the option of doing "MEASURE" or "DIVIDE", that the code not only allow custom object snapping, but also show visual hint where the potential object snapping point is, so that the user does not have to guess where the snapping point is by moving around the mouse cursor blindly along the curve.
So, I streamlined the code showed in previous 2 posts and added code to show visual hint for the snapping points and pull all of them together as following.
Firstly, I place the "Measure"/"Divide" point calculating code in a static class CurvePointTool:
1 using System;
2 using System.Collections.Generic;
3 using Autodesk.AutoCAD.DatabaseServices;
4 using Autodesk.AutoCAD.Geometry;
5
6 namespace MeasureDivideWithSnap
7 {
8 public static class CurvePointTool
9 {
10 public static Point3d[] GetMeasurePoints(ObjectId entId, double segmentLength)
11 {
12 List<Point3d> points = new List<Point3d>();
13
14 using (Transaction tran =
15 entId.Database.TransactionManager.StartTransaction())
16 {
17
18 Curve curve = tran.GetObject(
19 entId, OpenMode.ForRead) as Curve;
20
21 if (curve == null)
22 {
23 throw new TypeAccessException(
24 "Invalid entity: must be LINE/POLYLINE/ARC/CIRCLE.");
25 }
26
27 //Add snap point at start point
28 points.Add(curve.StartPoint);
29
30 double length = curve.GetDistanceAtParameter(curve.EndParam);
31
32 //get each segment length
33 double segLength = segmentLength;
34
35 //Add snap points.
36 double l = segLength;
37 while (l <= length)
38 {
39 Point3d pt = curve.GetPointAtDist(l);
40 points.Add(pt);
41
42 l += segLength;
43 }
44
45 tran.Commit();
46 }
47
48 return points.ToArray();
49 }
50
51 public static Point3d[] GetDividePoints(ObjectId entId, int segmentNumber)
52 {
53 List<Point3d> points = new List<Point3d>();
54
55 using (Transaction tran =
56 entId.Database.TransactionManager.StartTransaction())
57 {
58
59 Curve curve = tran.GetObject(
60 entId, OpenMode.ForRead) as Curve;
61
62 if (curve == null)
63 {
64 throw new TypeAccessException(
65 "Invalid entity: must be LINE/POLYLINE/ARC/CIRCLE.");
66 }
67
68 //Add snap point at start point
69 points.Add(curve.StartPoint);
70
71 double length = curve.GetDistanceAtParameter(curve.EndParam);
72
73 //get each segment length
74 double segLength = length / segmentNumber;
75
76 //Add snap points between start and end points
77 double l = segLength;
78 while ((length - l) > segLength * 0.99)
79 {
80 Point3d pt = curve.GetPointAtDist(l);
81 points.Add(pt);
82
83 l += segLength;
84 }
85
86 //Add snap point at end point
87 points.Add(curve.EndPoint);
88
89 tran.Commit();
90 }
91
92 return points.ToArray();
93 }
94 }
95 }
Below are the streamlined custom object snap classes.
class CustomGlyphObjectSnap:
1 using System;
2 using System.Collections.Generic;
3 using Autodesk.AutoCAD.DatabaseServices;
4 using Autodesk.AutoCAD.GraphicsInterface;
5 using Autodesk.AutoCAD.Geometry;
6 using Autodesk.AutoCAD.Runtime;
7
8 namespace MeasureDivideWithSnap
9 {
10 public class CustomGlyphObjectSnap : Glyph
11 {
12 private static CustomGlyphObjectSnap _instance = null;
13 private bool _started = false;
14
15 private const string LOCAL_MODEL_STRING = "MyCustomSnap";
16 private const string GLOBAL_MODEL_STRING = "_MyCustomSnap";
17 private const string TOOL_TIP_STRING = "Customized object snap";
18
19 private CustomObjectSnapMode _snapMode;
20
21 private ObjectId _entId = ObjectId.Null;
22 private Point3d _point;
23 private IEnumerable<Point3d> _points = null;
24
25 public static CustomGlyphObjectSnap Instance
26 {
27 get
28 {
29 if (_instance == null) _instance = new CustomGlyphObjectSnap();
30 return _instance;
31 }
32 }
33
34 public void StartSnap(ObjectId entId, IEnumerable<Point3d> points)
35 {
36 if (_started) return;
37
38 _entId = entId;
39 _points = points;
40 _snapMode = CreateCustomObjectSnapMode();
41
42 _started = true;
43 }
44
45 public void StopSnap()
46 {
47 if (!_started) return;
48
49 RemoveCustomObjectSnapMode();
50
51 _entId = ObjectId.Null;
52 _points = null;
53 _started = false;
54 }
55
56 #region Overriding base class methods
57
58 public override void SetLocation(Point3d point)
59 {
60 _point = point;
61 }
62
63 protected override void SubViewportDraw(ViewportDraw vd)
64 {
65 //Draw a square polygon at snap point
66 Point2d gSize = vd.Viewport.GetNumPixelsInUnitSquare(_point);
67 double gHeight = CustomObjectSnapMode.GlyphSize / gSize.Y;
68 Matrix3d dTOw = vd.Viewport.EyeToWorldTransform;
69
70 Point3d[] gPts =
71 {
72 new Point3d(
73 _point.X - gHeight/2.0,
74 _point.Y - gHeight/2.0,
75 _point.X).TransformBy(dTOw),
76 new Point3d(
77 _point.X + gHeight/2.0,
78 _point.Y - gHeight/2.0,
79 _point.X).TransformBy(dTOw),
80 new Point3d(
81 _point.X + gHeight/2.0,
82 _point.Y + gHeight/2.0,
83 _point.X).TransformBy(dTOw),
84 new Point3d(
85 _point.X - gHeight/2.0,
86 _point.Y + gHeight/2.0,
87 _point.X).TransformBy(dTOw),
88 };
89
90 vd.Geometry.Polygon(new Point3dCollection(gPts));
91
92 ////-----------------------------------------------------------
93 ////If you want to draw a circle at snap point,
94 ////simply comment out above code and
95 ////uncomment code below
96 ////-----------------------------------------------------------
97 //Point2d gSize = vd.Viewport.GetNumPixelsInUnitSquare(_point);
98 //double dia = CustomObjectSnapMode.GlyphSize / gSize.Y;
99 //vd.Geometry.Circle(_point, dia / 2.0, Vector3d.ZAxis);
100 }
101
102 #endregion
103
104 #region private methods of creating CustomObjectSnapMode object
105
106 protected CustomObjectSnapMode CreateCustomObjectSnapMode()
107 {
108 CustomObjectSnapMode snap = new CustomObjectSnapMode(
109 LOCAL_MODEL_STRING, GLOBAL_MODEL_STRING,
110 TOOL_TIP_STRING, Instance);
111
112 Type t = GetEntityType();
113
114 snap.ApplyToEntityType(
115 RXClass.GetClass(t), AddMeasureObjectSnapInfo);
116
117 CustomObjectSnapMode.Activate(GLOBAL_MODEL_STRING);
118
119 return snap;
120 }
121
122 protected void RemoveCustomObjectSnapMode()
123 {
124 CustomObjectSnapMode.Deactivate(GLOBAL_MODEL_STRING);
125
126 Type t = GetEntityType();
127 _snapMode.RemoveFromEntityType(RXClass.GetClass(t));
128 _snapMode.Dispose();
129 _snapMode = null;
130 }
131
132 protected void AddMeasureObjectSnapInfo(
133 ObjectSnapContext context, ObjectSnapInfo result)
134 {
135 if (context.PickedObject.ObjectId != _entId) return;
136
137 Point3dCollection points = result.SnapPoints;
138 points.Clear();
139
140 foreach (var p in _points)
141 points.Add(p);
142 }
143
144 #endregion
145
146 #region private methods
147
148 private Type GetEntityType()
149 {
150 Type t;
151 switch (_entId.ObjectClass.DxfName.ToUpper())
152 {
153 case "CIRCLE":
154 t = typeof(Circle);
155 break;
156 case "ARC":
157 t = typeof(Arc);
158 break;
159 case "LINE":
160 t = typeof(Line);
161 break;
162 default:
163 t = typeof(Autodesk.AutoCAD.DatabaseServices.Polyline);
164 break;
165 }
166
167 return t;
168 }
169
170 #endregion
171 }
172 }
class CustomOverruleObjectSnap:
1 using System;
2 using System.Collections.Generic;
3 using Autodesk.AutoCAD.DatabaseServices;
4 using Autodesk.AutoCAD.Geometry;
5 using Autodesk.AutoCAD.Runtime;
6
7 namespace MeasureDivideWithSnap
8 {
9 public class CustomOverruleObjectSnap : OsnapOverrule
10 {
11 private static CustomOverruleObjectSnap _instance = null;
12 private bool _overruling;
13 private bool _started = false;
14
15 private ObjectId _entId = ObjectId.Null;
16 private IEnumerable<Point3d> _points = null;
17
18 public CustomOverruleObjectSnap()
19 {
20 _overruling = Overrule.Overruling;
21 }
22
23 public static CustomOverruleObjectSnap Instance
24 {
25 get
26 {
27 if (_instance == null)
28 {
29 _instance = new CustomOverruleObjectSnap();
30 }
31 return _instance;
32 }
33 }
34
35 public void StartSnap(ObjectId entId, IEnumerable<Point3d> points)
36 {
37 if (_started)
38 {
39 StopSnap();
40 }
41
42 _entId = entId;
43 _points = points;
44
45 Type t = GetEntityType();
46
47 Overrule.AddOverrule(RXClass.GetClass(t), this, false);
48 Overrule.Overruling = true;
49 _started = true;
50
51 this.SetIdFilter(new ObjectId[] { _entId });
52 }
53
54 public void StopSnap()
55 {
56 if (!_started) return;
57
58 Type t = GetEntityType();
59
60 Overrule.RemoveOverrule(RXClass.GetClass(t), this);
61 Overrule.Overruling = _overruling;
62 _started = false;
63
64 _entId = ObjectId.Null;
65 _points = null;
66 }
67
68 public override void GetObjectSnapPoints(
69 Entity entity, ObjectSnapModes snapMode, IntPtr gsSelectionMark,
70 Point3d pickPoint, Point3d lastPoint, Matrix3d viewTransform,
71 Point3dCollection snapPoints, IntegerCollection geometryIds)
72 {
73 snapPoints.Clear();
74 snapMode = ObjectSnapModes.ModeNear;
75
76 foreach (var p in _points)
77 snapPoints.Add(p);
78 }
79
80 public override bool IsContentSnappable(Entity entity)
81 {
82 return false;
83 }
84
85 #region private methods
86
87 private Type GetEntityType()
88 {
89 Type t;
90 switch (_entId.ObjectClass.DxfName.ToUpper())
91 {
92 case "CIRCLE":
93 t = typeof(Circle);
94 break;
95 case "ARC":
96 t = typeof(Arc);
97 break;
98 case "LINE":
99 t = typeof(Line);
100 break;
101 default:
102 t = typeof(Polyline);
103 break;
104 }
105
106 return t;
107 }
108
109 #endregion
110 }
111 }
Then, here is newly added class that show all the points that snap-able:
1 using System;
2 using System.Collections.Generic;
3 using Autodesk.AutoCAD.ApplicationServices;
4 using Autodesk.AutoCAD.DatabaseServices;
5 using Autodesk.AutoCAD.Geometry;
6 using Autodesk.AutoCAD.GraphicsInterface;
7
8 namespace MeasureDivideWithSnap
9 {
10 public class CustomObjectSnapVisual
11 {
12 private static CustomObjectSnapVisual _instance = null;
13
14 private ObjectId _entId = ObjectId.Null;
15 private IEnumerable<Point3d> _points = null;
16
17 private List<Drawable> _drawables = null;
18 private double _relativeCircleSize = 0.03;
19
20 public static CustomObjectSnapVisual Instance
21 {
22 get
23 {
24 if (_instance == null) _instance = new CustomObjectSnapVisual();
25 return _instance;
26 }
27 }
28
29 public void ShowSnapPoints(
30 ObjectId entId, IEnumerable<Point3d> points,
31 double visualRelativeSize = 0.03)
32 {
33 _entId = entId;
34 _points = points;
35 _relativeCircleSize = visualRelativeSize;
36
37 DrawMeasureGraphics();
38 }
39
40 public void ClearSnapPoints()
41 {
42 TransientManager tMng =
43 TransientManager.CurrentTransientManager;
44
45 tMng.EraseTransients(
46 TransientDrawingMode.DirectTopmost,
47 128, new IntegerCollection());
48
49 DisposeDrawbles();
50 }
51
52 #region private methods
53
54 private List<Drawable> GetMeasureGraphics()
55 {
56 List<Drawable> circles = new List<Drawable>();
57
58 double dia = GetCircleDiameter();
59
60 foreach (var p in _points)
61 {
62 Circle c = new Circle();
63 c.Center = p;
64 c.Radius = dia / 2;
65 c.ColorIndex = 1; //Red in color;
66
67 circles.Add(c);
68 }
69
70 return circles;
71 }
72
73 private void DrawMeasureGraphics()
74 {
75 TransientManager tMng =
76 TransientManager.CurrentTransientManager;
77
78 DisposeDrawbles();
79
80 _drawables = GetMeasureGraphics();
81
82 foreach (var d in _drawables)
83 {
84 tMng.AddTransient(d,
85 TransientDrawingMode.DirectTopmost,
86 128, new IntegerCollection());
87 }
88 }
89
90 private double GetCircleDiameter()
91 {
92 double viewSize = Convert.ToDouble(
93 Application.GetSystemVariable("VIEWSIZE"));
94
95 return viewSize * _relativeCircleSize;
96 }
97
98 private void DisposeDrawbles()
99 {
100 if (_drawables != null)
101 {
102 foreach (var d in _drawables)
103 d.Dispose();
104
105 _drawables = null;
106 }
107 }
108
109 #endregion
110 }
111 }
It is noticeable that the custom object snap classes (and the visual one) are only responsible to create snap-able points based on points argument when the StartSnap() method is called. This make the classes more convenient for reuse: whatever points are supplied, those points become snap-able.
Now come to the 2 custom commands MyMeasure and MyDivide:
1 using System.Collections.Generic;
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(MeasureDivideWithSnap.Commands))]
9
10 namespace MeasureDivideWithSnap
11 {
12 public class Commands
13 {
14 [CommandMethod("MyMeasure")]
15 public static void RunMyMeasureCommand()
16 {
17 Document dwg = Application.DocumentManager.MdiActiveDocument;
18 Editor ed = dwg.Editor;
19
20 ObjectId entId = PickEntity(ed, "measure");
21 if (entId == ObjectId.Null)
22 {
23 ed.WriteMessage("\n*Cancel*");
24 }
25 else
26 {
27 double segmentLength = GetSetmentLength(ed);
28 if (segmentLength == double.MinValue)
29 {
30 ed.WriteMessage("\n*Cancel*");
31 }
32 else
33 {
34 Point3d[] points =
35 CurvePointTool.GetMeasurePoints(entId, segmentLength);
36
37 //Do somethong with object snap for measuring
38 DoSomethingWithCustomSnap(ed, entId, points);
39 }
40 }
41
42 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
43 }
44
45 [CommandMethod("MyDivide")]
46 public static void RunDivideCommand()
47 {
48 Document dwg = Application.DocumentManager.MdiActiveDocument;
49 Editor ed = dwg.Editor;
50
51 ObjectId entId = PickEntity(ed, "divide");
52 if (entId == ObjectId.Null)
53 {
54 ed.WriteMessage("\n*Cancel*");
55 }
56 else
57 {
58 int segmentNumber = GetSegmentNumber(ed);
59 if (segmentNumber == 0)
60 {
61 ed.WriteMessage("\n*Cancel*");
62 }
63 else
64 {
65 Point3d[] points =
66 CurvePointTool.GetDividePoints(entId, segmentNumber);
67
68 //Do something woth object snap for dividing
69 DoSomethingWithCustomSnap(ed, entId, points);
70 }
71 }
72
73 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
74 }
75
76 #region private methods
77
78 private static ObjectId PickEntity(Editor ed, string commandType)
79 {
80 ObjectId entId = ObjectId.Null;
81
82 PromptEntityOptions opt =
83 new PromptEntityOptions(
84 "\nPick a line/polyline/arc/circle to " + commandType + ":");
85 opt.SetRejectMessage(
86 "\nInvalid pick: must be a line/polyline/arc/circle.");
87 opt.AddAllowedClass(typeof(Line), true);
88 opt.AddAllowedClass(typeof(Polyline), true);
89 opt.AddAllowedClass(typeof(Arc), true);
90 opt.AddAllowedClass(typeof(Circle), true);
91
92 PromptEntityResult res = ed.GetEntity(opt);
93
94 if (res.Status == PromptStatus.OK)
95 {
96 entId = res.ObjectId;
97 }
98
99 return entId;
100 }
101
102 private static int GetSegmentNumber(Editor ed)
103 {
104 //Get segment count
105 PromptIntegerOptions iop = new PromptIntegerOptions(
106 "\nEnter segment count: ");
107 iop.AllowNegative = false;
108 iop.AllowNone = false;
109 iop.AllowZero = false;
110
111 PromptIntegerResult ires = ed.GetInteger(iop);
112 if (ires.Status == PromptStatus.OK)
113 {
114 return ires.Value;
115 }
116
117 return 0;
118 }
119
120 private static double GetSetmentLength(Editor ed)
121 {
122 PromptDoubleOptions dop = new PromptDoubleOptions(
123 "\nEnter segment length: ");
124 dop.AllowNegative = false;
125 dop.AllowNone = false;
126 dop.AllowZero = false;
127
128 PromptDoubleResult dres = ed.GetDouble(dop);
129 if (dres.Status == PromptStatus.OK)
130 {
131 return dres.Value;
132 }
133
134 return double.MinValue;
135 }
136
137 private static void DoSomethingWithCustomSnap(
138 Editor ed, ObjectId entId, IEnumerable<Point3d> points)
139 {
140 //Start custom object snap
141 CustomOverruleObjectSnap.Instance.StartSnap(entId, points);
142
143 ////Or we can use CustomGlyphObjectSnao
144 //CustomGlyphObjectSnap.Instance.StartSnap(entId, points);
145
146 //Show all custom object snap points
147 CustomObjectSnapVisual.Instance.ShowSnapPoints(entId, points);
148
149 //Do drafting work here when custom object snap helps
150 PromptPointOptions pOp = new PromptPointOptions(
151 "\nPick point: ");
152 PromptPointResult pres = ed.GetPoint(pOp);
153 if (pres.Status == PromptStatus.OK)
154 {
155 ed.WriteMessage("\nPoint: X={0}, Y={1}",
156 pres.Value.X, pres.Value.Y);
157 }
158 else
159 {
160 ed.WriteMessage("\n*Cancel*");
161 }
162
163 //Stop custom object snap
164 CustomOverruleObjectSnap.Instance.StopSnap();
165
166 ////Or for CustomGlyphObjectSnap
167 //CustomGlyphObjectSnap.Instance.StopSnap();
168
169 //Clear custom snap points visual
170 CustomObjectSnapVisual.Instance.ClearSnapPoints();
171 }
172
173 #endregion
174 }
175 }
See this video clip for the commands in action.
When using CustomOverruleObjectSnap class with AutoCAD 2012, I ran into an issue: if the curve is polyline (LWPOYLINE), moving mouse cursor may cause AutoCAD fatal crash. I currently have access to AutoCAD 2011/2012/2014. Since this only happens with AutoCAD 2012, so it must be some kind of bug with AutoCAD 2012. Well, AutoCAD 2012 is already 2 versions old, I'll not bother reporting this to Autodesk, since AutoCAD 2014 works OK with my code here.
Also, pay attention to the Overrule.Overruling property. It is an static property applied to all loaded overrules prior to AutoCAD 2013. My code is written against AutoCAD 2012. If used for AutoCAD2013 or later, make sure use Overrule.Overruling property properly.