Selecting the nearest face of an AutoCAD solid using .NET

This post has come out of an interesting discussion I had with Jim Cameron at the ADN party at AU 2008. He mentioned an idea, which he kindly later reminded me of by email, which was to develop an AutoCAD equivalent for Inventor's LookAt functionality. I didn't know about LookAt before this discussion, but it seems it allows you to look at a particular face: you pick a face and it rotates the view and zooms in to centre it on the screen.

Rather than try to attack the whole problem at once, this post tackles selecting a face (which is slightly more complicated than perhaps it might be) and in a future post I'll hopefully manage to post some code to perform the view change.

Here's the C# code:

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.BoundaryRepresentation;

using BrFace =

  Autodesk.AutoCAD.BoundaryRepresentation.Face;

using BrException =

  Autodesk.AutoCAD.BoundaryRepresentation.Exception;

namespace LookAtFace

{

  public class Commands

  {

    // Keep a list of trhe things we've drawn

    // so we can undraw them

    List<Drawable> _drawn = new List<Drawable>();

    [CommandMethod("PICKFACE")]

    public void PickFace()

    {

      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)

        {

          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 out 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

            List<Curve3d> curves = new List<Curve3d>();

            if (CheckContainment(

                  ed,

                  brp,

                  hits[found].Point,

                  ref curves

                )

            )

            {

              // If we get some back, get drawables for them and

              // pass them through to the transient graphics API

              TransientManager tm =

                TransientManager.CurrentTransientManager;

              IntegerCollection ic = new IntegerCollection();

              foreach (Curve3d curve in curves)

              {

                Drawable d = GetDrawable(curve);

                tm.AddTransient(

                  d,

                  TransientDrawingMode.DirectTopmost,

                  0,

                  ic

                );

                _drawn.Add(d);

              }

            }

          }

        }

        tr.Commit();

      }

    }

    private void ClearDrawnGraphics()

    {

      // 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();

    }

    private Drawable GetDrawable(Curve3d curve)

    {

      // We could support multiple curve types here, but for

      // now let's just return a line approximating it

      Line ln = new Line(curve.StartPoint, curve.EndPoint);

      ln.ColorIndex = 1;

      return ln;

    }

    private static bool CheckContainment(

      Editor ed,

      Brep brp,

      Point3d pt,

      ref List<Curve3d> curves

    )

    {

      bool res = false;

      // 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 (Edge edge in bl.Edges)

                {

                  curves.Add(edge.Curve);

                }

              }

              res = true;

            }

            catch (BrException)

            {

              res = false;

            }

          }

        }

      }

      return res;

    }

  }

}

A few comments on the implementation:

  • We use the standard Editor.GetEntity() selection method - it gives us the ObjectId of the selected Solid3d but also the point that was picked.
  • Using this point and the view direction, we can then draw a line (which we make as big as the diagonal of the solid's bounding box, which should be large enough) from that point in the direction of the user.
  • The Boundary Representation (BRep) API allows us to determine how this line intersects the solid: we select the intersection nearest the screen, as presumably that's the one the user was intending to pick.
  • We will then use the BRep API to test the solid to see whether the point is contained by (or - and this is more likely - on) the solid, and it very helpfully provides us with the lowest-level topological entity that contains the point (which we hope to be a face).
  • The BRep API will throw an exception when traversing (typically during calls to GetEnumerator() for various collections) for a couple of unbounded solid-types (spheres, etc.) as we traverse: in this case we simply abort the containment checking operation.
  • We use the Transient Graphics API to display the edges of the selected face. Right now we just draw lines for each curve - which will be wrong for anything with arcs or circles, for instance - but at this stage we don't care a great deal about the graphics we're drawing - this is really just to make sure we're approximately accurate, and later we'll do something more intelligent with the edges we get back for the selected face.

Here's what happens when we use the PICKFACE command to select first one face and then another of a simple box:

Face picked

Another face picked

7 responses to “Selecting the nearest face of an AutoCAD solid using .NET”

  1. Hi Kean, thanks for another great
    example.

    "this post tackles selecting a face (which is slightly more complicated than perhaps it might be)"

    This isn't really that difficult. The EditorInput namespace has classes that facilitate selection of faces and edges of solids.

    The file at the following link has a helper class that simplifies it:

    caddzone.com/Sol...

    However, with regards to Inventor's "Look At" functionality, this comes reasonably close (with some needed error checking omitted):


    (defun C:LOOKAT ( / e )
    (if (setq e (entsel))
    (command
    "._UCS" "_Face" (cadr e) ""
    "._PLAN" "_current"
    "._ZOOM" "_Object" (car e) ""
    "._UCS" "_p"
    )
    )
    (princ)
    )

  2. Thanks, Tony.

    I initially started down the path of the standard selection functions, but hit a roadblock. I'll take a look at your helper file - it'll no doubt point out what I was missing.

    And yes - it is indeed much simpler just calling commands. I think I'm going to persevere with my existing approach as the code is likely to prove interesting/useful for other purposes, even if not the most efficient (and certainly not the most succinct) solution.

    Kean

  3. Hi Kean
    a quick question about my code and an error that it made me mad (now it appears resolved...)
    but i don't understood the real reason.
    Autocad without the bold rows in the code below , after a loop (8/10 times with 35 solids ) did crash.
    with this rows added now it's going ok.Have You got some idea ?
    I arrived here at this modify with debugger set "code unmanaged" and his file dmp.
    Thanks in advance

    public static IList<point3d> PrendiPuntiDelSolido(Solid3d sol)
    {

    IList<point3d> ptResult = new List<point3d>(0);
    try
    {
    if (sol.IsNull)
    return ptResult;

    using (AcBr.Brep brp = new AcBr.Brep(sol))
    {
    AcBr.BrepEdgeCollection edgeTrav = brp.Edges;
    foreach (AcBr.Edge edge in edgeTrav)
    {
    Point3d p = Point3d.Origin;
    Point3d p2 = Point3d.Origin;
    using (Autodesk.AutoCAD.BoundaryRepresentation.Vertex v = edge.Vertex1)
    {
    p = v.Point;
    if (!ptResult.Contains(p))
    ptResult.Add(p);

    }
    using (Autodesk.AutoCAD.BoundaryRepresentation.Vertex v2 = edge.Vertex2)
    {
    p2 = v2.Point;
    if (!ptResult.Contains(p2))
    ptResult.Add(p2);

    }

    using (Curve3d c3d = edge.Curve)
    {

    if (c3d == null)
    continue;

    if (p.IsEqualTo(p2, new Tolerance(0.01, 0.01)) && c3d.IsClosed())
    {
    using (ExternalCurve3d ca = edge.Curve as ExternalCurve3d)
    {
    using (CircularArc3d c = ca.NativeCurve as CircularArc3d)
    {
    if (c != null && c.IsClosed())
    {
    Vector3d vraggio = c.Center.GetAsVector() - p.GetAsVector();
    p2 = p + (vraggio * 2);
    if (!ptResult.Contains(p2))
    ptResult.Add(p2);

    }
    }
    }

    }
    }
    edge.Dispose();
    }
    }

    }
    catch (Autodesk.AutoCAD.BoundaryRepresentation.Exception exp)
    {
    System.Diagnostics.Trace.WriteLine("PrendiPuntiDelSolido errore " + exp.Message);

    }
    catch (System.Exception ex)
    {
    System.Diagnostics.Trace.WriteLine("PrendiPuntiDelSolido errore " + ex.Message);
    }
    return ptResult;
    }

  4. Hi Giuliano,

    Presumably some types of edge don't have associated curves. I honestly don't know the ins and outs of the Brep API in AutoCAD.

    Once again, this really isn't a forum for support: please post your questions (that don't relate directly to code I've posted) to the ADN team or the AutoCAD .NET Discussion Group.

    Regards,

    Kean

  5. ok sorry . I though this were be interesting . You can clean this page of my messagges.

  6. Thx for the code. Looks like a slight typo here?

    Point3d picked = per.PickedPoint,

    nearerUser =

    per.PickedPoint - (dir - Point3d.Origin);

    I think nearerUser should be a Point3d object?

    1. That's right: it's a multi-line variable declaration, so nearerUser is in fact a Point3d.

      Kean

Leave a Reply

Your email address will not be published. Required fields are marked *