using Autodesk.
AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using
DbSurface =
Autodesk.AutoCAD.DatabaseServices.Surface;
using
DbNurbSurface =
Autodesk.AutoCAD.DatabaseServices.NurbSurface;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using System.Collections.Generic;
using System;
namespace MinimumEnclosingSphere
{
public
class
Commands
{
[CommandMethod("MES", CommandFlags.UsePickSet)]
public
void MinimumEnclosingSphere()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Ask user to select entities
PromptSelectionOptions pso =
new
PromptSelectionOptions();
pso.MessageForAdding = "\nSelect objects to enclose: ";
pso.AllowDuplicates = false;
pso.AllowSubSelections = true;
pso.RejectObjectsFromNonCurrentSpace = true;
pso.RejectObjectsOnLockedLayers = false;
PromptSelectionResult psr = ed.GetSelection(pso);
if (psr.Status != PromptStatus.OK)
return;
bool oneSpherePerEnt = false;
if (psr.Value.Count > 1)
{
PromptKeywordOptions pko =
new
PromptKeywordOptions(
"\nMultiple objects selected: create " +
"individual spheres around each one?"
);
pko.AllowNone = true;
pko.Keywords.Add("Yes");
pko.Keywords.Add("No");
pko.Keywords.Default = "No";
PromptResult pkr = ed.GetKeywords(pko);
if (pkr.Status != PromptStatus.OK)
return;
oneSpherePerEnt = (pkr.StringResult == "Yes");
}
// There may be a SysVar defining the buffer
// to add to our radius
double buffer = 0.0;
try
{
object bufvar =
Application.GetSystemVariable(
"ENCLOSINGCIRCLEBUFFER"
);
if (bufvar != null)
{
short bufval = (short)bufvar;
buffer = bufval / 100.0;
}
}
catch
{
object bufvar =
Application.GetSystemVariable("USERI1");
if (bufvar != null)
{
short bufval = (short)bufvar;
buffer = bufval / 100.0;
}
}
// Collect points on the component entities
Point3dCollection pts = new
Point3dCollection();
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForWrite
);
for (int i = 0; i < psr.Value.Count; i++)
{
Entity ent =
(Entity)tr.GetObject(
psr.Value.ObjectId,
OpenMode.ForRead
);
// Collect the points for each selected entity
CollectPoints(tr, ent, pts);
// Create a sphere for each entity (if so chosen) or
// just once after collecting all the points
if (oneSpherePerEnt || i == psr.Value.Count - 1)
{
try
{
Solid3d sol = SphereFromPoints(pts, buffer);
btr.AppendEntity(sol);
tr.AddNewlyCreatedDBObject(sol, true);
}
catch
{
ed.WriteMessage(
"\nUnable to calculate enclosing sphere."
);
}
pts.Clear();
}
}
tr.Commit();
}
}
private
Solid3d SphereFromPoints(
Point3dCollection pts, double buffer
)
{
// Copy the points across
List<Point3d> pts3d = new
List<Point3d>(pts.Count);
for (int i = 0; i < pts.Count; i++)
{
pts3d.Add(pts);
}
// Assuming we have some points in our list...
if (pts.Count > 0)
{
// We need the center and radius of our sphere
Point3d center;
double radius = 0;
// Use our fast approximation algorithm to
// calculate the center and radius of our
// sphere to within 1% (calling the function
// with 100 iterations gives 10%, calling it
// with 10K gives 1%)
BadoiuClarksonIteration(
pts3d, 10000, out center, out radius
);
// Create the sphere and return it
Solid3d sol = new
Solid3d();
sol.CreateSphere(radius * (1.0 + buffer));
sol.TransformBy(
Matrix3d.Displacement(center - Point3d.Origin)
);
return sol;
}
return
null;
}
private
void CollectPoints(
Transaction tr, Entity ent, Point3dCollection pts
)
{
// We'll start by checking a block reference for
// attributes, getting their bounds and adding
// them to the point list. We'll still explode
// the BlockReference later, to gather points
// from other geometry, it's just that approach
// doesn't work for attributes (we only get the
// AttributeDefinitions, which don't have bounds)
BlockReference br = ent as
BlockReference;
if (br != null)
{
foreach (ObjectId arId in br.AttributeCollection)
{
DBObject obj = tr.GetObject(arId, OpenMode.ForRead);
if (obj is
AttributeReference)
{
AttributeReference ar = (AttributeReference)obj;
ExtractBounds(ar, pts);
}
}
}
// For surfaces we'll do something similar: we'll
// collect points across its surface (by first getting
// NURBS surfaces for the surface) and will still
// explode the surface later to get points from the
// curves
DbSurface sur = ent as
DbSurface;
if (sur != null)
{
DbNurbSurface[] nurbs = sur.ConvertToNurbSurface();
foreach (DbNurbSurface nurb in nurbs)
{
// Calculate the parameter increments in u and v
double ustart = nurb.UKnots.StartParameter,
uend = nurb.UKnots.EndParameter,
uinc = (uend - ustart) / nurb.UKnots.Count,
vstart = nurb.VKnots.StartParameter,
vend = nurb.VKnots.EndParameter,
vinc = (vend - vstart) / nurb.VKnots.Count;
// Pick up points across the surface
for (double u = ustart; u <= uend; u += uinc)
{
for (double v = vstart; v <= vend; v += vinc)
{
pts.Add(nurb.Evaluate(u, v));
}
}
}
}
// For 3D solids we'll fire a number of rays from the
// centroid in random directions in order to get a
// sampling of points on the outside
Solid3d sol = ent as
Solid3d;
if (sol != null)
{
for (int i = 0; i < 500; i++)
{
Solid3dMassProperties mp = sol.MassProperties;
using (Plane pl = new
Plane())
{
pl.Set(mp.Centroid, randomVector3d());
using (Region reg = sol.GetSection(pl))
{
using (Ray ray = new
Ray())
{
ray.BasePoint = mp.Centroid;
ray.UnitDir = randomVectorOnPlane(pl);
reg.IntersectWith(
ray, Intersect.OnBothOperands, pts,
IntPtr.Zero, IntPtr.Zero
);
}
}
}
}
}
// Now we start the terminal cases - for basic objects -
// before we recurse for more complex objects (including
// the ones we've already collected points for above).
// If we have a curve - other than a polyline, which
// we will want to explode - we'll get points along
// its length
Curve cur = ent as
Curve;
if (cur != null &&
!(cur is
Polyline ||
cur is
Polyline2d ||
cur is
Polyline3d))
{
// Two points are enough for a line, we'll go with
// a higher number for other curves
int segs = (ent is
Line ? 2 : 20);
double param = cur.EndParam - cur.StartParam;
for (int i = 0; i < segs; i++)
{
try
{
Point3d pt =
cur.GetPointAtParameter(
cur.StartParam + (i * param / (segs - 1))
);
pts.Add(pt);
}
catch { }
}
}
else
if (ent is
DBPoint)
{
// Points are easy
pts.Add(((DBPoint)ent).Position);
}
else
if (ent is
DBText)
{
// For DBText we use the same approach as
// for AttributeReferences
ExtractBounds((DBText)ent, pts);
}
else
if (ent is
MText)
{
// MText is also easy - you get all four corners
// returned by a function. That said, the points
// are of the MText's box, so may well be different
// from the bounds of the actual contents
MText txt = (MText)ent;
Point3dCollection pts2 = txt.GetBoundingPoints();
foreach (Point3d pt in pts2)
{
pts.Add(pt);
}
}
else
if (ent is
Face)
{
Face f = (Face)ent;
try
{
for (short i = 0; i < 4; i++)
{
pts.Add(f.GetVertexAt(i));
}
}
catch { }
}
else
if (ent is
Solid)
{
Solid s = (Solid)ent;
try
{
for (short i = 0; i < 4; i++)
{
pts.Add(s.GetPointAt(i));
}
}
catch { }
}
else
{
// Here's where we attempt to explode other types
// of object
DBObjectCollection oc = new
DBObjectCollection();
try
{
ent.Explode(oc);
if (oc.Count > 0)
{
foreach (DBObject obj in oc)
{
Entity ent2 = obj as
Entity;
if (ent2 != null && ent2.Visible)
{
CollectPoints(tr, ent2, pts);
}
obj.Dispose();
}
}
}
catch { }
}
}
// Return a random 3D vector on a plane
private
Vector3d randomVectorOnPlane(Plane pl)
{
// Create our random number generator
Random ran = new
Random();
// First we get the absolute value
// of our x, y and z coordinates
double absx = ran.NextDouble();
double absy = ran.NextDouble();
// Then we negate them, half of the time
double x = (ran.NextDouble() < 0.5 ? -absx : absx);
double y = (ran.NextDouble() < 0.5 ? -absy : absy);
Vector2d v2 = new
Vector2d(x,y);
return
new
Vector3d(pl, v2);
}
// Return a random 3D vector
private
Vector3d randomVector3d()
{
// Create our random number generator
Random ran = new
Random();
// First we get the absolute value
// of our x, y and z coordinates
double absx = ran.NextDouble();
double absy = ran.NextDouble();
double absz = ran.NextDouble();
// Then we negate them, half of the time
double x = (ran.NextDouble() < 0.5 ? -absx : absx);
double y = (ran.NextDouble() < 0.5 ? -absy : absy);
double z = (ran.NextDouble() < 0.5 ? -absz : absz);
return
new
Vector3d(x, y, z);
}
private
void ExtractBounds(
DBText txt, Point3dCollection pts
)
{
// We have a special approach for DBText and
// AttributeReference objects, as we want to get
// all four corners of the bounding box, even
// when the text or the containing block reference
// is rotated
if (txt.Bounds.HasValue && txt.Visible)
{
// Create a straight version of the text object
// and copy across all the relevant properties
// (stopped copying AlignmentPoint, as it would
// sometimes cause an eNotApplicable error)
// We'll create the text at the WCS origin
// with no rotation, so it's easier to use its
// extents
DBText txt2 = new
DBText();
txt2.Normal = Vector3d.ZAxis;
txt2.Position = Point3d.Origin;
// Other properties are copied from the original
txt2.TextString = txt.TextString;
txt2.TextStyleId = txt.TextStyleId;
txt2.LineWeight = txt.LineWeight;
txt2.Thickness = txt2.Thickness;
txt2.HorizontalMode = txt.HorizontalMode;
txt2.VerticalMode = txt.VerticalMode;
txt2.WidthFactor = txt.WidthFactor;
txt2.Height = txt.Height;
txt2.IsMirroredInX = txt2.IsMirroredInX;
txt2.IsMirroredInY = txt2.IsMirroredInY;
txt2.Oblique = txt.Oblique;
// Get its bounds if it has them defined
// (which it should, as the original did)
if (txt2.Bounds.HasValue)
{
Point3d maxPt = txt2.Bounds.Value.MaxPoint;
// Place all four corners of the bounding box
// in an array
Point2d[] bounds =
new
Point2d[] {
Point2d.Origin,
new
Point2d(0.0, maxPt.Y),
new
Point2d(maxPt.X, maxPt.Y),
new
Point2d(maxPt.X, 0.0)
};
// We're going to get each point's WCS coordinates
// using the plane the text is on
Plane pl = new
Plane(txt.Position, txt.Normal);
// Rotate each point and add its WCS location to the
// collection
foreach (Point2d pt in bounds)
{
pts.Add(
pl.EvaluatePoint(
pt.RotateBy(txt.Rotation, Point2d.Origin)
)
);
}
}
}
}
// Algorithm courtesy (and copyright of) Frank Nielsen
public
void BadoiuClarksonIteration(
List<Point3d> set, int iter,
out
Point3d cen, out
double rad
)
{
// Choose any point of the set as the initial
// circumcenter
cen = set[0];
rad = 0;
for (int i = 0; i < iter; i++)
{
int winner = 0;
double distmax = (cen - set[0]).Length;
// Maximum distance point
for (int j = 1; j < set.Count; j++)
{
double dist = (cen - set[j]).Length;
if (dist > distmax)
{
winner = j;
distmax = dist;
}
}
rad = distmax;
// Update
cen =
new
Point3d(
cen.X + (1.0 / (i + 1.0)) * (set[winner].X - cen.X),
cen.Y + (1.0 / (i + 1.0)) * (set[winner].Y - cen.Y),
cen.Z + (1.0 / (i + 1.0)) * (set[winner].Z - cen.Z)
);
}
}
}
}