Given the last few posts – where we gathered 2D points, created a minimal circle around them, and then gathered 3D points – the topic of this post shouldn’t come as much of a surprise. :-)
As suggested, the changes needed to support 3D in the algorithm we used to solve the smallest circle problem were trivial: we had to adjust the function protocol to pass in a list of 3D points and the implementation to deal with the Z coordinates provided. Looking at the code again, I did consider adjusting it to pass in a Point3dCollection rather than a List<Point3d> (I has chosen to use a generic list when implementing the original recursive algorithm and had just kept it with the newer iterative one), but then decided to leave that particular aspect alone.
Here’s the updated C# code:
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[i].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[i]);
}
// 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
// http://blog.informationgeometry.org/article.php?id=164
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)
);
}
}
}
}
We’ll run the new MES (Minimal Enclosing Sphere) command against the 3D geometry we saw in the last post…
… after setting ENCLOSINGCIRCLEBUFFER (we haven’t bothered implementing a separate ENCLOSINGSPHEREBUFFER, but we very easily could, if needed) to zero.
Command: ENCLOSINGCIRCLEBUFFER
Enter new value for ENCLOSINGCIRCLEBUFFER <3>: 0
Command: MES
Select objects to enclose: ALL
9 found
Select objects to enclose:
Multiple objects selected: create individual spheres around each one? [Yes/No] <No>: Y
Here are the results:
Which are perhaps more visible with the x-ray visual style applied:
Such operations certainly have the potential to be time-consuming… the code should probably be adjusted to show a progress meter or – at the very least – check HostApplicationServices.Current.UserBreak() from time to time. Something I’d certainly do myself, if preparing this code for production use, but for now left as an exercise for the reader.

Subscribe via RSS