Using transient graphics to simulate AutoCAD’s MOVE command using .NET

A big thanks to Norman Yuan for the technique shown in this post. I stumbled across Norman's post on this topic, and decided to take his code and rework it for posting here, with Norman's permission, of course. Very interesting stuff! ๐Ÿ™‚

First things first: you probably don't have a burning need to reinvent either the wheel or AutoCAD's MOVE command. That said, this is a technique that may be of interest to many of you, especially if โ€“ for whatever reason โ€“ you're looking for an alternative to using a jig. The technique Norman has shown uses a combination of a PointMonitor and AutoCAD's transient graphics API (available since AutoCAD 2009), to effectively draw a set of entities transiently at the cursor as you move it across the screen.

From a purist's perspective, I'd almost always use a jig for this โ€“ as in many ways this is the kind of low-level behaviour a DrawJig encapsulates, for instance โ€“ but then I'm also aware that there are always strange edge-cases where people want more control, or just want to do something downright strange. This technique is another tool in your box that may be of assistance in those situations, and is certainly not lacking in ingenuity.

My main changes to Norman's original code were to remove the use of member state โ€“ which also meant shifting away from a member variable for the PointMonitor event handler to using a local delegate with access to variables in scope when the handler was added โ€“ as well as removing some manual techniques for selection & highlighting. If you're interested, please do head across to Norman's blog to check out his original implementation.

Here's the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.GraphicsInterface;

using Autodesk.AutoCAD.Runtime;

using System.Collections.Generic;

using System;

 

namespace CustomMove

{

  public class Commands

  {

    [CommandMethod("MYMOVE", CommandFlags.UsePickSet)]

    public static void CustomMoveCmd()

    {

      Document doc =

        Autodesk.AutoCAD.ApplicationServices.

          Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

 

      // Start by getting the objects to move

      // (will use the pickfirst set, if defined)

 

      PromptSelectionResult psr = ed.GetSelection();

      if (psr.Status != PromptStatus.OK || psr.Value.Count == 0)

        return;

 

      // Create a collection of the selected objects' IDs

 

      ObjectIdCollection ids =

        new ObjectIdCollection(psr.Value.GetObjectIds());

 

      // Ask the user to select a base point for the move

 

      PromptPointResult ppr =

        ed.GetPoint("\nSpecify base point: ");

      if (ppr.Status != PromptStatus.OK)

        return;

 

      Point3d basePt = ppr.Value;

      Point3d curPt = basePt;

 

      // A local delegate for our event handler so

      // we can remove it at the end

 

      PointMonitorEventHandler handler = null;

 

      // Our transaction

 

      Transaction tr =

        doc.Database.TransactionManager.StartTransaction();

      using (tr)

      {

        // Create our transient drawables, with associated

        // graphics, from the selected objects

 

        List<Drawable> drawables = CreateTransGraphics(tr, ids);

        try

        {

          // Add our point monitor

          // (as a delegate we have access to basePt and curPt,

          //  which avoids having to access global/member state)

 

          handler =

            delegate(object sender, PointMonitorEventArgs e)

            {

              // Get the point, with "ortho" applied, if needed

 

              Point3d pt = e.Context.RawPoint;

              if (IsOrthModeOn())

                pt = GetOrthoPoint(basePt, pt);

 

              // Update our graphics and the current point

 

              UpdateTransGraphics(drawables, curPt, pt);

              curPt = pt;

            };

 

          ed.PointMonitor += handler;

 

          // Ask for the destination, during which the point

          // monitor will be updating the transient graphics

 

       &#
160; 
PromptPointOptions opt =

            new PromptPointOptions("\nSpecify second point: ");

          opt.UseBasePoint = true;

          opt.BasePoint = basePt;

          ppr = ed.GetPoint(opt);

 

          // If the point was selected successfully...

 

          if (ppr.Status == PromptStatus.OK)

          {

            // ... move the entities to their destination

 

            MoveEntities(

              tr, basePt,

              IsOrthModeOn() ?

                GetOrthoPoint(basePt, ppr.Value) :

                ppr.Value,

              ids

            );

 

            // And inform the user

 

            ed.WriteMessage(

              "\n{0} object{1} moved", ids.Count,

              ids.Count == 1 ? "" : "s"

            );

          }

        }

        catch (Autodesk.AutoCAD.Runtime.Exception ex)

        {

          ed.WriteMessage("\nException: {0}", ex.Message);

        }

        finally

        {

          // Clear any transient graphics

 

          ClearTransGraphics(drawables);

 

          // Remove the event handler

 

          if (handler != null)

            ed.PointMonitor -= handler;

 

          tr.Commit();

          tr.Dispose();

        }

      }

    }

 

    private static bool IsOrthModeOn()


0;   {

      // Check the value of the ORTHOMODE sysvar

 

      object orth =

        Autodesk.AutoCAD.ApplicationServices.Application.

        GetSystemVariable("ORTHOMODE");

 

      return Convert.ToInt32(orth) > 0;

    }

 

    private static Point3d GetOrthoPoint(

      Point3d basePt, Point3d pt

    )

    {

      // Apply a crude orthographic mode

 

      double x = pt.X;

      double y = pt.Y;

 

      Vector3d vec = basePt.GetVectorTo(pt);

      if (Math.Abs(vec.X) >= Math.Abs(vec.Y))

        y = basePt.Y;

      else

        x = basePt.X;

 

      return new Point3d(x, y, 0.0);

    }

 

    private static void MoveEntities(

      Transaction tr, Point3d basePt, Point3d moveTo,

      ObjectIdCollection ids

    )

    {

      // Transform a set of entities to a new location

 

      Matrix3d mat =

        Matrix3d.Displacement(basePt.GetVectorTo(moveTo));

      foreach (ObjectId id in ids)

      {

        Entity ent = (Entity)tr.GetObject(id, OpenMode.ForWrite);

        ent.TransformBy(mat);

      }

    }

 

    private static List<Drawable> CreateTransGraphics(

      Transaction tr, ObjectIdCollection ids

    )

    {

      // Create our list of drawables to return

 

      List<Drawable> drawables = new List<Drawable>();

 

      foreach (ObjectId id in ids)

      {

        // Read each entity

 

        Entity ent = (Entity)tr.GetObject(id, OpenMode.ForRead);

 

        // Clone it, make it red & add the clone to the list

 

        Entity drawable = ent.Clone() as Entity;

        drawable.ColorIndex = 1;

        drawables.Add(drawable);

      }

 

      // Draw each one initially

 

      foreach (Drawable d in drawables)

      {

        TransientManager.CurrentTransientManager.AddTransient(

          d, TransientDrawingMode.DirectShortTerm,

          128, new IntegerCollection()

        );

      }

      return drawables;

    }

 

    private static void UpdateTransGraphics(

      List<Drawable> drawables, Point3d curPt, Point3d moveToPt

    )

    {

      // Displace each of our drawables

 

      Matrix3d mat =

        Matrix3d.Displacement(curPt.GetVectorTo(moveToPt));

 

      // Update their graphics

 

      foreach (Drawable d in drawables)

      {

        Entity e = d as Entity;

        e.TransformBy(mat);

 

        TransientManager.CurrentTransientManager.UpdateTransient(

          d, new IntegerCollection()

        );

      }

    }

 

    private static void ClearTransGraphics(

      List<Drawable> drawables

    )

    {

      // Clear the transient graphics for our drawables

 

      TransientManager.CurrentTransientManager.EraseTransients(

        TransientDrawingMode.DirectShortTerm,

        128, new IntegerCollection()

      );

 

      // Dispose of them and clear the list

 

      foreach (Drawable d in drawables)

      {

        d.Dispose();

      }

      drawables.Clear();

    }

  }

}

When you run the MYMOVE command, selecting some geometry to move, you'll be prompted for a base point and destination:

Custom move command in action

You should see the selected geometry in red โ€“ unless you're moving complex such as block references, in which case it depends on whether the contents are picking up the colour of the reference, or not โ€“ until you cancel the command or select the destination.

One issue that I deliberately chose not to address, for now, is that of ORTHOMODE support: the above implementation provides fairly crude orthographic drawing support, but I did notice more work is needed for it to work properly in 3D with a non-World UCS. This is probably something I'll look at addressing in a follow-up post (if Norman doesn't end up posting a fix here in the meantime :-).

Thanks again, Norman, for this interesting technique!

11 responses to “Using transient graphics to simulate AutoCAD’s MOVE command using .NET”

  1. Hi Kean, you sure crank out new posts fast, good stuff. Couple q's: 1) I am on win 7 64 bit, and this comment window I am typing in is displaying as cut off on one side or the other.
    It shifts so I can see the word I am typing, but cuts off the right or left of the sentences. It also cuts off the published comment, I'll email you a pic.
    real question now - I use this technique of pre-drawing things before the user picks where to put them, and much of the time they have extra info compared to the final entities. Would you use jig for things that are not, and will not be drawn? I am not as familiar with jig as I am with transient graphics, so the answer might be obvious to someone who is. thx

  2. Hi James,

    The comments entry & preview are a bit flakey, to say the least.

    I don't know whether it's down to TypePad or the templates used for this blog, but I generally consider it a minor annoyance (as long as the comments show up properly it doesn't really matter).

    I generally author my comments in Notepad, anyway, to make sure they don't disappear if the post fails (which does happen, from time to time).

    Anyway...

    Jigs are great (they take care of a lot of the complexity around displaying/undisplaying temp graphics), especially if you have an entity you want to display. You could probably use a combination of Overrules and Jigs to avoid direct use of the transient graphics API, but it's a question of choice.

    Regards,

    Kean

  3. Thorsten Meinecke Avatar
    Thorsten Meinecke

    Hi Kean,
    thanks for bringing up transient graphics. Norman's blog certainly deserves a wide readership.

    Since it's customary to display transient entities with an empty IntegerCollection as last parameter, they often do not display satisfactorily if TILEMODE = 0. Any chance to demonstrate how the IntegerCollection can be populated to restrict the drawing of transients to either the current viewport (e.g. if CVPORT = 1) or else to the active viewports without the paperspace viewport?

  4. Hi Thorsten,

    I'd be happy to, although I don't know the answer, myself (and it'll take me time to research it).

    It sounds as though you've already tackled this... feel like signing up for a guest post? ๐Ÿ™‚

    Regards,

    Kean

  5. p_derockere@hotmail.com Avatar
    p_derockere@hotmail.com

    Is it possible to give me a brief description of how a *.net could implement my drawing.
    I have no experience with this method.
    Thank you.
    Pieter

  6. Is it possible to give me a brief description of how a *.net could implement my drawing.
    I have no experience with this method.
    Thank you.
    Pieter

  7. Pieter,

    This isn't a forum for support.

    Your comment doesn't appear to relate to this post, so please submit your question to the ADN team, if you're a member, or otherwise the AutoCAD .NET Discussion Group.

    Kean

  8. Hi.
    If I had to draw a square instead of selecting in the drawing, what is better, clone entity (or dbObject) to add to a drawable List or using a DbOjectCollection?

    1. If you're creating your own, new geometry, then you should just create the objects and add them to the collection. And dispose of them when no longer needed.

      I'd simply "new" them rather than cloning existing entities, personally.

      Kean

  9. Hey Kean thx for this post (and I know you're enjoying holidays - but i figure it's a quick question nonetheless which you'd know right away) - don't jigs make use of the transient graphics API under the surface anyways (through the WorldDraw object)?

    1. In a sense, yes: they certainly use the same low-level graphics APIs (ObjectARX jigs no doubt pass via the ObjectARX transient graphics mechanism, and the .NET jigs wrap ObjectARX jigs).

      Kean

Leave a Reply

Your email address will not be published. Required fields are marked *