Using a jig to rotate an AutoCAD entity via .NET

An interesting request came in via a previous post followed up by a similar question came in via another post. The original problem was to rotate a rectangular polyline entity around its centre, and be able to continue rotating it afterwards. A few things were interesting to me:

  • Rectangles are simply polylines between four points, so have no inherent concept of either a centre point or a rotation angle
    • The obvious answer (to me, at least) being to calculate the centre and store the rotation as XData on the polyline
  • To modify an entity graphically it makes sense to use a jig
    • Most examples show how to use jigs to create new entities, not modify existing ones

So with that in mind, I created the below C# code to solve this problem:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;


namespace RotatingRectangles

{

  public class Commands

  {

    // Define some constants we'll use to

    // store our XData


    // AppName is our RDS (TTIF, for

    // "Through The InterFace") plus an indicator

    // what it's for (ROTation)


    const string kRegAppName = "TTIF_ROT";

    const int kAppCode = 1001;

    const int kRotCode = 1040;


    class RotateJig : EntityJig

    {

      // Declare some internal state


      double m_baseAngle, m_deltaAngle;

      Point3d m_rotationPoint;

      Matrix3d m_ucs;


      // Constructor sets the state and clones

      // the entity passed in

      // (adequate for simple entities)


      public RotateJig(

        Entity ent,

        Point3d rotationPoint,

        double baseAngle,

        Matrix3d ucs)

        : base(ent.Clone() as Entity)

      {

        m_rotationPoint = rotationPoint;

        m_baseAngle = baseAngle;

        m_ucs = ucs;

      }


      protected override SamplerStatus Sampler(

        JigPrompts jp

      )

      {

        // We acquire a single angular value


        JigPromptAngleOptions jo =

          new JigPromptAngleOptions(

            "\nAngle of rotation: "

          );

        jo.BasePoint = m_rotationPoint;

        jo.UseBasePoint = true;


        PromptDoubleResult pdr =

          jp.AcquireAngle(jo);


        if (pdr.Status == PromptStatus.OK)

        {

          // Check if it has changed or not

          // (reduces flicker)


          if (m_deltaAngle == pdr.Value)

          {

            return SamplerStatus.NoChange;

          }

          else

          {

            // Set the change in angle to

            // the new value


            m_deltaAngle = pdr.Value;

            return SamplerStatus.OK;

          }

        }

        return SamplerStatus.Cancel;

      }


      protected override bool Update()

      {

        // We rotate the polyline by the change

        // minus the base angle


        Matrix3d trans =

          Matrix3d.Rotation(

            m_deltaAngle - m_baseAngle,

            m_ucs.CoordinateSystem3d.Zaxis,

            m_rotationPoint);

        Entity.TransformBy(trans);


        // The base becomes the previous delta

        // and the delta gets set to zero


        m_baseAngle = m_deltaAngle;

        m_deltaAngle = 0.0;


        return true;

      }


      public Entity GetEntity()

      {

        return Entity;

      }


      public double GetRotation()

      {

        // The overall rotation is the

        // base plus the delta


        return m_baseAngle + m_deltaAngle;

      }

    }


    [CommandMethod("ROT")]

    public void RotateEntity()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      Database db = doc.Database;


      // First we prompt for the entity to rotate


      PromptEntityOptions peo =

        new PromptEntityOptions(

          "\nSelect entity to rotate: "

        );

      PromptEntityResult per =

        ed.GetEntity(peo);


      if (per.Status == PromptStatus.OK)

      {

        Transaction tr =

          db.TransactionManager.StartTransaction();

        using (tr)

        {

          DBObject obj =

            tr.GetObject(per.ObjectId, OpenMode.ForRead);

          Entity ent = obj as Entity;


          // Use the origin as the default center


          Point3d rotationPoint = Point3d.Origin;


          // If the entity is a polyline,

          // assume it is rectangular and then

          // set the rotation point as its center


          Polyline pl = obj as Polyline;

          if (pl != null)

          {

            LineSegment3d ps0 =

              pl.GetLineSegmentAt(0);

            LineSegment3d ps1 =

              pl.GetLineSegmentAt(1);

            Vector3d vec =

              ((ps0.EndPoint - ps0.StartPoint) / 2.0) +

              ((ps1.EndPoint - ps1.StartPoint) / 2.0);

            rotationPoint = pl.StartPoint + vec;

          }


          // Get the base rotation angle stored with the

          // entity, if there was one (default is 0.0)


          double baseAngle = GetStoredRotation(obj);


          if (ent != null)

          {

            // Get the current UCS, to pass to the Jig


            Matrix3d ucs =

              ed.CurrentUserCoordinateSystem;


            // Create our jig object


            RotateJig jig =

              new RotateJig(

                ent,

                rotationPoint,

                baseAngle,

                ucs

              );


            PromptResult res = ed.Drag(jig);

            if (res.Status == PromptStatus.OK)

            {

              // Get the overall rotation angle

              // and dispose of the temp clone


              double newAngle = jig.GetRotation();

              jig.GetEntity().Dispose();


              // Rotate the original entity


              Matrix3d trans =

                Matrix3d.Rotation(

                  newAngle - baseAngle,

                  ucs.CoordinateSystem3d.Zaxis,

                  rotationPoint);


              ent.UpgradeOpen();

              ent.TransformBy(trans);


              // Store the new rotation as XData


              SetStoredRotation(ent, newAngle);

            }

          }

          tr.Commit();

        }

      }

    }


    // Helper function to create a RegApp


    static void AddRegAppTableRecord(string regAppName)

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      Database db = doc.Database;


      Transaction tr =

        doc.TransactionManager.StartTransaction();

      using (tr)

      {

        RegAppTable rat =

          (RegAppTable)tr.GetObject(

            db.RegAppTableId,

            OpenMode.ForRead,

            false

          );

        if (!rat.Has(regAppName))

        {

          rat.UpgradeOpen();

          RegAppTableRecord ratr =

            new RegAppTableRecord();

          ratr.Name = regAppName;

          rat.Add(ratr);

          tr.AddNewlyCreatedDBObject(ratr, true);

        }

        tr.Commit();

      }

    }


    // Store our rotation angle as XData


    private void SetStoredRotation(

      DBObject obj, double rotation)

    {

      AddRegAppTableRecord(kRegAppName);

      ResultBuffer rb = obj.XData;

      if (rb == null)

      {

        rb =

          new ResultBuffer(

            new TypedValue(kAppCode, kRegAppName),

            new TypedValue(kRotCode, rotation)

          );

      }

      else

      {

        // We can simply add our values - no need

        // to remove the previous ones, the new ones

        // are the ones that get stored


        rb.Add(new TypedValue(kAppCode, kRegAppName));

        rb.Add(new TypedValue(kRotCode, rotation));

      }

      obj.XData = rb;

      rb.Dispose();

    }


    // Retrieve the existing rotation angle from XData


    private double GetStoredRotation(DBObject obj)

    {

      double ret = 0.0;


      ResultBuffer rb = obj.XData;

      if (rb != null)

      {

        // If we find our group code, it means that on

        // the next iteration, we'll get our rotation


        bool bReadyForRot = false;

        foreach (TypedValue tv in rb)

        {

          if (bReadyForRot)

          {

            if (tv.TypeCode == kRotCode)

              ret = (double)tv.Value;

            bReadyForRot = false;

          }

          if (tv.TypeCode == kAppCode)

            bReadyForRot = true;

        }

        rb.Dispose();

      }

      return ret;

    }

  }

}

To try this out, we can use the RECTANG command, to create a horizontal rectangle, and then use our custom ROT command to rotate it:

Horizontal rectangle Rotating a rectangle Rotated rectangle

Calling the ROT command subsequently works fine, as it "remembers" the angle it was rotated at. If other tools are used to rotate the rectangle, all bets are off. One alternative would be to determine the "rotation" by performing slightly deeper analysis on the rectangle: determining the longer side and getting its angle should do it. It would also be trivial to implement a command to set the rotation to one input by the user (whether by selecting two points or entering it).

March 28, 2008 in AutoCAD, AutoCAD .NET, Jigs | Permalink | Comments (6) | TrackBack

Creating a multileader in AutoCAD using a jig from .NET

I'm now back from a fantastic break in Italy and am trying hard to catch back up. Next week I'm off again to San Diego (work, this time), which may cause further interruptions in blog postings.

This question came through from Genésio from Brazil:

I wish jig a leader with an bubble in the and of the leader, at the same time. Can you help me. Perhaps post the solution in your blog (through the interface).

It took me a while - frustratingly long, in fact, and probably this is not exactly what Genésio is after - but here's what I managed to come up with. The "bubble" is framed MText, but it should be modifiable to use a classic bubble block, instead. I drew heavily on this previous post for the jig code.

The positioning of the text took some work, but I'm reasonably happy with the results. If anyone has tweaks to suggest, please post a comment.

Here's the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;


namespace DimensionLibrary

{

  public class DimensionCmds

  {

    class MLeaderJig : EntityJig

    {

      Point3dCollection m_pts;

      Point3d m_tempPoint;

      string m_contents;

      int m_leaderIndex;

      int m_leaderLineIndex;


      public MLeaderJig(string contents)

        : base(new MLeader())

      {

        // Store the string passed in


        m_contents = contents;


        // Create a point collection to store our vertices


        m_pts = new Point3dCollection();


        // Create mleader and set defaults


        MLeader ml = Entity as MLeader;

        ml.SetDatabaseDefaults();


        // Set up the MText contents


        ml.ContentType = ContentType.MTextContent;

        MText mt = new MText();

        mt.SetDatabaseDefaults();

        mt.Contents = m_contents;

        ml.MText = mt;

        ml.TextAlignmentType =

          TextAlignmentType.LeftAlignment;

        ml.TextAttachmentType =

          TextAttachmentType.AttachmentMiddle;


        // Set the frame and landing properties


        ml.EnableDogleg = true;

        ml.EnableFrameText = true;

        ml.EnableLanding = true;


        // Reduce the standard landing gap


        ml.LandingGap = 0.05;


        // Add a leader, but not a leader line (for now)


        m_leaderIndex = ml.AddLeader();

        m_leaderLineIndex = -1;

      }


      protected override SamplerStatus Sampler(

        JigPrompts prompts

      )

      {

        JigPromptPointOptions opts =

          new JigPromptPointOptions();


        // Not all options accept null response

        opts.UserInputControls =

          (UserInputControls.Accept3dCoordinates |

          UserInputControls.NoNegativeResponseAccepted

          );


        // Get the first point

        if (m_pts.Count == 0)

        {

          opts.UserInputControls |=

            UserInputControls.NullResponseAccepted;

          opts.Message =

            "\nStart point of multileader: ";

          opts.UseBasePoint = false;

        }

        // And the second

        else if (m_pts.Count == 1)

        {

          opts.BasePoint = m_pts[m_pts.Count - 1];

          opts.UseBasePoint = true;

          opts.Message =

            "\nSpecify multileader vertex: ";

        }

        // And subsequent points

        else if (m_pts.Count > 1)

        {

          opts.UserInputControls |=

            UserInputControls.NullResponseAccepted;

          opts.BasePoint = m_pts[m_pts.Count - 1];

          opts.UseBasePoint = true;

          opts.SetMessageAndKeywords(

            "\nSpecify multileader vertex or [End]: ",

            "End"

          );

        }

        else // Should never happen

          return SamplerStatus.Cancel;


        PromptPointResult res =

          prompts.AcquirePoint(opts);


        if (res.Status == PromptStatus.Keyword)

        {

          if (res.StringResult == "End")

          {

            return SamplerStatus.Cancel;

          }

        }


        if (m_tempPoint == res.Value)

        {

          return SamplerStatus.NoChange;

        }

        else if (res.Status == PromptStatus.OK)

        {

          m_tempPoint = res.Value;

          return SamplerStatus.OK;

        }

        return SamplerStatus.Cancel;

      }


      protected override bool Update()

      {

        try

        {

          if (m_pts.Count > 0)

          {

            // Set the last vertex to the new value


            MLeader ml = Entity as MLeader;

            ml.SetLastVertex(

              m_leaderLineIndex,

              m_tempPoint

            );


            // Adjust the text location


            Vector3d dogvec =

              ml.GetDogleg(m_leaderIndex);

            double doglen =

              ml.DoglegLength;

            double landgap =

              ml.LandingGap;

            ml.TextLocation =

              m_tempPoint +

              ((doglen + landgap) * dogvec);

          }

        }

        catch (System.Exception ex)

        {

          Document doc =

            Application.DocumentManager.MdiActiveDocument;

          doc.Editor.WriteMessage(

            "\nException: " + ex.Message

          );

          return false;

        }

        return true;

      }


      public void AddVertex()

      {

        MLeader ml = Entity as MLeader;


        // For the first point...


        if (m_pts.Count == 0)

        {

          // Add a leader line


          m_leaderLineIndex =

            ml.AddLeaderLine(m_leaderIndex);


          // And a start vertex


          ml.AddFirstVertex(

            m_leaderLineIndex,

            m_tempPoint

          );


          // Add a second vertex that will be set

          // within the jig


          ml.AddLastVertex(

            m_leaderLineIndex,

            new Point3d(0, 0, 0)

          );

        }

        else

        {

          // For subsequent points,

          // just add a vertex


          ml.AddLastVertex(

            m_leaderLineIndex,

            m_tempPoint

          );

        }


        // Reset the attachment point, otherwise

        // it seems to get forgotten


        ml.TextAttachmentType =

          TextAttachmentType.AttachmentMiddle;


        // Add the latest point to our history


        m_pts.Add(m_tempPoint);

      }


      public void RemoveLastVertex()

      {

        // We don't need to actually remove

        // the vertex, just reset it


        MLeader ml = Entity as MLeader;

        if (m_pts.Count >= 1)

        {

          Vector3d dogvec =

            ml.GetDogleg(m_leaderIndex);

          double doglen =

            ml.DoglegLength;

          double landgap =

            ml.LandingGap;

          ml.TextLocation =

            m_pts[m_pts.Count - 1] +

            ((doglen + landgap) * dogvec);

        }

      }


      public Entity GetEntity()

      {

        return Entity;

      }

    }


    [CommandMethod("MYML")]

    public void MyMLeaderJig()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      Database db = doc.Database;


      // Get the text outside of the jig


      PromptStringOptions pso =

        new PromptStringOptions(

          "\nEnter text: "

        );

      pso.AllowSpaces = true;

      PromptResult pr =

        ed.GetString(pso);

      if (pr.Status == PromptStatus.OK)

      {

        // Create MleaderJig


        MLeaderJig jig =

          new MLeaderJig(pr.StringResult);


        // Loop to set vertices


        bool bSuccess = true, bComplete = false;

        while (bSuccess && !bComplete)

        {

          PromptResult dragres = ed.Drag(jig);

          bSuccess =

            (dragres.Status == PromptStatus.OK);

          if (bSuccess)

            jig.AddVertex();

          bComplete =

            (dragres.Status == PromptStatus.None);

          if (bComplete)

            jig.RemoveLastVertex();

        }


        if (bComplete)

        {

          // Append entity


          Transaction tr =

            db.TransactionManager.StartTransaction();

          using (tr)

          {

            BlockTable bt =

              (BlockTable)tr.GetObject(

                db.BlockTableId,

                OpenMode.ForRead,

                false

              );

            BlockTableRecord btr =

              (BlockTableRecord)tr.GetObject(

                bt[BlockTableRecord.ModelSpace],

                OpenMode.ForWrite,

                false

              );

            btr.AppendEntity(jig.GetEntity());

            tr.AddNewlyCreatedDBObject(

              jig.GetEntity(),

              true

            );

            tr.Commit();

          }

        }

      }

    }

  }

}

Here's what you get when you run the MYML command:

Jigged_mleader

September 21, 2007 in AutoCAD, AutoCAD .NET, Dimensions, Jigs | Permalink | Comments (5) | TrackBack

Using a jig from .NET to multiply insert AutoCAD blocks - Part 2

In the last post we looked at a jig that can be used to add block references to an AutoCAD drawing. This post extends that code to support annotative block definitions (available from AutoCAD 2008) and blocks with attributes. Thanks once again to Holger Steiner for the jig class and to Roland Feletic for posting the code to support annotative blocks.

A comment on the previous post asked about having attributes visible during the jig process: unfortunately that's not currently possible, as the existing managed AttributeCollection implementation wraps the version of the AcDbBlockReference::appendAttribute() ObjectARX function that requires the block reference to already have been added to the drawing. So for now you will need to live with the fact that the attributes don't display during the jig, but do display as soon as the block has been added to the drawing.

Below is the modified C# code, with line numbers. And here's the source file for download.

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.DatabaseServices;

    3 using Autodesk.AutoCAD.EditorInput;

    4 using Autodesk.AutoCAD.Runtime;

    5 using Autodesk.AutoCAD.Geometry;

    6 using Autodesk.AutoCAD.Internal;

    7

    8 namespace BlockJigTest

    9 {

   10   class BlockJig : EntityJig

   11   {

   12     Point3d mCenterPt, mActualPoint;

   13

   14     public BlockJig(BlockReference br)

   15       : base(br)

   16     {

   17       mCenterPt = br.Position;

   18     }

   19

   20     protected override SamplerStatus Sampler(JigPrompts prompts)

   21     {

   22       JigPromptPointOptions jigOpts =

   23         new JigPromptPointOptions();

   24       jigOpts.UserInputControls =

   25         (UserInputControls.Accept3dCoordinates

   26         | UserInputControls.NoZeroResponseAccepted

   27         | UserInputControls.NoNegativeResponseAccepted);

   28

   29       jigOpts.Message =

   30         "\nEnter insert point: ";

   31

   32       PromptPointResult dres =

   33         prompts.AcquirePoint(jigOpts);

   34

   35       if (mActualPoint == dres.Value)

   36       {

   37         return SamplerStatus.NoChange;

   38       }

   39       else

   40       {

   41         mActualPoint = dres.Value;

   42       }

   43       return SamplerStatus.OK;

   44     }

   45

   46     protected override bool Update()

   47     {

   48       mCenterPt = mActualPoint;

   49       try

   50       {

   51         ((BlockReference)Entity).Position = mCenterPt;

   52       }

   53       catch (System.Exception)

   54       {

   55         return false;

   56       }

   57       return true;

   58     }

   59

   60     public Entity GetEntity()

   61     {

   62       return Entity;

   63     }

   64   }

   65

   66   public class Commands

   67   {

   68     [CommandMethod("BJIG")]

   69     public void CreateBlockWithJig()

   70     {

   71       Document doc =

   72         Application.DocumentManager.MdiActiveDocument;

   73       Database db = doc.Database;

   74       Editor ed = doc.Editor;

   75

   76       // First let's get the name of the block

   77       PromptStringOptions opts =

   78         new PromptStringOptions("\nEnter block name: ");

   79       PromptResult pr = ed.GetString(opts);

   80       if (pr.Status == PromptStatus.OK)

   81       {

   82         Transaction tr =

   83           doc.TransactionManager.StartTransaction();

   84         using (tr)

   85         {

   86           // Then open the block table and check the

   87           // block definition exists

   88           BlockTable bt =

   89             (BlockTable)tr.GetObject(

   90               db.BlockTableId,

   91               OpenMode.ForRead

   92             );

   93           if (!bt.Has(pr.StringResult))

   94           {

   95             ed.WriteMessage("\nBlock not found.");

   96           }

   97           else

   98           {

   99             ObjectId bdId = bt[pr.StringResult];

  100

  101             // We loop until the jig is cancelled

  102             while (pr.Status == PromptStatus.OK)

  103             {

  104               // Create the block reference and

  105               // add it to the jig

  106               Point3d pt = new Point3d(0, 0, 0);

  107               BlockReference br =

  108                 new BlockReference(pt, bdId);

  109

  110               BlockJig entJig = new BlockJig(br);

  111

  112               // Perform the jig operation

  113               pr = ed.Drag(entJig);

  114               if (pr.Status == PromptStatus.OK)

  115               {

  116                 // If all is OK, let's go and add the

  117                 // entity to the modelspace

  118                 BlockTableRecord ms =

  119                   (BlockTableRecord)tr.GetObject(

  120                     bt[BlockTableRecord.ModelSpace],

  121                     OpenMode.ForWrite

  122                   );

  123                 ms.AppendEntity(

  124                   entJig.GetEntity()

  125                 );

  126                 tr.AddNewlyCreatedDBObject(

  127                   entJig.GetEntity(),

  128                   true

  129                 );

  130

  131                 // Start attrib/annot-scale support code

  132                 BlockTableRecord bd =

  133                   (BlockTableRecord)tr.GetObject(

  134                     bdId,

  135                     OpenMode.ForRead

  136                   );

  137                 if (bd.Annotative == AnnotativeStates.True)

  138                 {

  139                   ObjectContextManager ocm =

  140                     db.ObjectContextManager;

  141                   ObjectContextCollection occ =

  142                     ocm.GetContextCollection(

  143                       "ACDB_ANNOTATIONSCALES"

  144                     );

  145                   ObjectContexts.AddContext(

  146                     br,

  147                     occ.CurrentContext

  148                   );

  149                 }

  150

  151                 // Add the attributes

  152                 foreach (ObjectId attId in bd)

  153                 {

  154                   Entity ent =

  155                     (Entity)tr.GetObject(

  156                       attId,

  157                       OpenMode.ForRead

  158                     );

  159                   if (ent is AttributeDefinition)

  160                   {

  161                     AttributeDefinition ad =

  162                       (AttributeDefinition)ent;

  163                     AttributeReference ar =

  164                       new AttributeReference();

  165                     ar.SetAttributeFromBlock(

  166                       ad,

  167                       br.BlockTransform

  168                     );

  169                     br.AttributeCollection.AppendAttribute(ar);

  170                     tr.AddNewlyCreatedDBObject(ar, true);

  171                   }

  172                 }

  173                 // End attrib/annot-scale su