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.