This question came in a few weeks ago, which I thought worth addressing:
Hi, Kean! I have a question - how can we create hatch during jig?
The code in this post endeavours to do just that, but – be warned – isn’t fully successful. The problem I chose to interpret this as is “how do you jig the creation of a polyline, using it as the boundary for an associative hatch?”, and it’s not an easy one to solve.
I started by taking Philippe Leefsma’s code from this previous post, which jigs a polyline (including arc segments). I switched it to use a DrawJig – rather than an EntityJig – as we’re creating multiple entities (a polyline and a hatch). I created the objects we want to jig, adding them to the database and the transaction before passing them to the jig’s constructor – an approach that has proven successful for jigging objects in a certain way, whether a Solid3d with a visual style, or a BlockReference with attributes.
Here’s the 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
)
{
Vector3d 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;
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)
{
Point3d 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)
{
CircularArc3d arcSegment =
_pline.GetArcSegmentAt(_pline.NumberOfVertices - 3);
Line3d tangent = arcSegment.GetTangent(lastVertex);
// Reference direction is the invert of the arc tangent
// at last vertex
refDir = tangent.Direction.MultiplyBy(-1.0);
}
else
{
Point3d 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
{
// 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)
);
if (_pline.NumberOfVertices == 3)
{
_pline.Closed = true;
ObjectIdCollection ids = new ObjectIdCollection();
ids.Add(_pline.ObjectId);
// Add the hatch loops and complete the hatch
_hat.Associative = true;
_hat.AppendLoop(HatchLoopTypes.Default, ids);
}
if (!wd.RegenAbort)
{
wd.Geometry.Draw(_pline);
if (_pline.NumberOfVertices > 2)
{
_hat.EvaluateHatch(true);
if (!wd.RegenAbort)
wd.Geometry.Draw(_hat);
}
}
return true;
}
protected override SamplerStatus Sampler(JigPrompts prompts)
{
JigPromptPointOptions jigOpts = new JigPromptPointOptions();
jigOpts.UserInputControls =
(UserInputControls.Accept3dCoordinates |
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
PromptPointResult res = prompts.AcquirePoint(jigOpts);
if (res.Status == PromptStatus.Keyword)
{
if (res.StringResult.ToUpper() == "ARC")
_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 void RemoveLastVertex()
{
// Let's first 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);
}
}
[CommandMethod("HATJIG")]
public static void RunHatchJig()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Create a transaction, as we're jigging
// db-resident objects
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId, OpenMode.ForWrite
);
Vector3d normal =
Vector3d.ZAxis.TransformBy(
ed.CurrentUserCoordinateSystem
);
// We will pass a plane to our jig, to help
// with UCS transformations
Plane plane = new Plane(Point3d.Origin, normal);
// We also pass a db-resident polyline
Polyline pl = new Polyline();
pl.Normal = normal;
btr.AppendEntity(pl);
tr.AddNewlyCreatedDBObject(pl, true);
// And a db-resident hatch
Hatch 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
ObjectId hatId = btr.AppendEntity(hat);
tr.AddNewlyCreatedDBObject(hat, true);
// And finally pass everything to the jig
HatchJig jig =
new HatchJig(
ed.CurrentUserCoordinateSystem, plane, pl, hat
);
while (true)
{
PromptResult 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
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;
}
}
}
}
}
}
Overall the code works, but the graphics refresh has some issues. The HATJIG command starts well enough:
But as we move the cursor around, there is unfortunate ghosting of the hatch graphics:
Even as we pick new boundary points the hatch gets shown but doesn’t stop being displayed as the boundary changes. It seems as though the transient graphics sub-system – which is being fed the graphics from the DrawJig’s WorldDraw() method – seems to think more needs to be displayed than is actually the case.
At least when the jig is completed the polyline and hatch are displayed properly:
There may be other approaches to solve this – such as using the transient graphics API directly – but I suspect that the nature of the hatch implementation will make it hard to rely on its evaluation and display work effectively when called transiently, whether via a jig or directly.
I decided to post this anyway, in case any readers have bright ideas on how either to fix this implementation or to solve it differently. Someone may even have implemented a solution they don’t mind sharing. Anyone?