October 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 31  










« Selecting the nearest face of an AutoCAD solid using .NET | Main | Free online training: learn 3ds Max, Maya or MotionBuilder APIs in just one hour a day! »

January 22, 2009

Implementing a LookAt command for AutoCAD using .NET

This post follows on from this previous one, where we looked at a technique for picking a face on an AutoCAD solid. Tony Tanzillo kindly pointed out this much cleaner solution for this problem, and also highlighted a really simple (and elegant) way to implement LookAt using standard AutoCAD commands. While I really like both pointers provided by Tony, I've decided to persevere with my existing - admittedly sub-optimal - approach, as much as to show ways to exercise some APIs that people may not have used themselves. Please be warned, this isn't the simplest way to address this problem, and it doesn't even do as much as I'd like, but anyway. :-)

Here's the C# code:

using System;

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

using BrFace =

  Autodesk.AutoCAD.BoundaryRepresentation.Face;

using BrException =

  Autodesk.AutoCAD.BoundaryRepresentation.Exception;

using Autodesk.AutoCAD.Interop;


namespace LookAtFace

{

  public class Commands

  {

    [CommandMethod("lookat")]

    public void PickFace()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;


      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 dirp =

              (Point3d)Application.GetSystemVariable("VIEWDIR");


            Vector3d dir = dirp - Point3d.Origin;


            Point3d picked = per.PickedPoint,

                    nearerUser = per.PickedPoint + dir;


            // 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 * 10;

            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;

              }

            }


            Point3d closest = hits[found].Point;


            // 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,

                  closest,

                  ref curves

                )

            )

            {

              // Now we want to get a plane from our curves,

              // along with its normal


              Plane plane = null;

              if (curves.Count == 1)

              {

                // If we just have one curve, hopefully it's planar


                if (!curves[0].IsPlanar(out plane))

                {

                  plane = null;

                }

              }

              else if (curves.Count > 1)

              {

                // Otherwise we use two curves to define the plane


                if (!curves[0].IsCoplanarWith(curves[1], out plane))

                {

                  plane = null;

                }

              }


              // Assuming we have a plane, let's check the normal

              // is facing outwards


              if (plane != null)

              {

                // Get intersections between our "normal" line

                // and the solid


                ln = new Line3d(closest, closest + plane.Normal);

                hits = brp.GetLineContainment(ln, numHits);


                // Check whether these points are actually on the

                // line (if the params are zero or positive, that

                // means they are on the line). If both are, then

                // we need to reverse the normal, as it cuts

                // through our solid


                bool reverseNeeded = false;

                double param1, param2;

                if (ln.IsOn(hits[0].Point, out param1) &&

                    ln.IsOn(hits[1].Point, out param2))

                {

                  if (

                  (Math.Abs(param1) <= Tolerance.Global.EqualPoint

                        || param1 > 0) &&

                  (Math.Abs(param2) <= Tolerance.Global.EqualPoint

                        || param2 > 0)

                  )

                  {

                    reverseNeeded = true;

                  }

                }


                ln.Dispose();


                // Reverse, if needed


                Vector3d norm;

                if (reverseNeeded)

                {

                  norm = -plane.Normal;

                }

                else

                {

                  norm = plane.Normal;

                }


                // Now we set the view based on the normal


                SetView(

                  ed,

                  norm,

                  sol.GeometricExtents

                );

              }

            }

          }

        }

        tr.Commit();

      }

    }


    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;

    }


    private void SetView(

      Editor ed,

      Vector3d viewDir,

      Extents3d ext

    )

    {

      // We do a two part zoom... one gets us in the right

      // viewing direction


      ViewTableRecord rec = new ViewTableRecord();


      rec.IsPaperspaceView = false;

      rec.ViewDirection = viewDir;

      rec.CenterPoint = Point2d.Origin;

      rec.ViewTwist = 0.0;


      ed.SetCurrentView(rec);


      // And the other does a Zoom Window


      ZoomWin(

        ed,

        new Point2d(ext.MinPoint.X, ext.MinPoint.Y),

        new Point2d(ext.MaxPoint.X, ext.MaxPoint.Y)

      );

    }


    private static void ZoomWin(

      Editor ed, Point2d min, Point2d max

    )

    {

      string lower =

        min.ToString().Substring(

          1,

          min.ToString().Length - 2

        );


      string upper =

        max.ToString().Substring(

          1,

          max.ToString().Length - 2

        );


      string cmd =

        "_.ZOOM _W " + lower + " " + upper + " ";


      // Send the command(s)


      SendQuietCommand(ed.Document, cmd);

    }


    #region QuietCommandCalling


    const string kFinishCmd = "FINISH_COMMAND";


    private static void SendQuietCommand(

      Document doc,

      string cmd

    )

    {

      // Get the old value of NOMUTT


      object nomutt =

        Application.GetSystemVariable("NOMUTT");


      // Add the string to reset NOMUTT afterwards


      AcadDocument oDoc =

        (AcadDocument)doc.AcadDocument;

      oDoc.StartUndoMark();


      cmd += "_" + kFinishCmd + " ";


      // Set NOMUTT to 1, reducing cmd-line noise


      Application.SetSystemVariable("NOMUTT", 1);


      doc.SendStringToExecute(cmd, true, false, false);

    }


    [CommandMethod(kFinishCmd, CommandFlags.NoUndoMarker)]

    static public void FinishCommand()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      AcadDocument oDoc =

        (AcadDocument)doc.AcadDocument;

      oDoc.EndUndoMark();

      Application.SetSystemVariable("NOMUTT", 0);

    }

    #endregion

  }

}

 

Some comments on the implementation:

  • I've split the view change into two sections:
    • Change the view using ed.SetCurrentView() to have the right view direction
    • Use quiet command calling to Zoom Window on the face's extents
  • My ideal would be to have a smooth transition between the 3D views, showing the rotation of the model
    • I'm looking into ways to do this - SetCurrentView() doesn't support smooth view transitions, but I'm hoping that since the introduction of the ViewCube we have some other API of which I'm unaware that will fit the bill

Anyway, that's it for today. Please bear in mind the various caveats I've made about different approaches to solving this problem: while I don't usually like to post something using a sub-optimal technique, I think there are still pieces of the code that people will find of value.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e2010536eee26e970c

Listed below are links to weblogs that reference Implementing a LookAt command for AutoCAD using .NET:

blog comments powered by Disqus

Feed/Share

10 Random Posts