A blog for developers programming with Autodesk platforms, particularly AutoCAD and Forge. With a special focus on AR/VR and IoT.

August 13, 2008

Rolling back the effect of AutoCAD commands using .NET

Another big thank you to Jeremy Tammik, from our DevTech team in Europe, for providing this elegant sample. This is another one Jeremy presented at the recent advanced custom entity workshop in Prague. I have added some initial commentary as well as some steps to see the code working. Jeremy also provided the code for the last post.

We sometimes want to stop entities from being modified in certain ways, and there are a few different approaches possible, for instance: at the simplest - and least granular - level, we can place entities on locked layers or veto certain commands using an editor reactor. Or we can go all-out and implement custom objects that have complete control over their behaviour. The below technique provides a nice balance between control and simplicity: it makes use of a Document event to check when a particular command is being called, a Database event to cache the information we wish to restore and finally another Document event to restore it. In this case it's all about location (or should I say "location, location, location" ? :-). We're caching an object's state before the MOVE command (which changes an object's position in the model), but if we wanted to roll back the effect of other commands, we would probably want to cache other properties.

Here's the C# code:

using System.Diagnostics;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

namespace Reactor

{

///<summary>

/// Reactor command.

///

/// Demonstrate a simple object reactor, as well as

/// cascaded event handling.

///

/// In this sample, the MOVE command is cancelled for

/// all red circles. This is achieved by attaching an

/// editor reactor and watching for the MOVE command begin.

/// When triggered, the reactor attaches an object reactor

/// to the database and watches for red circles. If any are

/// detected, their object id and original position are

/// stored. When the command ends, the positions are

/// restored and the object reactor removed again.

///

/// Reactors create overhead, so we should add them only

/// when needed and remove them as soon as possible

/// afterwards.

///</summary>

publicclassCmdReactor

{

privatestaticDocument _doc;

privatestaticObjectIdCollection _ids =

newObjectIdCollection();

privatestaticPoint3dCollection _pts =

newPoint3dCollection();

[CommandMethod("REACTOR")]

staticpublicvoid Reactor()

{

_doc =

Application.DocumentManager.MdiActiveDocument;

_doc.CommandWillStart +=

newCommandEventHandler(doc_CommandWillStart);

}

staticvoid doc_CommandWillStart(

object sender,

CommandEventArgs e

)

{

if (e.GlobalCommandName == "MOVE")

{

_ids.Clear();

_pts.Clear();

_doc.Database.ObjectOpenedForModify +=

newObjectEventHandler(_db_ObjectOpenedForModify);

_doc.CommandCancelled +=

newCommandEventHandler(_doc_CommandEnded);

_doc.CommandEnded +=

newCommandEventHandler(_doc_CommandEnded);

_doc.CommandFailed +=

newCommandEventHandler(_doc_CommandEnded);

}

}

staticvoid removeEventHandlers()

{

_doc.CommandCancelled -=

newCommandEventHandler(_doc_CommandEnded);

_doc.CommandEnded -=

newCommandEventHandler(_doc_CommandEnded);

_doc.CommandFailed -=

newCommandEventHandler(_doc_CommandEnded);

_doc.Database.ObjectOpenedForModify -=

newObjectEventHandler(_db_ObjectOpenedForModify);

}

staticvoid _doc_CommandEnded(

object sender,

CommandEventArgs e

)

{

// Remove database reactor before restoring positions

removeEventHandlers();

rollbackLocations();

}

staticvoid _db_ObjectOpenedForModify(

object sender,

ObjectEventArgs e

)

{

Circle circle = e.DBObject asCircle;

if (null != circle && 1 == circle.ColorIndex)

{

// In AutoCAD 2007, OpenedForModify is called only

// once by MOVE.

// In 2008, OpenedForModify is called multiple

// times by the MOVE command ... we are only

// interested in the first call, because

// in the second one, the object location

// has already been changed:

if (!_ids.Contains(circle.ObjectId))

{

_ids.Add(circle.ObjectId);

_pts.Add(circle.Center);

}

}

}

staticvoid rollbackLocations()

{

Debug.Assert(

_ids.Count == _pts.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) asCircle;

circle.Center = _pts[i++];

}

t.Commit();

}

}

}

}

To see the code at work, draw some circles and make some of them red:

Now run the REACTOR command and try to MOVE all the circles:

Although all the circles are dragged during the move, once we complete the command we can see that the red circles have remained in the same location (or have, in fact, had their location rolled back). The other circles have been moved, as expected.