This post is going to focus on something slightly different: it’s going to look at making the points added to a particular curve be associative to that curve – i.e. travel along with it as the curve is moved – and in the process we’re going to adjust the way we link between our objects, by moving the information into an object’s Extended Entity Data (XData). This does a few things: firstly it breaks our network-related technique (never fear – we’ll be able to get it back, in due course :-) by removing the idea of a central list of curves. It also allows us to have points that are connected to curves, and points that are not (which is clearly desirable). It also lays a more solid foundation for other operations, such as erasing the points on a curve when the curve itself is erased. All good stuff.

Just to be clear: there are lots of ways ways to approach this problem – this is just another possible mechanism – so you might be interested in checkingoutthispreviousseries, which addressed a similar problem a little differently.

For now let’s see this basic implementation of our XData-related code. Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

namespace PointOnCurveTest

{

publicclassLinkApplication

{

staticstring regAppName = "TTIF_PTS";

publicclassLinkTransOverrule : TransformOverrule

{

staticbool _transforming = false;

// A static pointer to our overrule instance

staticpublicLinkTransOverrule theOverrule =

newLinkTransOverrule();

// A flag to indicate whether we're overruling

staticprivatebool overruling = false;

// Our overrule should only be applied to objects

// carrying our XData

public LinkTransOverrule()

{

SetXDataFilter(regAppName);

}

// Turn on the transform overrule if it isn't already

publicstaticvoid TurnOn()

{

if (!overruling)

{

ObjectOverrule.AddOverrule(

RXClass.GetClass(typeof(Entity)),

LinkTransOverrule.theOverrule,

true

);

overruling = true;

TransformOverrule.Overruling = true;

}

}

// Turn off the transform overrule if it's on

publicstaticvoid TurnOff()

{

if (overruling)

{

ObjectOverrule.RemoveOverrule(

RXClass.GetClass(typeof(Entity)),

LinkTransOverrule.theOverrule

);

overruling = false;

TransformOverrule.Overruling = false;

}

}

// Our primary overruled function

publicoverridevoid TransformBy(Entity e, Matrix3d mat)

{

// If we're already transforming, don't re-enter, just

// super-message to the base

if (_transforming)

base.TransformBy(e, mat);

else

{

Database db = HostApplicationServices.WorkingDatabase;

// Otherwise get our linked objects: check whether

// there are any

ObjectIdCollection ids = GetLinkedObjects(e);

if (ids.Count > 0)

{

// If we're dealing with a point...

DBPoint pt = e asDBPoint;

if (pt != null)

{

// Work through the curves to find the closest to our

// transformed point

double min = 0.0;

Point3d bestPt = Point3d.Origin;

bool first = true;

ObjectId bestId = ObjectId.Null;

// 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)

{

// For now linked objects can be curves, else

// they'll be ignored

Curve cur;

foreach (ObjectId curId in ids)

{

DBObject obj =

tr.GetObject(curId, OpenMode.ForRead, true);

// If our linked object was erased, remove it

// from this object's links

if (obj.IsErased)

{

RemoveLinkedObject(e, curId);

}

else

{

// Otherwise we get the closest point on it

// to our point, to compare with others

cur = obj asCurve;

if (cur != null)

{

Point3d ptLoc =

pt.Position.TransformBy(mat);

Point3d ptOnCurve =

cur.GetClosestPointTo(ptLoc, false);

Vector3d dist = ptOnCurve - ptLoc;

if (first || dist.Length < min)

{

first = false;

min = dist.Length;

bestPt = ptOnCurve;

bestId = curId;

}

}

}

}

// If we didn't find a point, super-message

// (will transform the point as normal)

if (first)

base.TransformBy(e, mat);

else

pt.Position = bestPt;

}

}

if (e isCurve)

{

// Automatically super-message: we're not changing the

// transform behaviour of the curve, only of the

// linked objects

base.TransformBy(e, mat);

// Get each linked object and transform it along with

// the "parent"

OpenCloseTransaction tr =

db.TransactionManager.StartOpenCloseTransaction();

using (tr)

{

foreach (ObjectId id in ids)

{

DBObject obj = tr.GetObject(id, OpenMode.ForWrite);

Entity ent = obj asEntity;

if (ent != null)

{

_transforming = true;

ent.TransformBy(mat);

_transforming = false;

}

}

tr.Commit();

}

}

}

}

}

}

// Function to add a reference between one object and another

// (stored in an object's XData)

publicstaticvoid AddLinkedObject(DBObject obj, ObjectId id)

{

Database db =

HostApplicationServices.WorkingDatabase;

// First we need to make sure our application name is

// in the Registered Application Table

OpenCloseTransaction tr =

db.TransactionManager.StartOpenCloseTransaction();

using (tr)

{

RegAppTable rat =

(RegAppTable)tr.GetObject(

db.RegAppTableId, OpenMode.ForRead

);

if (!rat.Has(regAppName))

{

rat.UpgradeOpen();

RegAppTableRecord ratr =

newRegAppTableRecord();

ratr.Name = regAppName;

tr.AddNewlyCreatedDBObject(ratr, true);

rat.Add(ratr);

}

tr.Commit();

}

// Get our object's current XData

ResultBuffer rb = obj.XData;

// Check the XData, to see whether our link already exists

bool foundLink = false;

if (rb != null)

{

bool foundStart = false;

foreach (TypedValue tv in rb)

{

// The first TypedValue is our application name

if (tv.TypeCode ==

(int)DxfCode.ExtendedDataRegAppName &&

tv.Value.ToString() == regAppName)

foundStart = true;

else

{

if (foundStart)

{

// Our links will all have the same code (1005)

if (tv.TypeCode == (int)DxfCode.ExtendedDataHandle)

{

if (id.Handle.ToString() == tv.Value.ToString())

foundLink = true;

}

}

}

}

}

// If we didn't find the link, add it

if (!foundLink)

{

// This is our link

TypedValue tv2 =

newTypedValue(

(int)DxfCode.ExtendedDataHandle, id.Handle

);

// If there was no previous XData, create the ResultBuffer

if (rb == null)

{

rb =

newResultBuffer(

newTypedValue(

(int)DxfCode.ExtendedDataRegAppName,

regAppName

),

tv2

);

}

else

{

// Add our TypeValue to an existing ResultBuffer

rb.Add(tv2);

}

// Set the XData on our object

obj.XData = rb;

}

rb.Dispose();

}

// Function to remove a reference from one object to another

// (stored in an object's XData)

publicstaticvoid RemoveLinkedObject(DBObject obj, ObjectId id)

{

// Get the existing XData

ResultBuffer oldRb = obj.XData;

// Create a ResultBuffer for the "filtered" XData

// (will contain all the old links, except the one we want

// to remove)

ResultBuffer newRb =

newResultBuffer(

newTypedValue(

(int)DxfCode.ExtendedDataRegAppName,

regAppName)

);

bool foundLink = false;

if (oldRb != null)

{

bool foundStart = false;

// Loop through the XData

foreach (TypedValue tv in oldRb)

{

// The first TypedValue is our application name

if (tv.TypeCode ==

(int)DxfCode.ExtendedDataRegAppName &&

tv.Value.ToString() == regAppName)

foundStart = true;

else

{

if (foundStart)

{

// Our links will all have the same code (1005)

if (tv.TypeCode == (int)DxfCode.ExtendedDataHandle)

{

// If the link is not the one we want to remove,

// add it to our list of XData to retain,

// otherwise set a flag to say we found it

if (id.Handle.ToString() != tv.Value.ToString())

newRb.Add(tv);

else

foundLink = true;

}

}

}

}

}

// If we found the link (and therefore have XData to set)

// change the object's XData

if (foundLink)

obj.XData = newRb;

// Clean-up after ourselves

oldRb.Dispose();

newRb.Dispose();

}

// Function to retrieve the references from one object to another

// (stored in an object's XData)

publicstaticObjectIdCollection GetLinkedObjects(DBObject obj)

{

Database db =

HostApplicationServices.WorkingDatabase;

// Will return an ObjectIdCollection

ObjectIdCollection ids = newObjectIdCollection();

// Get the object's XData

using (ResultBuffer rb = obj.XData)

{

if (rb != null)

{

bool foundStart = false;

foreach (TypedValue tv in rb)

{

// The first TypedValue is our application name

if (tv.TypeCode ==

(int)DxfCode.ExtendedDataRegAppName &&

tv.Value.ToString() == regAppName)

foundStart = true;

else

{

if (foundStart)

{

// Our links will all have the same code (1005)

if (tv.TypeCode == (int)DxfCode.ExtendedDataHandle)

{

// Get the long integer from the string stored in

// our XData, and the Handle from the long

long lg =

System.Convert.ToInt64(tv.Value.ToString(), 16);

Handle hd = newHandle(lg);

// And then the ObjectId from the Handle

ObjectId id = db.GetObjectId(false, hd, -1);

// Add it to our list, if it isn't already in it

if (!ids.Contains(id))

ids.Add(id);

}

}

}

}

}

}

return ids;

}

[CommandMethod("POC")]

publicvoid CreatePointOnCurve()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

// Ask the user to select a curve

PromptEntityOptions opts =

newPromptEntityOptions(

"\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 asCurve;

if (cur != null)

{

// Our initial point should be the closest point

// on the curve to the one picked

Point3d pos =

cur.GetClosestPointTo(per.PickedPoint, false);

DBPoint pt = newDBPoint(pos);

// Add it to the same space as the curve

BlockTableRecord btr =

(BlockTableRecord)tr.GetObject(

cur.BlockId,

OpenMode.ForWrite

);

ObjectId ptId = btr.AppendEntity(pt);

AddLinkedObject(pt, curId);

cur.UpgradeOpen();

AddLinkedObject(cur, ptId);

tr.AddNewlyCreatedDBObject(pt, true);

}

tr.Commit();

LinkTransOverrule.TurnOn();

}

}

}

}

}

Some points to note about this implementation:

We now register the overrule to work for all entity types, but at the same time we tell AutoCAD to filter on our Register Application Name

Only objects with our XData will cause the overrule to be called

Our TransformBy() function will therefore apply to all entities with our XData, although we’re only (currently) interested in DBPoints and Curves

We do need to stop the function re-entering for the points we transform along with the parent curve, but that’s a simple matter of setting/checking a boolean flag

We have kept the technique of checking a number of curves, to see which is closest, but as we’re only adding the XData during the POC command – which currently only allows selection of a single curve – the behaviour is now once again similar to the first post in the series (although this is very easy to change)

Now let’s see what happens when our code is executed. We’ll take the same drawing as last time – which contains a Spline and a Polyline – and use the POC command to add a few points on each:

When we move the Spline, we see the points on it now move along with it:

What’s interesting is that when we do the same for a Polyline, the points don’t appear during the drag…

… but they do, indeed, show up in the right place once the Polyline has completed the move to its destination:

This is apparently due to optimization inside AutoCAD: during a recent discussion with our Engineering team I learned that during certain operations – such as when a modification to an object or set of objects can be represented by a standard transformation (such as scale, rotate or move) – AutoCAD captures the graphics of those objects and then performs a “sprite” optimization to move the object(s), rather than continually asking the object(s) to participate in the movement by drawing their own graphics or by transforming their internal state.

It’s not fully clear to me why this is happening in this particular situation – and with the Polyline rather than the Spline – but it does appear that this optimization has been turned on during the movement of the Polyline. This only occurs in very specific situations – such as in 3D rather than 2D – but it does seem something that would be valuable for applications to have more explicit control over.

That’s it for today’s post. I’m actually on vacation this week, but I do have something to share later in the week related to our “Plugin on the Month” initiative…