The code in the following two posts was provided by Jeremy Tammik, from our DevTech team in Europe, who presented it at an advanced custom entity workshop he delivered recently in Prague to rave reviews. I've formatted the code to fit the blog and added some commentary plus steps to see it working. Thank you, Jeremy!
Those of you who are familiar with the workings of AutoCAD Architecture - and especially the Object Modeling Framework - will know of the very cool ability for entities to be anchored to one another. This works because graphical ACA classes derive from a "geo" class, which exposes basic location information in a generic way. While AutoCAD entities don't provide this generic location information, it is, however, possible to implement your own anchoring by depending on the location information exposed by specific classes.
The below example does just this: it takes the simple example of anchoring a circle to a line. This technique is especially useful in .NET as it allows us to build in intelligence for standard AutoCAD entities without the need to implement (much more complex) custom entities. Which is why Jeremy was showing it at a custom entity workshop. :-)
Here's the C# code:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
namespace Anchor
{
/// <summary>
/// Anchor command.
///
/// Demonstrate a simple anchoring system.
///
/// In OMF, an anchor is implemented as a custom object
/// which keeps track of the object ids of the host object
/// and the object anchored to the host.
///
/// Here, we implement a simpler mechanism, which maintains
/// lists of host objects and anchored objects with mappings
/// to each other.
///
/// The host objects are lines, and the anchored objects are
/// circles. Any number of circles can be anchored to a line,
/// but a circle can only be anchored to one line at a time.
///
/// The main command prompts the user to select a circle to
/// anchor and a line to host it. From then on, the circle
/// will remain anchored on that line, regardsless how the
/// user tries to move either the line or the circle.
/// Currently, supported manipulations are the MOVE and
/// GRIP_STRETCH commands.
///
/// The implementation is similar to the simpler Reactor
/// sample, and the same principles about cascaded reactors
/// apply.
///
/// We make the command a non-static method, so that each
/// document has its own instance of the command class.
/// </summary>
public class CmdAnchor
{
static List<string> _commandNames =
new List<string>(
new string[] { "MOVE", "GRIP_STRETCH" }
);
private Document _doc;
private Dictionary<ObjectId, List<ObjectId>>
_mapHostToAnchored;
private Dictionary<ObjectId, ObjectId> _mapAnchoredToHost;
private static ObjectIdCollection _ids;
private static List<double> _pos;
Editor Ed
{
get
{
return _doc.Editor;
}
}
public CmdAnchor()
{
_doc =
Application.DocumentManager.MdiActiveDocument;
_doc.CommandWillStart +=
new CommandEventHandler(doc_CommandWillStart);
_mapHostToAnchored =
new Dictionary<ObjectId, List<ObjectId>>();
_mapAnchoredToHost =
new Dictionary<ObjectId, ObjectId>();
_ids = new ObjectIdCollection();
_pos = new List<double>();
Ed.WriteMessage(
"Anchors initialised for '{0}'. ",
_doc.Name
);
}
bool selectEntity(Type t, out ObjectId id)
{
id = ObjectId.Null;
string name = t.Name.ToLower();
string prompt =
string.Format("Please select a {0}: ", name);
string msg =
string.Format(
"Selected entity is not a {0}, please try again...",
name
);
PromptEntityOptions optEnt =
new PromptEntityOptions(prompt);
optEnt.SetRejectMessage(msg);
optEnt.AddAllowedClass(t, true);
PromptEntityResult resEnt =
Ed.GetEntity(optEnt);
if (PromptStatus.OK == resEnt.Status)
{
id = resEnt.ObjectId;
}
return !id.IsNull;
}
/// <summary>
/// Command to define an anchor between a selected host
/// line and an anchored circle.
/// </summary>
[CommandMethod("ANCHOR")]
public void Anchor()
{
ObjectId hostId, anchoredId;
if (selectEntity(typeof(Line), out hostId)
&& selectEntity(typeof(Circle), out anchoredId))
{
// Check for previously stored anchors:
if (_mapAnchoredToHost.ContainsKey(anchoredId))
{
Ed.WriteMessage("Previous anchor removed.");
ObjectId oldHostId =
_mapAnchoredToHost[anchoredId];
_mapAnchoredToHost.Remove(anchoredId);
_mapHostToAnchored[oldHostId].Remove(anchoredId);
}
// Add new anchor data:
if (!_mapHostToAnchored.ContainsKey(hostId))
{
_mapHostToAnchored[hostId] =
new List<ObjectId>();
}
_mapHostToAnchored[hostId].Add(anchoredId);
_mapAnchoredToHost.Add(anchoredId, hostId);
// Ensure that anchored object is located on host:
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
Line line =
t.GetObject(hostId, OpenMode.ForRead)
as Line;
Circle circle =
t.GetObject(anchoredId, OpenMode.ForWrite)
as Circle;
Point3d ps = line.StartPoint;
Point3d pe = line.EndPoint;
LineSegment3d segment =
new LineSegment3d(ps, pe);
Point3d p = circle.Center;
circle.Center =
segment.GetClosestPointTo(p).Point;
t.Commit();
}
}
}
void doc_CommandWillStart(
object sender,
CommandEventArgs e
)
{
if (_commandNames.Contains(e.GlobalCommandName))
{
_ids.Clear();
_pos.Clear();
_doc.Database.ObjectOpenedForModify +=
new ObjectEventHandler(_db_ObjectOpenedForModify);
_doc.CommandCancelled +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed +=
new CommandEventHandler(_doc_CommandEnded);
}
}
void removeEventHandlers()
{
_doc.CommandCancelled -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed -=
new CommandEventHandler(_doc_CommandEnded);
_doc.Database.ObjectOpenedForModify -=
new ObjectEventHandler(_db_ObjectOpenedForModify);
}
void _doc_CommandEnded(
object sender,
CommandEventArgs e
)
{
// Remove database reactor before restoring positions
removeEventHandlers();
rollbackLocations();
}
void saveLocation(
ObjectId hostId,
ObjectId anchoredId,
bool hostModified
)
{
if (!_ids.Contains(anchoredId))
{
// If the host was moved, remember the location of
// the anchored object on the host so we can restore
// it.
// If the anchored object was moved, we do not need
// to remember its location of the anchored object,
// because we will simply snap back the the host
// afterwards.
double a = double.NaN;
if (hostModified)
{
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
Line line =
t.GetObject(hostId, OpenMode.ForRead)
as Line;
Circle circle =
t.GetObject(anchoredId, OpenMode.ForRead)
as Circle;
{
Point3d p = circle.Center;
Point3d ps = line.StartPoint;
Point3d pe = line.EndPoint;
double lineLength = ps.DistanceTo(pe);
double circleOffset = ps.DistanceTo(p);
a = circleOffset / lineLength;
}
t.Commit();
}
}
_ids.Add(anchoredId);
_pos.Add(a);
}
}
void _db_ObjectOpenedForModify(
object sender,
ObjectEventArgs e
)
{
ObjectId id = e.DBObject.Id;
if (_mapAnchoredToHost.ContainsKey(id))
{
Debug.Assert(
e.DBObject is Circle,
"Expected anchored object to be a circle"
);
saveLocation(_mapAnchoredToHost[id], id, false);
}
else if (_mapHostToAnchored.ContainsKey(id))
{
Debug.Assert(
e.DBObject is Line,
"Expected host object to be a line"
);
foreach (ObjectId id2 in _mapHostToAnchored[id])
{
saveLocation(id, id2, true);
}
}
}
void rollbackLocations()
{
Debug.Assert(
_ids.Count == _pos.Count,
"Expected same number of ids and locations"
);
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
int i = 0;
foreach (ObjectId id in _ids)
{
Circle circle =
t.GetObject(id, OpenMode.ForWrite)
as Circle;
Line line =
t.GetObject(
_mapAnchoredToHost[id],
OpenMode.ForRead
) as Line;
Point3d ps = line.StartPoint;
Point3d pe = line.EndPoint;
double a = _pos[i++];
if (a.Equals(double.NaN))
{
LineSegment3d segment =
new LineSegment3d(ps, pe);
Point3d p = circle.Center;
circle.Center =
segment.GetClosestPointTo(p).Point;
}
else
{
circle.Center = ps + a * (pe - ps);
}
}
t.Commit();
}
}
}
}
To see how it works, draw a circle and a line:
Call the ANCHOR command, selecting the line and then the circle. The circle gets moved such that its centre is at the closest point on the line:
Now we can grip-stretch the top point on the line:
And the circle snaps back onto the line:
If we grip-stretch the circle away from the line...
And the circle then snaps back to be on the line, but at a different point (the closest to the one selected):
If you're interested in other posts demonstrating the use of reactors to anchor entities, this series of posts shows how to link circles together: Linking Circles Parts 1, 2, 3, 4 & 5.