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