December 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      










« Jigging a frustum-shaped AutoCAD solid using .NET | Main | Update to the AutoCAD .NET entity jig framework »

December 05, 2012

A framework for defining AutoCAD entity jigs using .NET

In the last post we saw some code to create a frustum-shaped Solid3d object inside AutoCAD. I mentioned at the bottom of that post that there seemed to be an opportunity to write a framework of some kind to abstract away some of the repetitive code needed to create a multi-input jig. I probably didn’t say it in quite that way, but that was what I was getting at. :-)

Anyway, after having looked at it some more, here’s what I came up with: the EntityJigFramework. It’s a class derived from EntityJig that encapsulates some of the common code you’d otherwise need to write when creating a “complex” jig.

It’s still very much a work in progress – I’ve added support for JigPrompts.AcquireString(), AcquirePoint(), AcquireDistance() and AcquireAngle() in the various “phases” (more on that later), but there’s probably still work needed to make them fully functional – but I thought I’d put it out there for feedback, anyway.

Here’s the C# EntityJigFramework implementation, as it stands:

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using System.Collections.Generic;

using System;

 

namespace JigFrameworks

{

  // Our jig framework class

 

  public enum PhaseType

  {

    Distance = 0,

    Point,

    Angle,

    String

  }

 

  // For each input item to be requested by the user, we have a

  // "phase" object passed into the jig

 

  public class Phase

  {

    public Phase(

      string msg,

      PhaseType t = PhaseType.Distance,

      object defval = null,

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

    )

    {

      // Each phase has a type - depending on the type of data

      // being requested - a message, and optionally an offset

      // from the base point passed into the jig and a default

      // value

 

      Type = t;

      Message = msg;

      Offset = offset;

 

      // Initialise the relevant value property to the default

      // value, if provided

 

      if (t == PhaseType.Distance)

      {

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

      }

      else if (t == PhaseType.Point)

      {

        PointValue =

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

      }

      else if (t == PhaseType.String)

      {

        StringValue =

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

      }

      else if (t == PhaseType.Angle)

      {

        DoubleValue = (defval == null ? 0 : (double)defval);

      }

    }

 

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

 

    public PhaseType Type;

    public string Message;

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

 

    // The value we use will depend on the type (we could

    // probably use some kind of union here)

 

    public double DoubleValue;

    public string StringValue;

    public Point3d PointValue;

  };

 

  public class EntityJigFramework : EntityJig

  {

    // Member data

 

    Matrix3d _ucs;

    Point3d _cen;

    Entity _ent;

    List<Phase> _phases;

    int _phase;

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

 

    // Constructor

 

    public EntityJigFramework(

      Matrix3d ucs, Entity ent, Point3d cen,

      List<Phase> phases,

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

    ) : base(ent)

    {

      _ucs = ucs;

      _ent = ent;

      _cen = cen;

      _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

 

      Phase p = _phases[_phase];

 

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

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

 

      if (p.Type < PhaseType.String)

      {

        JigPromptGeometryOptions opts;

        switch (p.Type)

        {

          case PhaseType.Distance:

            opts = new JigPromptDistanceOptions();

            break;

          case PhaseType.Point:

            opts = new JigPromptPointOptions();

            break;

          case PhaseType.Angle:

            opts = new JigPromptAngleOptions();

            break;

          default: // Should never happen

            opts = null;

            break;

        }

 

        // 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 =

          (p.Offset == null ?

            _cen.TransformBy(_ucs) :

            (_cen + p.Offset.Invoke(_phases, _cen)).TransformBy(_ucs)

          );

 

        // The acquisition method varies on the phase type

 

        switch (p.Type)

        {

          case PhaseType.Distance:

            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(_phases[_phase].DoubleValue - pdr.Value) <

                Tolerance.Global.EqualPoint

              )

                return SamplerStatus.NoChange;

 

              // Otherwise we update the appropriate variable

              // based on the phase

 

              _phases[_phase].DoubleValue = pdr.Value;

              return SamplerStatus.OK;

            }

            break;

          case PhaseType.Point:

            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"

 

              if (

                (_phases[_phase].PointValue - ppr.Value).Length <

                Tolerance.Global.EqualPoint

              )

                return SamplerStatus.NoChange;

 

              // Otherwise we update the appropriate variable

              // based on the phase

 

              _phases[_phase].PointValue = ppr.Value;

              return SamplerStatus.OK;

            }

            break;

          case PhaseType.Angle:

            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 (

                _phases[_phase].DoubleValue - par.Value <

                Tolerance.Global.EqualPoint

              )

                return SamplerStatus.NoChange;

 

              // Otherwise we update the appropriate variable

              // based on the phase

 

              _phases[_phase].DoubleValue = par.Value;

              return SamplerStatus.OK;

            }

            break;

          default:

            break;

        }

      }

      else

      {

        // p.Type == PhaseType.String

 

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

 

        if (psr.Status == PromptStatus.OK)

        {

          _phases[_phase].StringValue = psr.StringResult;

          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, _cen, _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;

        }

      }

    }

  }

}

The basic idea is that you pass a list of “phase” objects – each of which defines a specific piece of data that needs to be acquired from the user – into your  framework’s constructor, along with a function to be called when your entity gets updated.

This function hopefully receives the information it needs to allow the entity to be updated – for now it gets the entity itself, the list of our phase data (the most important part of this being the values that have been acquired from the user), the “start point” and the current UCS. If there’s something else that needs passing through, it’s simple enough to modify the framework to do so.

You might notice in the phase object that there’s the option to specify a default value. I’ve been lazy here, and for the “distance” property I’ve set the hard-coded default to 1e-05 (i.e. 0.00001), as this is a value that allows the Solid3d.CreateFrustum() (and its counterparts) to consider the value as non-zero, so the calls don’t fail. I will probably modify the framework to set the hard-coded default to zero and the client code to pass in 1e-05 via the constructor to each phase object for distance acquisition, at some point.

So here’s how we can now reduce the C# code for our frustum jig:

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

 

        Transaction 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 Phase("\nSpecify bottom radius: "),

                new Phase("\nSpecify height: "),

                new Phase(

                  "\nSpecify top radius: ",

                  PhaseType.Distance,

                  1e-05,

                  (vals, pt) =>

                  {

                    return new Vector3d(0, 0, vals[1].DoubleValue);

                  }

                )

              },

              (e, vals, cen, ucs) =>

              {

                // Our entity update function

 

                Solid3d s = (Solid3d)e;

                s.CreateFrustum(

                  vals[1].DoubleValue,

                  vals[0].DoubleValue,

                  vals[0].DoubleValue,

                  vals[2].DoubleValue

                );

                s.TransformBy(

                  Matrix3d.Displacement(

                    cen.GetAsVector() +

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

                  ).PreMultiplyBy(ucs)

                );

                return true;

              }

            );

          jf.RunTillComplete(ed, tr);

        }

      }

    }

  }

}

Running the code results in exactly the same behaviour as before, as you might expect.

Much of the start of the above function is also fairly “boilerplate”, in that the jig framework might also own the transaction and the initial point selection, but I felt leaving that outside the framework provides more flexibility (even if it means more code needs copy & pasting – and ultimately maintaining – each time you create a new jig).

In a future post, I’ll go ahead and use this framework to implement further jigs for AutoCAD objects, to make sure it’s flexible enough.

blog comments powered by Disqus

Feed/Share

10 Random Posts