I finally came up with a succinct title for this post after struggling with “Shading a face of an AutoCAD solid with a transparent hatch pumped through the transient graphics sub-system using .NET”. Or words to that effect. :-)
So yes, this post shows how to create a temporary hatch with transparent shading that then gets drawn as transient graphics at the right place in the model: in this case, the face selected using the approach shown in this previous post (which later evolved into a “look at” type feature).
The post was inspired by an email I received a number of weeks ago from Danijel Ivankovic. Danijel was looking to shade a transparent polygon inside AutoCAD – to highlight an area of a terrain model, if I remember correctly – and in the meantime had started heading down the path of layering custom graphics on top of AutoCAD’s using an OS-level API (something I advised him strongly against).
The transient graphics API introduced in AutoCAD 2009 is perfect for this, and works very well with the entity transparency introduced in AutoCAD 2011. Aside from the post linked to earlier, this implementation also borrowed heavily from this recent post.
Right then… here’s some C# code that asks the user to pick a solid’s face which then gets shaded using a temporary hatch:
using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Colors;
using Autodesk.AutoCAD.BoundaryRepresentation;
using BrFace =
Autodesk.AutoCAD.BoundaryRepresentation.Face;
using BrException =
Autodesk.AutoCAD.BoundaryRepresentation.Exception;
namespace FaceShading
{
public class Commands
{
// Static color index for auto-incrementing
static int _index = 1;
// Keep a list of the things we've drawn
// so we can undraw them
List<Drawable> _drawn = new List<Drawable>();
[CommandMethod("SHADEIDX")]
public void ResetShadingIndex()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter start vaue for color index shading: "
);
pio.LowerLimit = 1;
pio.UpperLimit = 256;
pio.DefaultValue = _index;
pio.UseDefaultValue = true;
PromptIntegerResult pir = ed.GetInteger(pio);
if (pir.Status == PromptStatus.OK)
{
_index = pir.Value;
}
}
[CommandMethod("SHADECLEAR")]
public void ClearShading()
{
// Clear any graphics we've drawn with the transient
// graphics API, then clear the list
TransientManager tm =
TransientManager.CurrentTransientManager;
IntegerCollection ic = new IntegerCollection();
foreach (Drawable d in _drawn)
{
tm.EraseTransient(d, ic);
}
_drawn.Clear();
}
[CommandMethod("SHADEFACE")]
public void ShadeFace()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
//ClearDrawnGraphics();
PromptEntityOptions peo =
new PromptEntityOptions(
"\nSelect face of solid:"
);
peo.SetRejectMessage("\nMust be a 3D solid.");
peo.AddAllowedClass(typeof(Solid3d), false);
PromptEntityResult per =
ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
Solid3d sol =
tr.GetObject(per.ObjectId, OpenMode.ForRead)
as Solid3d;
if (sol != null)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
sol.OwnerId,
OpenMode.ForWrite
);
Brep brp = new Brep(sol);
using (brp)
{
// We're going to check interference between our
// solid and a line we're creating between the
// picked point and the user (we use the view
// direction to decide in which direction to
// draw the line)
Point3d dir =
(Point3d)Application.GetSystemVariable("VIEWDIR");
Point3d picked = per.PickedPoint,
nearerUser =
per.PickedPoint - (dir - Point3d.Origin);
// Two hits should be enough (in and out)
const int numHits = 2;
// Create our line
Line3d ln = new Line3d(picked, nearerUser);
Hit[] hits = brp.GetLineContainment(ln, numHits);
ln.Dispose();
if (hits == null || hits.Length < numHits)
return;
// Set the shortest distance to something large
// and the index to the first item in the list
double shortest = (picked - nearerUser).Length;
int found = 0;
// Loop through and check the distance to the
// user (the depth of field).
for (int idx = 0; idx < numHits; idx++)
{
Hit hit = hits[idx];
double dist = (hit.Point - nearerUser).Length;
if (dist < shortest)
{
shortest = dist;
found = idx;
}
}
// Once we have the nearest point to the screen,
// use that one to get the containing curves
Point3dCollection verts;
if (CheckContainment(
ed,
brp,
hits[found].Point,
out verts
)
)
{
Hatch hat = CreateFromVertices(tr, btr, verts);
// If we get some back, get drawables for them and
// pass them through to the transient graphics API
if (hat != null)
{
TransientManager tm =
TransientManager.CurrentTransientManager;
IntegerCollection ic = new IntegerCollection();
tm.AddTransient(
hat,
TransientDrawingMode.Main,
0,
ic
);
_drawn.Add(hat);
}
}
}
}
tr.Commit();
}
}
private static bool CheckContainment(
Editor ed,
Brep brp,
Point3d pt,
out Point3dCollection pts
)
{
bool res = false;
pts = new Point3dCollection();
// Use the BRep API to get the lowest level
// container for the point
PointContainment pc;
BrepEntity be =
brp.GetPointContainment(pt, out pc);
using (be)
{
// Only if the point is on a boundary...
if (pc == PointContainment.OnBoundary)
{
// And only if the boundary is a face...
BrFace face = be as BrFace;
if (face != null)
{
// ... do we attempt to do something
try
{
foreach (BoundaryLoop bl in face.Loops)
{
// We'll return a curve for each edge in
// the containing loop
foreach (
Autodesk.AutoCAD.BoundaryRepresentation.Vertex v
in bl.Vertices
)
{
pts.Add(v.Point);
}
}
res = true;
}
catch (BrException)
{
res = false;
}
}
}
}
return res;
}
private Hatch CreateFromVertices(
Transaction tr,
BlockTableRecord btr,
Point3dCollection verts
)
{
if (verts.Count > 2)
{
// Create our first plane based on the first
// three points in our list (hopefully are not
// co-linear... maybe ought to check for this)
Vector3d u = verts[1] - verts[0];
Vector3d v = verts[2] - verts[0];
Point3d origin = verts[0];
Plane plane =
new Plane(
origin,
u.DivideBy(u.Length),
v.DivideBy(v.Length)
);
// Now recreate our plane from the first point and
// the normal of the temporary one (seems a little
// lazy - maybe there's a more elegant way to create
// an unbounded plane)
plane = new Plane(origin, plane.Normal);
// Create our polyline boundary, setting the normal
Autodesk.AutoCAD.DatabaseServices.Polyline pl =
new Autodesk.AutoCAD.DatabaseServices.Polyline();
pl.Normal = plane.Normal;
// Add our various vertices projected onto the plane
// of the polyline
foreach (Point3d vert in verts)
{
Point2d pt = vert.Convert2d(plane);
pl.AddVertexAt(pl.NumberOfVertices, pt, 0.0, 0.0, 0.0);
}
// Close our polyline and add it to the owning
// block table record (we'll soon erase it) and the
// transaction
pl.Closed = true;
ObjectIdCollection ids = new ObjectIdCollection();
ids.Add(btr.AppendEntity(pl));
tr.AddNewlyCreatedDBObject(pl, true);
// Create our hatch
Hatch hat = new Hatch();
hat.Normal = pl.Normal;
// Solid fill of our auto-incremented colour index
hat.SetHatchPattern(
HatchPatternType.PreDefined,
"SOLID"
);
hat.ColorIndex = _index++;
// Set our transparency to 25% (=127)
// Alpha value is Truncate(255 * (100-n)/100)
hat.Transparency = new Transparency(63);
// Add the hatch loop
hat.AppendLoop(HatchLoopTypes.Default, ids);
// Erase our polyline boundary
pl.Erase();
// Complete the hatch
hat.EvaluateHatch(true);
// Transform the hatch away from the origin
Matrix3d mat =
Matrix3d.Displacement(origin - Point3d.Origin);
hat.TransformBy(mat);
return hat;
}
return null;
}
}
}
For this to work your project will need a reference to acdbmgdbrep.dll, as well as the usual suspects, acmgd.dll and acdbmgd.dll.
To see the results let’s model a couple of simple solids:
We can then use the SHADEFACE command (and I admit I also used the SHADEIDX command to reset the auto-incremented colour index, as I wanted to keep the colours relatively bright – there’s a drab, gray patch early on in the colour index sequence :-) to shade individual faces in our two solids:
You’ll find that the graphic disappear when you REGEN, but they do come back when you start to orbit. To clear the graphics completely you may need to use the SHADECLEAR command, also implemented above.
I will say that this implementation has its quirks: the face picking may not work exactly as you expect it (as you need to pick an edge of the solids but the actual face detection uses the current view direction), and it won’t work for all solid types (including ones with circular face – even if they’re flat – such as cylinders), but the point is really to show the basic technique for this blog’s readership to tailor to their specific needs. As usual. :-)
I’m also not fully happy with creating a persistent polyline to define my hatch boundary. I tried using the AppendLoop() method with a HatchLoop created from a collection of transient geometry (such as LineSegment2d objects), but I wasn’t able to get it working. I ended up adding a polyline boundary temporarily to the drawing and then erasing it once the hatch has been generated properly. Not ideal, but there you go. If anyone has had better luck than I on this issue, please post a comment.