Jigging an AutoCAD hatch using .NET (redux)

Many thanks to Holger Rasch who worked out how to fix the code in this previous post. It really doesn't matter that 3 years have passed, Holger – I have no doubt people will greatly appreciate the fact the code can now run without causing the annoying display issues it produced previously.

Holger made a few adjustments to the implementation to make sure the persistent hatch loop gets added and removed in the right places. Here's the updated C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Colors;

using System;

 

namespace HatchJig

{

  class JigUtils

  {

    // Custom ArcTangent method, as the Math.Atan

    // doesn't handle specific cases

 

    public static double Atan(double y, double x)

    {

      if (x > 0)

        return Math.Atan(y / x);

      else if (x < 0)

        return Math.Atan(y / x) - Math.PI;

      else  // x == 0

      {

        if (y > 0)

          return Math.PI;

        else if (y < 0)

          return -Math.PI;

        else // if (y == 0) theta is undefined

          return 0.0;

      }

    }

 

    // Computes Angle between current direction

    // (vector from last vertex to current vertex)

    // and the last pline segment

 

    public static double ComputeAngle(

      Point3d startPoint, Point3d endPoint,

      Vector3d xdir, Matrix3d ucs

    )

    {

      var v =

        new Vector3d(

          (endPoint.X - startPoint.X) / 2,

          (endPoint.Y - startPoint.Y) / 2,

          (endPoint.Z - startPoint.Z) / 2

        );

 

      double cos = v.DotProduct(xdir);

      double sin =

        v.DotProduct(

          Vector3d.ZAxis.TransformBy(ucs).CrossProduct(xdir)

        );

 

      return Atan(sin, cos);

    }

  }

 

  public class HatchJig : DrawJig

  {

    Point3d _tempPoint;

    bool _isArcSeg = false;

    bool _isUndoing = false;

    Matrix3d _ucs;

    Plane _plane;

    Polyline _pline = null;

    Hatch _hat = null;

 


60;   public HatchJig(

      Matrix3d ucs, Plane plane, Polyline pl, Hatch hat

    )

    {

      _ucs = ucs;

      _plane = plane;

      _pline = pl;

      _hat = hat;

      AddDummyVertex();

    }

 

    protected override bool WorldDraw(

      Autodesk.AutoCAD.GraphicsInterface.WorldDraw wd

    )

    {

      // Update the dummy vertex to be our 3D point

      // projected onto our plane

 

      if (_isArcSeg)

      {

        var lastVertex =

          _pline.GetPoint3dAt(_pline.NumberOfVertices - 2);

 

        Vector3d refDir;

 

        if (_pline.NumberOfVertices < 3)

          refDir = new Vector3d(1.0, 1.0, 0.0);

        else

        {

          // Check bulge to see if last segment was an arc or a line

 

          if (_pline.GetBulgeAt(_pline.NumberOfVertices - 3) != 0)

          {

            var arcSegment =

              _pline.GetArcSegmentAt(_pline.NumberOfVertices - 3);

 

            var tangent = arcSegment.GetTangent(lastVertex);

 

            // Reference direction is the invert of the arc tangent

            // at last vertex

 

            refDir = tangent.Direction.MultiplyBy(-1.0);

          }

          else

          {

            var pt =

              _pline.GetPoint3dAt(_pline.NumberOfVertices - 3);

 

            refDir =

              new Vector3d(

                lastVertex.X - pt.X,

                lastVertex.Y - pt.Y,

                lastVertex.Z - pt.Z

              );

          }

        }

 

        double angle =

          JigUtils.ComputeAngle(

            lastVertex, _tempPoint, refDir, _ucs

          );

 

        // Bulge is defined as tan of one fourth of included angle

        // Need to double the angle since it represents the included

        // angle of the arc

        // So formula is: bulge = Tan(angle * 2 * 0.25)

 

        double bulge = Math.Tan(angle * 0.5);

 

        _pline.SetBulgeAt(_pline.NumberOfVertices - 2, bulge);

      }

      else

  &#
0160;   {

        // Line mode. Need to remove last bulge if there was one

 

        if (_pline.NumberOfVertices > 1)

          _pline.SetBulgeAt(_pline.NumberOfVertices - 2, 0);

      }

 

      _pline.SetPointAt(

        _pline.NumberOfVertices - 1, _tempPoint.Convert2d(_plane)

      );

 

      // Only when we have enough vertices

 

      if (_pline.NumberOfVertices >2)

      {

        _pline.Closed = true;

 

        AppendTheLoop();

      }

 

      if (!wd.RegenAbort)

      {

        wd.Geometry.Draw(_pline);

 

        if (_pline.NumberOfVertices > 2)

        {

          _hat.EvaluateHatch(true);

          if (!wd.RegenAbort)

            wd.Geometry.Draw(_hat);

 

          // Take it out, we will create a new one later

 

          _hat.RemoveLoopAt(0);

        }

      }

 

      return true;

    }

 

    protected override SamplerStatus Sampler(JigPrompts prompts)

    {

      var jigOpts = new JigPromptPointOptions();

 

      jigOpts.UserInputControls =

        (UserInputControls.NullResponseAccepted |

         UserInputControls.NoNegativeResponseAccepted |

         UserInputControls.GovernedByOrthoMode);

 

      _isUndoing = false;

 

      if (_pline.NumberOfVertices == 1)

      {

        // For the first vertex, just ask for the point

 

        jigOpts.Message = "\nSpecify start point: ";

      }

      else if (_pline.NumberOfVertices > 1)

      {

        string msgAndKwds =

          (_isArcSeg ?

            "\nSpecify endpoint of arc or [Line/Undo]: " :

            "\nSpecify next point or [Arc/Undo]: "

          );

 

        string kwds = (_isArcSeg ? "Line Undo" : "Arc Undo");

 

        jigOpts.SetMessageAndKeywords(msgAndKwds, kwds);

      }

      else

        return SamplerStatus.Cancel; // Should never happen

 

      // Get the point itself

 

      var res = prompts.AcquirePoint(jigOpts);

 

      if (res.Status == PromptStatus.Keyword)

      {

        if (res.StringResult.ToUpper() == "ARC")

   &#
0160;      _isArcSeg = true;

        else if (res.StringResult.ToUpper() == "LINE")

          _isArcSeg = false;

        else if (res.StringResult.ToUpper() == "UNDO")

          _isUndoing = true;

 

        return SamplerStatus.OK;

      }

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

      {

        // Check if it has changed or not (reduces flicker)

 

        if (_tempPoint == res.Value)

          return SamplerStatus.NoChange;

        else

        {

          _tempPoint = res.Value;

          return SamplerStatus.OK;

        }

      }

 

      return SamplerStatus.Cancel;

    }

 

    public bool IsUndoing

    {

      get

      {

        return _isUndoing;

      }

    }

 

    public void AddDummyVertex()

    {

      // Create a new dummy vertex... can have any initial value

 

      _pline.AddVertexAt(

        _pline.NumberOfVertices, new Point2d(0, 0), 0, 0, 0

      );

    }

 

    public bool RemoveLastVertex()

    {

      // If there's a single vertex, don't attempt to remove the

      // vertex: would cause a degenerate geometry error

      // Returning false will tell the calling function to

      // abort the transaction

 

      if (_pline.NumberOfVertices == 1)

        return false;

 

      // Let's remove our dummy vertex  

 

      if (_pline.NumberOfVertices > 0)

        _pline.RemoveVertexAt(_pline.NumberOfVertices - 1);

 

      // And then check the type of the last segment

 

      if (_pline.NumberOfVertices >= 2)

      {

        double blg = _pline.GetBulgeAt(_pline.NumberOfVertices - 2);

        _isArcSeg = (blg != 0);

 

        // We have to sync the hatch with the polyline

 

        AppendTheLoop();

      }

 

      return true;

    }

 

    private void AppendTheLoop()

    {

      var ids = new ObjectIdCollection();

      ids.Add(_pline.ObjectId);

      _hat.Associative = true;

      _hat.AppendLoop(HatchLoopTypes.Default, ids);

    }

 

    [CommandMethod("HATJIG")]

    public static void RunHatchJig()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Create a transaction, as we're jigging

      // db-resident objects

 

      using (var tr = db.TransactionManager.StartTransaction())

      {

        var btr =

          (BlockTableRecord)tr.GetObject(

            db.CurrentSpaceId, OpenMode.ForWrite

          );

 

        var normal =

          Vector3d.ZAxis.TransformBy(

            ed.CurrentUserCoordinateSystem

          );

 

        // We will pass a plane to our jig, to help

        // with UCS transformations

 

        var plane = new Plane(Point3d.Origin, normal);

 

        // We also pass a db-resident polyline

 

        var pl = new Polyline();

        pl.Normal = normal;

 

        btr.AppendEntity(pl);

        tr.AddNewlyCreatedDBObject(pl, true);

 

        // And a db-resident hatch

 

        var hat = new Hatch();

 

        // Use a non-solid hatch pattern, to aid jigging

 

        hat.SetHatchPattern(

          HatchPatternType.PreDefined,

          "ANGLE"

        );

 

        // But let's make it transparent, for fun

        // Alpha value is Truncate(255 * (100-n)/100)

 

        hat.ColorIndex = 1;

        hat.Transparency = new Transparency(127);

 

        // Add the hatch to the modelspace & transaction

 

        var hatId = btr.AppendEntity(hat);

        tr.AddNewlyCreatedDBObject(hat, true);

 

        // And finally pass everything to the jig

 

        var jig =

          new HatchJig(

            ed.CurrentUserCoordinateSystem, plane, pl, hat

          );

 

        while (true)

        {

          var res = ed.Drag(jig);

 

          switch (res.Status)

          {

            // New point was added, keep going

 

            case PromptStatus.OK:

              jig.AddDummyVertex();

              break;

 

            // Keyword was entered

 

            case PromptStatus.Keyword:

              if (jig.IsUndoing)

                jig.RemoveLastVertex();

              break;

 

            // The jig completed successfully

 

            case PromptStatus.None:

 

              // You can remove this next line if you want

              // the vertex being jigged to be included

 

              if (jig.RemoveLastVertex())

              {

                tr.Commit();

              }

              return;

 

            // User cancelled the command

 

            default:

              // No need to erase the polyline & hatch, as

              // the transaction will simply be aborted

              return;

          }

        }

      }

    }

  }

}

Here's the code in action, with none of the former visual artifacts:

Jigging a hatch

Thanks again, Holger! 🙂

Update:

After a comment from Robbo, I went ahead and made the generated hatch associative to its boundary by adding a single line of code ("hat.Associative = true;") once the hatch has been made db-resident. You can, of course, comment out this line to revert to the previous behaviour.

Update 2:

Well that's strange. It turns out Holger's code was already setting boundary associativity… not sure how I missed that. Anyway, I have addressed an issue pointed out in the comments (thanks, aks!) where if you hit the spacebar immediately after starting the jig, an exception gets thrown. This has now been fixed in the above code.

15 responses to “Jigging an AutoCAD hatch using .NET (redux)”

  1. But this thing is pretty much useless because you cannot snap to any points. Perhaps you could show by example in full how to write this so that the user can snap to points. Thanks.

    1. Kean Walmsley Avatar

      That's just how jigs work. If you want to enable object snapping for a jig, this post should help.

      adndevblog.typepad.com/autocad/

      Kean

      1. Thank you. I have the hatch jig working thanks to the link regarding the pointmontor handler. I do not know if I have done it right, but it works and I think I managed to remove the point monitor handler in a way so that AutoCAD does not crash when AutoCAD closes. In my mind this hatch jig, and anything similar to it, should never be presented without object snap operation. I sincerely appreciate your immediate response that sent me in the right direction but I must say the code as presented is obviously that by one who does not use it.

  2. Hello Kean,

    How can this be modified for the hatch to be associative?

    Kind regards, Robbo.

    1. Kean Walmsley Avatar

      Hello Robbo,

      It's super-easy (in fact I'll update the post to include the line of code). Just set hat.Associative to true once the hatch has been added to the database and you'll have an associative hatch.

      Regards,

      Kean

      1. Thank you Kean. Find this very useful and have added a command 'Quick Hatch' to the right click menu. Kind regards, Robbo.

        1. Kean,

          I tried a similar thing for making the hatch Annotative too. But currently get error during debug. Any suggestions?

          Kindest regards, Robbo.

  3. Kean Walmsley Avatar

    Hi Robbo,

    Does this help?

    keanw.com/2007/04/making_autocad_.html

    Regards,

    Kean

  4. Hello Kean,

    It does, thank you!

    Added Line: _hat.Annotative = AnnotativeStates.True;

    to achieve this.

    Kind regards, Robbo.

  5. Hello Kean,

    This is a cool feature but I would like to use it to show you a problem I have since the 2015 upgrade. In my application I need to show temporary surface to the user and I am doing it by using "SOLID" transparent hatch and add it to the TransientManager ;

    So if I use this HATCHJIG example with few modifications :

    //Add a transient manager to the constructor of HatchJig
    _tm = TransientManager.CurrentTransientManager;
    _tm.AddTransient(_hat, TransientDrawingMode.Main, 128, _ic);

    //Remove the Associative = true from RunHatchJig() and //AppendTheLoop()
    hat.Associative = false;

    //Comment the 2 lines where the hatch is added to the database
    //var hatId = btr.AppendEntity(hat);
    //tr.AddNewlyCreatedDBObject(hat, true);

    //Finally, change wd.Geometry.Draw(_hat); by :
    _tm.UpdateTransient(_hat, _ic);

    You will have then a transparent hatch drawn as a transient, and if you play with it : launch several HATCHJIG, do some REGEN, you will notice that sometimes hatches disappear or reappear sometimes color of the hatches change to the current layer color etc...

    1. This sample was intended to make use of the jigging mechanism inside AutoCAD: I really can't provide support for a scenario where you're choosing to bypass it and use transient graphics directly. Sorry about that.

      Kean

  6. A RemoveVertexAt error occurs when starting the input with a Spacebar. (NumberOfVertices is 1 at that condition.) Adding a Try Catch at RemoveLastVertex() seems to fix that. I guess that is the appropriate fix.

    There is also a problem I have in what I am trying to do, which is performing the hatch jig with snaps not using hatjig but within a dialog, where the hatch needs to be properly re-evaluated ( I am thinking. ) upon finishing the process. When removing a last vertex the hatch disappears or in some cases slightly spills outside the boundary. Hatjig exhibits some of this behavior but corrects itself upon completion. My dialog version does not. Initially this problem was worse during the jig action. Adding _hat.EvaluateHatch(True) to AppendTheLoop() and also adding AppendTheLoop() to the If _pline.NumberOfVertices > 2 condition in WorldDraw took care of most of the problem during the jig. The issue with the hatch not being in the boundary still exists when a vertex has been removed during the jig process. Simply readjusting any boundary vertex restores the hatch to what it should be. I think a crude fix would be to update the boundary after completion but I do not know how to do that.

    1. Kean Walmsley Avatar

      I see... we're trying to remove the only vertex, which leads to a "degenerate geometry" exception being thrown. Rather than catching the exception, I've adjusted the code to check for this condition (which is cheaper) and then returning false from the RemoveLastVertex() function, which now tells the calling function to abort the transaction (we could also erase the entities, but hey - the cost isn't significant).

      Opening an object for write should be enough to cause an update to the boundary, if you end up needing to pursue a crude fix.

      Kean

  7. Awesome solution. It' s just what i was looking for. Only one question. How would i approach this if i wanted polar tracking turned on while using the command?

    1. Sorry - I'm not working with AutoCAD, at the moment. Please post your question to the AutoCAD .NET forum: someone there should be able to help.

      Kean

Leave a Reply to Kean Walmsley Cancel reply

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