This is a really interesting topic. At least I think it is – hopefully at least some of you will agree. :-)
The requirement was to create selectable – or at least manipulatable – transient graphics inside AutoCAD’s drawing canvas. As many of you are probably aware, transient graphics are not hooked into AutoCAD’s selection mechanism. This is mostly fine, but if you want to implement a ViewCube-like gizmo that manipulates the view or drawing settings in some way, it’s hard to do so without the ability to react to the current cursor position is and what’s happening with the mouse.
When my esteemed colleague, Christer Janson, first pointed me at the internal C++ protocol extension that allows you to receive Windows messages and point input inside your application, I assumed there was no chance I’d be able to get this working in .NET (as it’s not exposed through the public ObjectARX API). But after some digging and head-scratching, I managed to find how it had been exposed via the .NET API and create a sample that makes use of it.
Here’s the C# code, showing how to use this very interesting mechanism:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
namespace TransientSelection
{
public class SelectableTransient : Transient
{
// Windows messages we care about
const int WM_LBUTTONDOWN = 513;
const int WM_LBUTTONUP = 514;
// Internal state
Entity _ent = null;
bool _picked = false, _clicked = false;
public SelectableTransient(Entity ent)
{
_ent = ent;
}
protected override int SubSetAttributes(DrawableTraits traits)
{
// If the cursor is over the entity, make it colored
// (whether it's red or yellow will depend on whether
// there's a mouse-button click, too)
traits.Color = (short)(_picked ? (_clicked ? 1 : 2) : 0);
return (int)DrawableAttributes.None;
}
protected override void SubViewportDraw(ViewportDraw vd)
{
_ent.ViewportDraw(vd);
}
protected override bool SubWorldDraw(WorldDraw wd)
{
_ent.WorldDraw(wd);
return true;
}
protected override void OnDeviceInput(DeviceInputEventArgs e)
{
bool redraw = false;
if (e.Message == WM_LBUTTONDOWN)
{
_clicked = true;
// If we're over the entity, absorb the click
// (stops the window selection from happening)
if (_picked)
{
e.Handled = true;
}
redraw = true;
}
else if (e.Message == WM_LBUTTONUP)
{
_clicked = false;
redraw = true;
}
// Only update the graphics if things have changed
if (redraw)
{
TransientManager.CurrentTransientManager.UpdateTransient(
this, new IntegerCollection()
);
// Force a Windows message, as we may have absorbed the
// click event (and this also helps when unclicking)
ForceMessage();
}
base.OnDeviceInput(e);
}
private void ForceMessage()
{
// Set the cursor without ectually moving it - enough to
// generate a Windows message
System.Drawing.Point pt =
System.Windows.Forms.Cursor.Position;
System.Windows.Forms.Cursor.Position =
new System.Drawing.Point(pt.X, pt.Y);
}
protected override void OnPointInput(PointInputEventArgs e)
{
bool wasPicked = _picked;
_picked = false;
Curve cv = _ent as Curve;
if (cv != null)
{
Point3d pt =
cv.GetClosestPointTo(e.Context.ComputedPoint, false);
if (
pt.DistanceTo(e.Context.ComputedPoint) <= 0.1
// Tolerance.Global.EqualPoint is too small
)
{
_picked = true;
}
}
// Only update the graphics if things have changed
if (_picked != wasPicked)
{
TransientManager.CurrentTransientManager.UpdateTransient(
this, new IntegerCollection()
);
}
base.OnPointInput(e);
}
}
public class Commands
{
Line _ln = null;
SelectableTransient _st = null;
[CommandMethod("TRS")]
public void TransientSelection()
{
// Create a line and pass it to the SelectableTransient
// This makes cleaning up much more straightforward
_ln = new Line(Point3d.Origin, new Point3d(10, 10, 0));
_st = new SelectableTransient(_ln);
// Tell AutoCAD to call into this transient's extended
// protocol when appropriate
Transient.CapturedDrawable = _st;
// Go ahead and draw the transient
TransientManager.CurrentTransientManager.AddTransient(
_st, TransientDrawingMode.DirectShortTerm,
128, new IntegerCollection()
);
}
[CommandMethod("TRU")]
public void RemoveTransientSelection()
{
// Removal is performed by setting to null
Transient.CapturedDrawable = null;
// Erase the transient graphics and dispose of the transient
if (_st != null)
{
TransientManager.CurrentTransientManager.EraseTransient(
_st,
new IntegerCollection()
);
_st.Dispose();
_st = null;
}
// And dispose of our line
if (_ln != null)
{
_ln.Dispose();
_ln = null;
}
}
}
}
When you run the TRS command, you’ll see a line between the origin and the point 10,10. This is transient geometry, although not as we know it. ;-)
When you move your cursor across the line (to within 0.1 of a drawing unit, which I chose as the standard geometric tolerance was way too small to be usable when dealing with point input in this way) it should turn yellow:
And when you click the left-button of the mouse, it should turn red:
In itself this isn’t anything very impressive, but it provides the underpinnings to create something much more so. You’re essentially now able to create a transient gizmo that can react to mouse movement and button clicking, and in turn manipulate state in your AutoCAD session, in some way.
I’ve provided a TRU command to remove graphics and dispose of everything properly. You would probably want to take care of all this in your own application initialization and termination code, of course.
Incidentally there are optimisations that are probably worth making to this particular implementation: for instance, as this mechanism could end up being used by users throughout their typical drawing session – much in the way the ViewCube is – I’d suggest checking the cursor position against an area of the drawing that you hold in memory, to stop always checking it against the geometry itself (as I’ve done in this sample). I’m sure you’ll find out other ways to optimise the use of this mechanism as you work through the specifics of your own implementation.