September 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        








« ADN Partner Channel on YouTube | Main | Future Plugin of the Month to embed QR Codes in AutoCAD drawings? »

June 28, 2010

Temporarily shading a face of an AutoCAD solid using .NET

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:

Standard 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:

Shaded 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.

blog comments powered by Disqus

Feed/Share

10 Random Posts