Over the weekend I put together a little prototype to prove a concept for an internal project I’m working on. The idea was to force a point onto a curve (meaning anything inheriting from Curve in AutoCAD, such as Arc, Circle, Ellipse, Leader, Line, Polyline, Polyline2d, Polyline3d, Ray, Spline, Xline…), so that when the point is moved it snaps onto the curve to which it’s assigned. The solution I’ve put together is far from being complete – which is partly why I’m planning on making this a series, so I can flesh it out a little further in further posts – but it does demonstrate a reasonable technique for addressing the requirement.
The approach I chose was to use a TransformOverrule to modify the standard AutoCAD point’s TransformBy() behaviour (see this previous post for some commentary on using a TransformOverrule vs. a GripOverrule). Our TransformOverrule stores a list of curves that have had points attached to them, and during TransformBy() we check each one to see which curve this point was on. We then get the transformed point (i.e. the one being chosen by the user) and from there we get the closest point on that curve, which becomes the point’s new location.
TransformBy() is a pretty handy operation to overrule: it’s used by grip-editing and by the MOVE command, so you know these operations will lead to your object’s positional integrity being maintained (direct modification of properties, such as via the Properties Palette, won’t lead to it being called, however, so it’s not enough if you need to maintain complete control).
Some comments on the choice of storing a list of curves rather than some other association between the point and the curve:
- Storing a map between the point (via its ObjectId) and the curve wouldn’t work, as TransformBy() often has to work on a temporary copy of an object, rather than the object itself (and the copy’s ObjectId will therefore be Null).
- We might also have looked at an approach such as the one used previously in our GripOverrule.
- It might be possible to attach data (perhaps XData) to the point identifying the curve it’s attached to, but this would need to be available on the temporary clone (and is something that I’d need to check works).
- It’s possible that working through a list of curves, checking each one, could become perceptibly slow if working with huge sets of data, but at that point a more efficient spatial indexing technique could be adopted.
- Using a list of curves also allows us to modify the implementation to allow a point to travel along a network of curves (we’ll go through this modification in a future post).
- One drawback of this approach is that once we move the curve independently from the point, they become detached as the point is no longer on any of the curves. We’ve put a specific clause in to allow points not on curves to be moved, but it would also be good to have the point be transformed along with the curve, so they stay together (another potential future modification). In the meantime the user will have to move the point back onto the curve (using the NEAr object snap, to make sure it’s precise) for the overrule to work for it, again.
One last point: we’re not persisting the curve list, in any way, so don’t expect points to reattach to lines when a session is restarted (until the POC command is used to create further points on curves and therefore add the curves to the list of those being managed by our system).
OK, enough blather, let’s get on with looking at the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
namespace PointOnCurveTest
{
public class PtTransOverrule : TransformOverrule
{
// A static pointer to our overrule instance
static public PtTransOverrule theOverrule =
new PtTransOverrule();
// A list of the curves that have had points
// attached to
static internal List<ObjectId> _curves =
new List<ObjectId>();
// A flag to indicate whether we're overruling
static bool overruling = false;
public PtTransOverrule() {}
// Out primary overruled function
public override void TransformBy(Entity e, Matrix3d mat)
{
// We only care about points
DBPoint pt = e as DBPoint;
if (pt != null)
{
Database db = HostApplicationServices.WorkingDatabase;
// For each curve, let's check whether our point is on it
bool found = false;
// We're using an Open/Close transaction, to avoid problems
// with us using transactions in an event handler
OpenCloseTransaction tr =
db.TransactionManager.StartOpenCloseTransaction();
using (tr)
{
foreach (ObjectId curId in _curves)
{
DBObject obj = tr.GetObject(curId, OpenMode.ForRead);
Curve cur = obj as Curve;
if (cur != null)
{
Point3d ptOnCurve =
cur.GetClosestPointTo(pt.Position, false);
Vector3d dist = ptOnCurve - pt.Position;
if (dist.IsZeroLength(Tolerance.Global))
{
Point3d pos =
cur.GetClosestPointTo(
pt.Position.TransformBy(mat),
false
);
pt.Position = pos;
found = true;
break;
}
}
}
// If the point isn't on any curve, let the standard
// TransformBy() do its thing
if (!found)
{
base.TransformBy(e, mat);
}
}
}
}
[CommandMethod("POC")]
public void CreatePointOnCurve()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Ask the user to select a curve
PromptEntityOptions opts =
new PromptEntityOptions(
"\nSelect curve at the point to create: "
);
opts.SetRejectMessage(
"\nEntity must be a curve."
);
opts.AddAllowedClass(typeof(Curve), false);
PromptEntityResult per = ed.GetEntity(opts);
ObjectId curId = per.ObjectId;
if (curId != ObjectId.Null)
{
// Let's make sure we'll be able to see our point
db.Pdmode = 97; // square with a circle
db.Pdsize = -10; // relative to the viewport size
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
DBObject obj =
tr.GetObject(curId, OpenMode.ForRead);
Curve cur = obj as Curve;
if (cur != null)
{
// Out initial point should be the closest point
// on the curve to the one picked
Point3d pos =
cur.GetClosestPointTo(per.PickedPoint, false);
DBPoint pt = new DBPoint(pos);
// Add it to the same space as the curve
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
cur.BlockId,
OpenMode.ForWrite
);
ObjectId ptId = btr.AppendEntity(pt);
tr.AddNewlyCreatedDBObject(pt, true);
}
tr.Commit();
// And add the curve to our central list
_curves.Add(curId);
}
// Turn on the transform overrule if it isn't already
if (!overruling)
{
ObjectOverrule.AddOverrule(
RXClass.GetClass(typeof(DBPoint)),
PtTransOverrule.theOverrule,
true
);
overruling = true;
TransformOverrule.Overruling = true;
}
}
}
}
}
Now let’s see what happens when we run the POC command (for Point On Curve, but then it’s also a Proof Of Concept – geddit? :-).
The POC command will create a point at the selected location on a curve, at which point we can grip-edit it:
We can see that as we move the grip point around the drawing, the closest point to the attached curve is always used:
The point is always on the original curve, even if we try to select a point on another curve (even one that may be on the list of curves maintained by our PtTransOverrule class, if the POC command has been used on other curves):
That’s enough to get us started. Later in the week we’ll look at extending the code to create a stronger attachment between the point and the curve, but also to allow the point to travel along a network of curves. Fun, fun, fun! :-)