Update to the AutoCAD .NET entity jig framework

In the last post I introduced a simple framework to make the definition of multi-input entity jigs more straightforward. A big thanks to Chuck Wilbur, who provided some feedback that has resulted in a nicer base implementation, which we'll take for a spin in today's post.

Here's the updated C# framework code that makes use of a simple class hierarchy for our phase definitions:

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using System.Collections.Generic;

using System;

 

namespace JigFrameworks

{

  // Two abstract base classes: one for all phases...

 

  public abstract class Phase

  {

    // Our member data (could add real properties, as needed)

 

    public string Message;

    public object Value;

 

    public Phase(string msg)

    {

      Message = msg;

    }

  }

 

  // And another for geometric classes...

 

  public abstract class GeometryPhase : Phase

  {

    // Geometric classes can also have an offset for the basepoint

 

    public Func<List<Phase>, Point3d, Vector3d> Offset;

 

    public GeometryPhase(

      string msg,

      Func<List<Phase>, Point3d, Vector3d> offset = null

    ) : base(msg)

    {

      Offset = offset;

    }

  }

 

  // A phase for distance input

 

  public class DistancePhase : GeometryPhase

  {


0;  
public DistancePhase(

      string msg,

      object defval = null,

      Func<List<Phase>, Point3d, Vector3d> offset = null

    ) : base(msg, offset)

    {

      Value = (defval == null ? 0 : defval);

    }

  }

 

  // A phase for distance input related to Autodesk Shape Manager

  // (whose internal tolerance if 1e-06, so we need a larger

  // default value to allow Solid3d creation to succeed)

 

  public class SolidDistancePhase : DistancePhase

  {

    public SolidDistancePhase(

      string msg,

      object defval = null,

      Func<List<Phase>, Point3d, Vector3d> offset = null

    ) : base(msg, defval, offset)

    {

      Value = (defval == null ? 1e-05 : defval);

    }

  }

 

  // A phase for point input

 

  public class PointPhase : GeometryPhase

  {

    public PointPhase(

      string msg,

      object defval = null,

      Func<List<Phase>, Point3d, Vector3d> offset = null

    ) : base(msg, offset)

    {

      Value =

        (defval == null ? Point3d.Origin : defval);

    }

  }

 

  // A phase for angle input

 

  public class AnglePhase : GeometryPhase

  {

    public AnglePhase(

      string msg,

      object defval = null,

      Func<List<Phase>, Point3d, Vector3d> offset = null

    ) : base(msg, offset)

    {

      Value = (defval == null ? 0 : defval);

    }

  }

 

  // And a non-geometric phase for string input

 

  public class StringPhase : Phase

  {

    public StringPhase(

      string msg,

      string defval = null

    ) : base(msg)

    {

      Value = (defval == null ? "" : defval);

    }

  }

 

  // Our jig framework class

 

  public class EntityJigFramework : EntityJig

  {

    // Member data

 

    Matrix3d _ucs;

    Point3d _pt;

    Entity _ent;

    List<Phase> _phases;

    int _phase;

    Func<Entity, List<Phase>, Point3d, Matrix3d, bool> _update;

 

    // Constructor

 

    public EntityJigFramework(

      Matrix3d ucs, Entity ent, Point3d pt, List<Phase> phases,

      Func<Entity, List<Phase>, Point3d, Matrix3d, bool> update

    ) : base(ent)

    {

      _ucs = ucs;

      _ent = ent;

      _pt = pt;

      _phases = phases;

      _phase = 0;

      _update = update;

    }

 

    // Move on to the next phase

 

    internal void NextPhase()

    {

      _phase++;

    }

 

    // Check whether we're at the last phase

 

    internal bool IsLastPhase()

    {

      return (_phase == _phases.Count - 1);

    }

 

    // EntityJig protocol

 

    protected override SamplerStatus Sampler(JigPrompts prompts)

    {

      // Get the current phase

 

      var p = _phases[_phase];

 

      // If we're dealing with a geometry-typed phase (distance,

      // point ot angle input) we can use some common code

 

      var gp = p as GeometryPhase;

      if (gp != null)

      {

        JigPromptGeometryOptions opts;

        if (gp is DistancePhase)

          opts = new JigPromptDistanceOptions();

        else if (gp is AnglePhase)

          opts = new JigPromptAngleOptions();

        else if (gp is PointPhase)

          opts = new JigPromptPointOptions();

        else // Should never happen

          opts = null;

 

        // Set up the user controls

 

        opts.UserInputControls =

          (UserInputControls.Accept3dCoordinates

          | UserInputControls.NoZeroResponseAccepted

          | UserInputControls.NoNegativeResponseAccepted);

 

        // All our distance inputs will be with a base point

        // (which means the initial base point or an offset from

        // that)

 

        opts.UseBasePoint = true;

        opts.Cursor = CursorType.RubberBand;

 

        opts.Message = p.Message;

        opts.BasePoint =

          (gp.Offset == null ?

            _pt.TransformBy(_ucs) :

            (_pt + gp.Offset.Invoke(_phases, _pt)).TransformBy(_ucs)

          );

 

        // The acquisition method varies on the phase type

 

        if (gp is DistancePhase)

        {

          var phase = (DistancePhase)gp;

          var pdr =

            prompts.AcquireDistance(

              (JigPromptDistanceOptions)opts

            );

 

          if (pdr.Status == PromptStatus.OK)

          {

            // If the difference between the new value and its

            // previous value is negligible, return "no change"

 

            if (

              Math.Abs((double)phase.Value - pdr.Value) <

              Tolerance.Global.EqualPoint

            )

              return SamplerStatus.NoChange;

 

            // Otherwise we update the appropriate variable

            // based on the phase

 

            phase.Value = pdr.Value;

            _phases[_phase] = phase;

            return SamplerStatus.OK;

          }

        }

        else if (gp is PointPhase)

        {

          var phase = (PointPhase)gp;

          var ppr =

            prompts.AcquirePoint((JigPromptPointOptions)opts);

 

          if (ppr.Status == PromptStatus.OK)

          {

            // If the difference between the new value and its

            // previous value is negligible, return "no change"

 

            var tmp = ppr.Value.TransformBy(_ucs.Inverse());

 

            if (

              tmp.DistanceTo((Point3d)phase.Value) <

              Tolerance.Global.EqualPoint

            )

              return SamplerStatus.NoChange;

 

            // Otherwise we update the appropriate variable

            // based on the phase

 

            phase.Value = tmp;

            _phases[_phase] = phase;

  &
#160;        
return SamplerStatus.OK;

          }

        }

        else if (gp is AnglePhase)

        {

          var phase = (AnglePhase)gp;

          var par =

            prompts.AcquireAngle((JigPromptAngleOptions)opts);

 

          if (par.Status == PromptStatus.OK)

          {

            // If the difference between the new value and its

            // previous value is negligible, return "no change"

 

            if (

              (double)phase.Value - par.Value <

              Tolerance.Global.EqualPoint

            )

              return SamplerStatus.NoChange;

 

            // Otherwise we update the appropriate variable

            // based on the phase

 

            phase.Value = par.Value;

            _phases[_phase] = phase;

            return SamplerStatus.OK;

          }

        }

      }

      else

      {

        // p is StringPhase

 

        var phase = (StringPhase)p;

 

        var psr = prompts.AcquireString(p.Message);

 

        if (psr.Status == PromptStatus.OK)

        {

          phase.Value = psr.StringResult;

          _phases[_phase] = phase;

          return SamplerStatus.OK;

        }

      }

      return SamplerStatus.Cancel;

    }

 

    protected override bool Update()

    {

      // Right now we have an indiscriminate catch around our

      // entity update callback: this could be modified to be

      // more selective and/or to provide information on exceptions

 

      try

      {

        return _update.Invoke(_ent, _phases, _pt, _ucs);

      }

      catch

      {

        return false;

      }

    }

 

    public Entity GetEntity()

    {

      return Entity;

    }

 

    // Our method to perform the jig and step through the

    // phases until done

 

    internal void RunTillComplete(Editor ed, Transaction tr)

    {

      // Perform the jig operation in a loop

 

      while (true)

      {

        var res = ed.Drag(this);

 

        if (res.Status == PromptStatus.OK)

        {

          if (!IsLastPhase())

          {

            // Progress the phase

 

            NextPhase();

          }

          else

          {

            // Only commit when all phases have been accepted

 

            tr.Commit();

            return;

          }

        }

        else

        {

          // The user has cancelled: returning aborts the

          // transaction

 

          return;

        }

      }

    }

  }

}

Now let's take it for a spin. Here's some C# code that uses the framework to define jigs (and their respective commands) for frustums (FJ), cylinders (CYJ), cones (COJ) and tori (TJ):

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using System.Collections.Generic;

using System;

using JigFrameworks;

 

namespace EntityJigs

{

  public class Commands

  {

    [CommandMethod("FJ")]

    public void FrustumJig()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // First let's get the start position of the frustum

 

      var ppr = ed.GetPoint("\nSpecify frustum location: ");

 

      if (ppr.Status == PromptStatus.OK)

      {

        // In order for the visual style to be respected,

        // we'll add the to-be-jigged solid to the database

 

        var tr = doc.TransactionManager.StartTransaction();

        using (tr)

        {

          var btr =

            (BlockTableRecord)tr.GetObject(

              db.CurrentSpaceId, OpenMode.ForWrite

< p style="margin: 0px">            );

 

          var sol = new Solid3d();

          btr.AppendEntity(sol);

          tr.AddNewlyCreatedDBObject(sol, true);

 

          // Create our jig object passing in the selected point

 

          var jf =

            new EntityJigFramework(

              ed.CurrentUserCoordinateSystem, sol, ppr.Value,

              new List<Phase>()

              {

                // Three phases, one of which has a custom

                // offset for the base point

 

                new SolidDistancePhase("\nSpecify bottom radius: "),

                new SolidDistancePhase("\nSpecify height: "),

                new SolidDistancePhase(

                  "\nSpecify top radius: ",

                  1e-05,

                  (vals, pt) =>

                  {

                    return

                      new Vector3d(0, 0, (double)vals[1].Value);

                  }

                )

              },

              (e, vals, cen, ucs) =>

              {

                // Our entity update function

 

                var s = (Solid3d)e;

                s.CreateFrustum(

                  (double)vals[1].Value,

       
;           (
double)vals[0].Value,

                  (double)vals[0].Value,

                  (double)vals[2].Value

                );

                s.TransformBy(

                  Matrix3d.Displacement(

                    cen.GetAsVector() +

                    new Vector3d(0, 0, (double)vals[1].Value / 2)

                  ).PreMultiplyBy(ucs)

                );

                return true;

              }

            );

          jf.RunTillComplete(ed, tr);

        }

      }

    }

 

    [CommandMethod("CYJ")]

    public void CylinderJig()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // First let's get the start position of the cylinder

 

      var ppr = ed.GetPoint("\nSpecify cylinder location: ");

 

      if (ppr.Status == PromptStatus.OK)

      {

        // In order for the visual style to be respected,

        // we'll add the to-be-jigged solid to the database

 

        var tr = doc.TransactionManager.StartTransaction();

        using (tr)

        {

          var btr =

            (BlockTableRecord)tr.GetObject(

              db.CurrentSpaceId, OpenMode.ForWrite

          
;  );

 

          var sol = new Solid3d();

          btr.AppendEntity(sol);

          tr.AddNewlyCreatedDBObject(sol, true);

 

          // Create our jig object passing in the selected point

 

          var jf =

            new EntityJigFramework(

              ed.CurrentUserCoordinateSystem, sol, ppr.Value,

              new List<Phase>()

              {

                // Three phases, one of which has a custom

                // offset for the base point

 

                new SolidDistancePhase("\nSpecify radius: "),

                new SolidDistancePhase("\nSpecify height: "),

              },

              (e, vals, cen, ucs) =>

              {

                // Our entity update function

 

                var s = (Solid3d)e;

                s.CreateFrustum(

                  (double)vals[1].Value,

                  (double)vals[0].Value,

                  (double)vals[0].Value,

                  (double)vals[0].Value

                );

                s.TransformBy(

                  Matrix3d.Displacement(

                    cen.GetAsVector() +

                    new Vector3d(0, 0, (double)vals[1].Value / 2)

                  ).PreMultiplyBy(ucs)

             
60;  );

                return true;

              }

            );

          jf.RunTillComplete(ed, tr);

        }

      }

    }

 

    [CommandMethod("COJ")]

    public void ConeJig()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // First let's get the start position of the cylinder

 

      var ppr = ed.GetPoint("\nSpecify cone location: ");

 

      if (ppr.Status == PromptStatus.OK)

      {

        // In order for the visual style to be respected,

        // we'll add the to-be-jigged solid to the database

 

        var tr = doc.TransactionManager.StartTransaction();

        using (tr)

        {

          var btr =

            (BlockTableRecord)tr.GetObject(

              db.CurrentSpaceId, OpenMode.ForWrite

            );

 

          var sol = new Solid3d();

          btr.AppendEntity(sol);

          tr.AddNewlyCreatedDBObject(sol, true);

 

          // Create our jig object passing in the selected point

 

          var jf =

            new EntityJigFramework(

              ed.CurrentUserCoordinateSystem, sol, ppr.Value,

              new List<Phase>()

              {

                // Three phases, one of which has a custom

                // offset for the base point

 

                new SolidDistancePhase("\nSpecify radius: "),

                new SolidDistancePhase("\nSpecify height: "),

              },

              (e, vals, cen, ucs) =>

              {

                // Our entity update function

 

                var s = (Solid3d)e;

                s.CreateFrustum(

                  (double)vals[1].Value,

                  (double)vals[0].Value,

                  (double)vals[0].Value,

                  0

                );

                s.TransformBy(

                  Matrix3d.Displacement(

                    cen.GetAsVector() +

                    new Vector3d(0, 0, (double)vals[1].Value / 2)

                  ).PreMultiplyBy(ucs)

                );

                return true;

              }

            );

          jf.RunTillComplete(ed, tr);

        }

      }

    }

 

    [CommandMethod("TJ")]

    public void TorusJig()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // First let's get the start position of the frustum

 

      var ppr = ed.GetPoint("\nSpecify torus location: ");

 

      if (ppr.Status == PromptStatus.OK)

      {

        // In order for the visual style to be respected,

        // we'll add the to-be-jigged solid to the database

 

        var tr = doc.TransactionManager.StartTransaction();

        using (tr)

        {

          var btr =

            (BlockTableRecord)tr.GetObject(

              db.CurrentSpaceId, OpenMode.ForWrite

            );

 

          var sol = new Solid3d();

          btr.AppendEntity(sol);

          tr.AddNewlyCreatedDBObject(sol, true);

 

          // Create our jig object passing in the selected point

 

          var jf =

            new EntityJigFramework(

              ed.CurrentUserCoordinateSystem, sol, ppr.Value,

              new List<Phase>()

              {

                // Three phases, one of which has a custom

                // offset for the base point

 

                new SolidDistancePhase("\nSpecify outer radius: "),

                new SolidDistancePhase(

                  "\nSpecify inner radius: ",

                  1e-05,

                  (vals, pt) =>

                  {

                   
return

                      new Vector3d(0, 0, (double)vals[0].Value);

                  }

                )

              },

              (e, vals, cen, ucs) =>

              {

                // Our entity update function

 

                var s = (Solid3d)e;

                s.CreateTorus(

                  (double)vals[0].Value,

                  (double)vals[1].Value

                );

                s.TransformBy(

                  Matrix3d.Displacement(

                    cen.GetAsVector() +

                    new Vector3d(0, 0, (double)vals[1].Value)

                  ).PreMultiplyBy(ucs)

                );

                return true;

              }

            );

          jf.RunTillComplete(ed, tr);

        }

      }

    }

  }

}

Let's see these commands in action:

Jigging solids

In the next post we'll see a slightly more complicated example, where we jig a square (at least in X and Y) box between diagonal corners.

2 responses to “Update to the AutoCAD .NET entity jig framework”

  1. Very nice. For my next trick I was going to try to get rid of all the casting and if(gp is ...) stuff in Sampler. My goal would be to make Sampler look like this:

    protected override SamplerStatus Sampler(JigPrompts prompts)
    {
    // Get the current phase
    var p = _phases[_phase];
    return p.Acquire(prompts);
    }

    by adding this to Phase:
    public abstract class Phase
    {
    ...
    public abstract SamplerStatus Acquire(JigPrompts prompts);
    }

    I got a little ways with it, but ran into trouble with some of the member variables of EntityJigFramework. I wasn't sure which ones I should keep and pass to the Phase constructor, and which I could get rid of (I can't figure out why in the middle of the Acquire process there's always a line to set _phase[_phases] = phase, for example, since phase is set to _phase[_phases] at the top of the Sampler function).

    Anyway, here are the refactored Phase subclasses. I feel like they're close to working, but it would take more time than I have to untangle the member dependencies and if I changed them that drastically I'd want to unit test them and... well, you get the idea. Run with them if you want ๐Ÿ˜‰

    public abstract class GeometryPhase : Phase
    {
    // Geometric classes can also have an offset for the basepoint

    public Func<list<phase>, Point3d, Vector3d> Offset;

    public GeometryPhase(
    string msg,
    Func<list<phase>, Point3d, Vector3d> offset = null
    )
    : base(msg)
    {
    Offset = offset;
    }

    protected void SetOptions(JigPromptGeometryOptions opts)
    {
    // If we're dealing with a geometry-typed phase (distance,
    // point ot angle input) we can use some common code

    // Set up the user controls

    opts.UserInputControls =
    (UserInputControls.Accept3dCoordinates
    | UserInputControls.NoZeroResponseAccepted
    | UserInputControls.NoNegativeResponseAccepted);

    // All our distance inputs will be with a base point
    // (which means the initial base point or an offset from
    // that)

    opts.UseBasePoint = true;
    opts.Cursor = CursorType.RubberBand;

    opts.Message = Message;
    opts.BasePoint =
    (Offset == null ?
    _pt.TransformBy(_ucs) :
    (_pt + Offset.Invoke(_phases, _pt)).TransformBy(_ucs)
    );
    }
    }

    // A phase for distance input

    public class DistancePhase : GeometryPhase
    {
    public DistancePhase(
    string msg,
    object defval = null,
    Func<list<phase>, Point3d, Vector3d> offset = null
    )
    : base(msg, offset)
    {
    Value = (defval == null ? 0 : defval);
    }

    public override SamplerStatus Acquire(JigPrompts prompts)
    {
    var opts = new JigPromptDistanceOptions();
    SetOptions(opts);

    var pdr =
    prompts.AcquireDistance(opts);

    if (pdr.Status == PromptStatus.OK)
    {
    // If the difference between the new value and its
    // previous value is negligible, return "no change"

    if (
    Math.Abs((double)Value - pdr.Value) <
    Tolerance.Global.EqualPoint
    )
    return SamplerStatus.NoChange;

    // Otherwise we update the appropriate variable
    // based on the phase

    Value = pdr.Value;
    _phases[_phase] = this;
    return SamplerStatus.OK;
    }
    return SamplerStatus.Cancel;
    }
    }

    // A phase for distance input related to Autodesk Shape Manager
    // (whose internal tolerance if 1e-06, so we need a larger
    // default value to allow Solid3d creation to succeed)

    public class SolidDistancePhase : DistancePhase
    {
    public SolidDistancePhase(
    string msg,
    object defval = null,
    Func<list<phase>, Point3d, Vector3d> offset = null
    )
    : base(msg, defval, offset)
    {
    Value = (defval == null ? 1e-05 : defval);
    }
    }

    // A phase for point input

    public class PointPhase : GeometryPhase
    {
    public PointPhase(
    string msg,
    object defval = null,
    Func<list<phase>, Point3d, Vector3d> offset = null
    )
    : base(msg, offset)
    {
    Value =
    (defval == null ? Point3d.Origin : defval);
    }

    public override SamplerStatus Acquire(JigPrompts prompts)
    {
    var opts = new JigPromptPointOptions();
    SetOptions(opts);

    var ppr =
    prompts.AcquirePoint(opts);

    if (ppr.Status == PromptStatus.OK)
    {
    // If the difference between the new value and its
    // previous value is negligible, return "no change"

    var tmp = ppr.Value.TransformBy(_ucs.Inverse());

    if (
    tmp.DistanceTo((Point3d)Value) <
    Tolerance.Global.EqualPoint
    )
    return SamplerStatus.NoChange;

    // Otherwise we update the appropriate variable
    // based on the phase

    Value = tmp;
    _phases[_phase] = this;
    return SamplerStatus.OK;
    }
    return SamplerStatus.Cancel;
    }
    }

    // A phase for angle input

    public class AnglePhase : GeometryPhase
    {
    public AnglePhase(
    string msg,
    object defval = null,
    Func<list<phase>, Point3d, Vector3d> offset = null
    )
    : base(msg, offset)
    {
    Value = (defval == null ? 0 : defval);
    }

    public override SamplerStatus Acquire(JigPrompts prompts)
    {
    var opts = new JigPromptAngleOptions();
    SetOptions(opts);

    var par =
    prompts.AcquireAngle(opts);

    if (par.Status == PromptStatus.OK)
    {
    // If the difference between the new value and its
    // previous value is negligible, return "no change"

    if (
    (double)Value - par.Value <
    Tolerance.Global.EqualPoint
    )
    return SamplerStatus.NoChange;

    // Otherwise we update the appropriate variable
    // based on the phase

    Value = par.Value;
    _phases[_phase] = this;
    return SamplerStatus.OK;
    }
    return SamplerStatus.Cancel;
    }
    }

    // And a non-geometric phase for string input

    public class StringPhase : Phase
    {
    public StringPhase(
    string msg,
    string defval = null
    )
    : base(msg)
    {
    Value = (defval == null ? "" : defval);
    }

    public override SamplerStatus Acquire(JigPrompts prompts)
    {
    var psr = prompts.AcquireString(Message);

    if (psr.Status == PromptStatus.OK)
    {
    Value = psr.StringResult;
    _phases[_phase] = this;
    return SamplerStatus.OK;
    }
    return SamplerStatus.Cancel;
    }
    }

  2. Hi Chuck,

    Yes, it'd be nice to be free of any explicit RTTI checks but I'm not that inclined to keep tweaking the implementation, as it does start to get a little complicated (as you're finding). Hopefully someone else will run with it, should they find the time. ๐Ÿ™‚

    The _phases[_phase] = phase calls are actually redundant, as you've noticed. My intention was to place the modified objects back in the list, but as we're dealing with object references they're not needed.

    Cheers,

    Kean

Leave a Reply to Chuck Cancel reply

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