August 03, 2009

Knowing when an AutoCAD object is grip-edited using overrules in .NET

This week I will mostly be posting about Overrules. [For those of you who haven’t seen The Fast Show (called Brilliant in the US), this is an obscure reference to a character named Jesse. :-)]

Aside from this post, Stephen Preston has sent me the samples he’s put together for his AU class on this topic, so expect some serious plagiarism (although now that I’ve given him credit I suppose it’s not really plagiarism :-).

Here’s a question I received recently by email:

Is there some posibility to write something in some of your next blogs about how to get coordinate of grip when user move on screen in realtime. By using grip_stretch command...

Example: we first draw polyline, then select it and move grip point...how we can know current position for grip coordinate before user pick point...in realtime...

Coincidentally I’ve been working on an internal project that uses Overrules to achieve this (or something quite similar). As I expect I’ve said before, AutoCAD 2010’s Overrule API is an incredibly powerful mechanism for hooking into and controlling object behaviour: it’s essentially our approach for providing the equivalent of custom object support to .NET programmers (which was the top item for a number of years on AutoCAD’s API wishlist).

To hook into object modification inside AutoCAD, we have a couple of options:

A TransformOverrule allows us to hook into an object’s TransformBy(), which tells use when it’s rotated, scaled or moved.

This is effective at trapping object-level transformations, whether via grips or commands such as MOVE, but won’t tell you when a specific vertex is modified (for instance)

A GripOverrule allows us to hook into GetGripPointsAt() and MoveGripPointsAt(), which can tell use when a grip-stretch is performed on our object.

This gives us more fine-grained information on a per-grip basis, but clearly won’t be called for commands such as MOVE which bypass the use of grips.

In this particular instance it makes more sense to use a GripOverrule, as we need to know when a particular polyline vertex is edited.

Here’s some C# code that implements a GripOverrule for AutoCAD entities (it works just as well for any entity with grips implemented via GetGripPointsAt() & MoveGripPointsAt(), so there’s no need to limit it just to Polylines, even if we could very easily).

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 GripOverruleTest

{

publicclassGripVectorOverrule : GripOverrule

{

// A static pointer to our overrule instance

staticpublicGripVectorOverrule theOverrule =

newGripVectorOverrule();

// A flag to indicate whether we're overruling

staticbool overruling = false;

// A single set of grips would not have worked in

// the case where multiple objects were selected.

staticDictionary<string, Point3dCollection> _gripDict =

newDictionary<string, Point3dCollection>();

public GripVectorOverrule()

{

}

privatestring GetKey(Entity e)

{

// Generate a key based on the name of the object's type

// and its geometric extents

// (We cannot use the ObjectId, as this is null during

// grip-stretch operations.)

return e.GetType().Name + ":" + e.GeometricExtents.ToString();

}

// Save the locations of the grips for a particular entity

privatevoid StoreGripInfo(Entity e, Point3dCollection grips)

{

string key = GetKey(e);

if (_gripDict.ContainsKey(key))

{

// Clear the grips if any already associated

Point3dCollection grps = _gripDict[key];

using (grps)

{

grps.Clear();

}

_gripDict.Remove(key);

}

// Now we add our grips

Point3d[] pts = newPoint3d[grips.Count];

grips.CopyTo(pts, 0);

Point3dCollection gps = newPoint3dCollection(pts);

_gripDict.Add(key, gps);

}

// Get the locations of the grips for an entity

privatePoint3dCollection RetrieveGripInfo(Entity e)

{

Point3dCollection grips = null;

string key = GetKey(e);

if (_gripDict.ContainsKey(key))

{

grips = _gripDict[key];

}

return grips;

}

publicoverridevoid GetGripPoints(

Entity e,

Point3dCollection grips,

IntegerCollection snaps,

IntegerCollection geomIds

)

{

base.GetGripPoints(e, grips, snaps, geomIds);

StoreGripInfo(e, grips);

}

publicoverridevoid MoveGripPointsAt(

Entity e,

IntegerCollection indices,

Vector3d offset

)

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

Point3dCollection grips = RetrieveGripInfo(e);

if (grips != null)

{

// Could get multiple points moved at once,

// hence the integer collection

foreach (int i in indices)

{

// Get the grip point from our internal state

Point3d pt = grips[i];

// Draw a vector from the grip point to the newly

// offset location, using the index into the

// grip array as the color (excluding colours 0 and 7).

// These vectors don't getting cleared, which makes

// for a fun effect.

ed.DrawVector(

pt,

pt + offset,

(i >= 6 ? i + 2 : i + 1), // exclude colours 0 and 7

false

);

}

}

base.MoveGripPointsAt(e, indices, offset);

}

[CommandMethod("GOO")]

publicvoid GripOverruleOnOff()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

if (overruling)

{

ObjectOverrule.RemoveOverrule(

RXClass.GetClass(typeof(Entity)),

GripVectorOverrule.theOverrule

);

}

else

{

ObjectOverrule.AddOverrule(

RXClass.GetClass(typeof(Entity)),

GripVectorOverrule.theOverrule,

true

);

}

overruling = !overruling;

GripOverrule.Overruling = overruling;

ed.WriteMessage(

"\nGrip overruling turned {0}.",

(overruling ? "on" : "off")

);

}

}

}

One important thing to bear in mind about this sample: we have to store the grips for a particular entity during the GetGripPointsAt() call and then use them during the MoveGripPointsAt() call. This is complicated for a number of reasons...

Firstly, we can’t just store the points in a single point collection, as there could be multiple calls to GetGripPointsAt() (while the grips are retrieved and displayed by AutoCAD) followed by multiple calls to MoveGripPointsAt() (while the grips are used to manipulate the objects). So we really need to map a set of points to a particular object.

The really tricky thing is that we have an entity passed into both GetGripPointsAt() and MoveGripPointsAt(), but – and here’s the rub – the GetGripPointsAt() entity is typically the original, database-dependent entity, while the entities passed into MoveGripPointsAt() are temporary clones. The temporary clones do not have ObjectIds, which is the best way to identify objects and associate data with them in a map (for instance).

The default cloning behaviour can be overruled using a TransformOverrule – by returning false from the CloneMeForDragging() callback – but assuming we don’t do that (and we don’t really want to do that, as it opens another can of worms), we need to find another way to identify an object that is passed into GetGripPointsAt() with its clone passed in to MoveGripPointsAt().

The solution I ended up going for was to generate a key based on the class-name of the object followed by its geometric extents. This isn’t perfect, but it’s as good as I could find. There is a possibility that entities of the same type could exist at the same spot and still have different grips (which is the main problem – if they have the same grip locations then it doesn’t matter), but that will cause problems with this implementation. If people want to implement a more robust technique, they will probably need to do so for specific entity types, where they have access to more specific information that will help identify their objects: we are sticking to generic entity-level information, which makes it a little hard to be 100% certain we have the right object.

One other point… as we’re using the geometric extents to identify an object, it’s hard to clear the grip information from our dictionary: by the time the OnGripStatusChanged() method is called, the entity’s geometry has typically changed, which means we can no longer find it in the dictionary. So for a cleaner solution it’s worth clearing the dictionary at an appropriate moment (probably using some kind of command- or document locking-event).

OK, now let’s see what happens when we NETLOAD our application and run our GOO command, which toggles the use of the grip overrule:

Command: GOO

Grip overruling turned on.

OK, so far so good. Let’s create a standard AutoCAD polyline containing both line and arc segments:

When we select it, we see its grips along with the quick properties panel:

And when we use its various grips, we see temporary vectors drawn during each MoveGripPointsAt() call (which will disappear at the next REGEN):

Fun stuff! I’m looking forward to diving further into Overrules over the next week or two… :-)