August 2014

Sun Mon Tue Wed Thu Fri Sat
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            








« AU 2009 YouTube contest | Main | OffsetInXref: September’s ADN Plugin of the Month, now live on Autodesk Labs »

August 31, 2009

Gluing a point to an AutoCAD curve using overrules from .NET – Part 3

In the most recent part of this series, we looked at one possible mechanism to allow points to be moved along a network of curves, extending the first part in this series, which focused on the case of a point on a single curve.

This post is going to focus on something slightly different: it’s going to look at making the points added to a particular curve be associative to that curve – i.e. travel along with it as the curve is moved – and in the process we’re going to adjust the way we link between our objects, by moving the information into an object’s Extended Entity Data (XData). This does a few things: firstly it breaks our network-related technique (never fear – we’ll be able to get it back, in due course :-) by removing the idea of a central list of curves. It also allows us to have points that are connected to curves, and points that are not (which is clearly desirable). It also lays a more solid foundation for other operations, such as erasing the points on a curve when the curve itself is erased. All good stuff.

Just to be clear: there are lots of ways ways to approach this problem – this is just another possible mechanism – so you might be interested in checking out this previous series, which addressed a similar problem a little differently.

For now let’s see this basic implementation of our XData-related code. Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

 

namespace PointOnCurveTest

{

  public class LinkApplication

  {

    static string regAppName = "TTIF_PTS";

 

    public class LinkTransOverrule : TransformOverrule

    {

      static bool _transforming = false;

 

      // A static pointer to our overrule instance

 

      static public LinkTransOverrule theOverrule =

        new LinkTransOverrule();

 

      // A flag to indicate whether we're overruling

 

      static private bool overruling = false;

 

      // Our overrule should only be applied to objects

      // carrying our XData

 

      public LinkTransOverrule()

      {

        SetXDataFilter(regAppName);

      }

 

      // Turn on the transform overrule if it isn't already

 

      public static void TurnOn()

      {

        if (!overruling)

        {

          ObjectOverrule.AddOverrule(

            RXClass.GetClass(typeof(Entity)),

            LinkTransOverrule.theOverrule,

            true

          );

          overruling = true;

          TransformOverrule.Overruling = true;

        }

      }

 

      // Turn off the transform overrule if it's on

 

      public static void TurnOff()

      {

        if (overruling)

        {

          ObjectOverrule.RemoveOverrule(

            RXClass.GetClass(typeof(Entity)),

            LinkTransOverrule.theOverrule

          );

          overruling = false;

          TransformOverrule.Overruling = false;

        }

      }

 

      // Our primary overruled function

 

      public override void TransformBy(Entity e, Matrix3d mat)

      {

        // If we're already transforming, don't re-enter, just

        // super-message to the base

 

        if (_transforming)

          base.TransformBy(e, mat);

        else

        {

          Database db = HostApplicationServices.WorkingDatabase;

 

          // Otherwise get our linked objects: check whether

          // there are any

 

          ObjectIdCollection ids = GetLinkedObjects(e);

          if (ids.Count > 0)

          {

            // If we're dealing with a point...

 

            DBPoint pt = e as DBPoint;

            if (pt != null)

            {

              // Work through the curves to find the closest to our

              // transformed point

 

              double min = 0.0;

              Point3d bestPt = Point3d.Origin;

              bool first = true;

              ObjectId bestId = ObjectId.Null;

 

              // We're using an Open/Close transaction, to avoid

              // problems with us using transactions in an event

              // handler

 

              OpenCloseTransaction tr =

                db.TransactionManager.StartOpenCloseTransaction();

              using (tr)

              {

                // For now linked objects can be curves, else

                // they'll be ignored

 

                Curve cur;

                foreach (ObjectId curId in ids)

                {

                  DBObject obj =

                    tr.GetObject(curId, OpenMode.ForRead, true);

 

                  // If our linked object was erased, remove it

                  // from this object's links

 

                  if (obj.IsErased)

                  {

                    RemoveLinkedObject(e, curId);

                  }

                  else

                  {

                    // Otherwise we get the closest point on it

                    // to our point, to compare with others

 

                    cur = obj as Curve;

                    if (cur != null)

                    {

                      Point3d ptLoc =

                        pt.Position.TransformBy(mat);

                      Point3d ptOnCurve =

                        cur.GetClosestPointTo(ptLoc, false);

                      Vector3d dist = ptOnCurve - ptLoc;

 

                      if (first || dist.Length < min)

                      {

                        first = false;

                        min = dist.Length;

                        bestPt = ptOnCurve;

                        bestId = curId;

                      }

                    }

                  }

                }

 

                // If we didn't find a point, super-message

                // (will transform the point as normal)

 

                if (first)

                  base.TransformBy(e, mat);

                else

                  pt.Position = bestPt;

              }

            }

 

            if (e is Curve)

            {

              // Automatically super-message: we're not changing the

              // transform behaviour of the curve, only of the

              // linked objects

 

              base.TransformBy(e, mat);

 

              // Get each linked object and transform it along with

              // the "parent"

 

              OpenCloseTransaction tr =

                db.TransactionManager.StartOpenCloseTransaction();

              using (tr)

              {

                foreach (ObjectId id in ids)

                {

                  DBObject obj = tr.GetObject(id, OpenMode.ForWrite);

                  Entity ent = obj as Entity;

 

                  if (ent != null)

                  {

                    _transforming = true;

                    ent.TransformBy(mat);

                    _transforming = false;

                  }

                }

                tr.Commit();

              }

            }

          }

        }

      }

    }

 

    // Function to add a reference between one object and another

    // (stored in an object's XData)

 

    public static void AddLinkedObject(DBObject obj, ObjectId id)

    {

      Database db =

        HostApplicationServices.WorkingDatabase;

 

      // First we need to make sure our application name is

      // in the Registered Application Table

 

      OpenCloseTransaction tr =

        db.TransactionManager.StartOpenCloseTransaction();

      using (tr)

      {

        RegAppTable rat =

          (RegAppTable)tr.GetObject(

            db.RegAppTableId, OpenMode.ForRead

          );

        if (!rat.Has(regAppName))

        {

          rat.UpgradeOpen();

          RegAppTableRecord ratr =

            new RegAppTableRecord();

          ratr.Name = regAppName;

          tr.AddNewlyCreatedDBObject(ratr, true);

          rat.Add(ratr);

        }

        tr.Commit();

      }

 

      // Get our object's current XData

 

      ResultBuffer rb = obj.XData;

 

      // Check the XData, to see whether our link already exists

 

      bool foundLink = false;

 

      if (rb != null)

      {

        bool foundStart = false;

 

        foreach (TypedValue tv in rb)

        {

          // The first TypedValue is our application name

 

          if (tv.TypeCode ==

              (int)DxfCode.ExtendedDataRegAppName &&

              tv.Value.ToString() == regAppName)

            foundStart = true;

          else

          {

            if (foundStart)

            {

              // Our links will all have the same code (1005)

 

              if (tv.TypeCode == (int)DxfCode.ExtendedDataHandle)

              {

                if (id.Handle.ToString() == tv.Value.ToString())

                  foundLink = true;

              }

            }

          }

        }

      }

 

      // If we didn't find the link, add it

 

      if (!foundLink)

      {

        // This is our link

 

        TypedValue tv2 =

          new TypedValue(

            (int)DxfCode.ExtendedDataHandle, id.Handle

          );

 

        // If there was no previous XData, create the ResultBuffer

 

        if (rb == null)

        {

          rb =

            new ResultBuffer(

              new TypedValue(

                (int)DxfCode.ExtendedDataRegAppName,

                regAppName

              ),

              tv2

            );

        }

        else

        {

          // Add our TypeValue to an existing ResultBuffer

 

          rb.Add(tv2);

        }

 

        // Set the XData on our object

 

        obj.XData = rb;

      }

      rb.Dispose();

    }

 

    // Function to remove a reference from one object to another

    // (stored in an object's XData)

 

    public static void RemoveLinkedObject(DBObject obj, ObjectId id)

    {

      // Get the existing XData

 

      ResultBuffer oldRb = obj.XData;

 

      // Create a ResultBuffer for the "filtered" XData

      // (will contain all the old links, except the one we want

      // to remove)

 

      ResultBuffer newRb =

        new ResultBuffer(

          new TypedValue(

            (int)DxfCode.ExtendedDataRegAppName,

            regAppName)

          );

 

      bool foundLink = false;

 

      if (oldRb != null)

      {

        bool foundStart = false;

 

        // Loop through the XData

 

        foreach (TypedValue tv in oldRb)

        {

          // The first TypedValue is our application name

 

          if (tv.TypeCode ==

              (int)DxfCode.ExtendedDataRegAppName &&

              tv.Value.ToString() == regAppName)

            foundStart = true;

          else

          {

            if (foundStart)

            {

              // Our links will all have the same code (1005)

 

              if (tv.TypeCode == (int)DxfCode.ExtendedDataHandle)

              {

                // If the link is not the one we want to remove,

                // add it to our list of XData to retain,

                // otherwise set a flag to say we found it

 

                if (id.Handle.ToString() != tv.Value.ToString())

                  newRb.Add(tv);

                else

                  foundLink = true;

              }

            }

          }

        }

      }

 

      // If we found the link (and therefore have XData to set)

      // change the object's XData

 

      if (foundLink)

        obj.XData = newRb;

 

      // Clean-up after ourselves

 

      oldRb.Dispose();

      newRb.Dispose();

    }

 

    // Function to retrieve the references from one object to another

    // (stored in an object's XData)

 

    public static ObjectIdCollection GetLinkedObjects(DBObject obj)

    {

      Database db =

        HostApplicationServices.WorkingDatabase;

 

      // Will return an ObjectIdCollection

 

      ObjectIdCollection ids = new ObjectIdCollection();

 

      // Get the object's XData

 

      using (ResultBuffer rb = obj.XData)

      {

        if (rb != null)

        {

          bool foundStart = false;

 

          foreach (TypedValue tv in rb)

          {

            // The first TypedValue is our application name

 

            if (tv.TypeCode ==

                (int)DxfCode.ExtendedDataRegAppName &&

                tv.Value.ToString() == regAppName)

              foundStart = true;

            else

            {

              if (foundStart)

              {

                // Our links will all have the same code (1005)

 

                if (tv.TypeCode == (int)DxfCode.ExtendedDataHandle)

                {

                  // Get the long integer from the string stored in

                  // our XData, and the Handle from the long

 

                  long lg =

                    System.Convert.ToInt64(tv.Value.ToString(), 16);

                  Handle hd = new Handle(lg);

 

                  // And then the ObjectId from the Handle

 

                  ObjectId id = db.GetObjectId(false, hd, -1);

 

                  // Add it to our list, if it isn't already in it

 

                  if (!ids.Contains(id))

                    ids.Add(id);

                }

              }

            }

          }

        }

      }

      return ids;

    }

 

    [CommandMethod("POC")]

    public void CreatePointOnCurve()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

 

      // Ask the user to select a curve

 

      PromptEntityOptions opts =

      new PromptEntityOptions(

        "\nSelect curve at the point to create: "

      );

      opts.SetRejectMessage(

        "\nEntity must be a curve."

      );

      opts.AddAllowedClass(typeof(Curve), false);

 

      PromptEntityResult per = ed.GetEntity(opts);

 

      ObjectId curId = per.ObjectId;

      if (curId != ObjectId.Null)

      {

        // Let's make sure we'll be able to see our point

 

        db.Pdmode = 97;  // square with a circle

        db.Pdsize = -10; // relative to the viewport size

 

        Transaction tr =

          doc.TransactionManager.StartTransaction();

        using (tr)

        {

          DBObject obj =

            tr.GetObject(curId, OpenMode.ForRead);

          Curve cur = obj as Curve;

          if (cur != null)

          {

            // Our initial point should be the closest point

            // on the curve to the one picked

 

            Point3d pos =

              cur.GetClosestPointTo(per.PickedPoint, false);

            DBPoint pt = new DBPoint(pos);

 

            // Add it to the same space as the curve

 

            BlockTableRecord btr =

              (BlockTableRecord)tr.GetObject(

                cur.BlockId,

                OpenMode.ForWrite

              );

            ObjectId ptId = btr.AppendEntity(pt);

            AddLinkedObject(pt, curId);

            cur.UpgradeOpen();

            AddLinkedObject(cur, ptId);

            tr.AddNewlyCreatedDBObject(pt, true);

          }

          tr.Commit();

 

          LinkTransOverrule.TurnOn();

        }

      }

    }

  }

}

Some points to note about this implementation:

  • We now register the overrule to work for all entity types, but at the same time we tell AutoCAD to filter on our Register Application Name
    • Only objects with our XData will cause the overrule to be called
  • Our TransformBy() function will therefore apply to all entities with our XData, although we’re only (currently) interested in DBPoints and Curves
    • We do need to stop the function re-entering for the points we transform along with the parent curve, but that’s a simple matter of setting/checking a boolean flag
  • We have kept the technique of checking a number of curves, to see which is closest, but as we’re only adding the XData during the POC command – which currently only allows selection of a single curve – the behaviour is now once again similar to the first post in the series (although this is very easy to change)

Now let’s see what happens when our code is executed. We’ll take the same drawing as last time – which contains a Spline and a Polyline – and use the POC command to add a few points on each:

Two points on each curve

When we move the Spline, we see the points on it now move along with it:

Moving the spline with its points

What’s interesting is that when we do the same for a Polyline, the points don’t appear during the drag…

Moving the polyline without its points

… but they do, indeed, show up in the right place once the Polyline has completed the move to its destination:

Both curves moved with their points

This is apparently due to optimization inside AutoCAD: during a recent discussion with our Engineering team I learned that during certain operations – such as when a modification to an object or set of objects can be represented by a standard transformation (such as scale, rotate or move) – AutoCAD captures the graphics of those objects and then performs a “sprite” optimization to move the object(s), rather than continually asking the object(s) to participate in the movement by drawing their own graphics or by transforming their internal state.

It’s not fully clear to me why this is happening in this particular situation – and with the Polyline rather than the Spline – but it does appear that this optimization has been turned on during the movement of the Polyline. This only occurs in very specific situations – such as in 3D rather than 2D – but it does seem something that would be valuable for applications to have more explicit control over.

That’s it for today’s post. I’m actually on vacation this week, but I do have something to share later in the week related to our “Plugin on the Month” initiative…

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e20120a58ff152970c

Listed below are links to weblogs that reference Gluing a point to an AutoCAD curve using overrules from .NET – Part 3:

blog comments powered by Disqus

Feed/Share

10 Random Posts