Creating the smallest possible sphere around 3D AutoCAD geometry using .NET

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 =

 
0;       
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(

  &
#0160;  
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…

Our 3D geometry

… 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:

Mult
iple objects selected: create individual spheres around each one? [Yes/No] <No>: Y

Here are the results:

Spheres enclosing each 3D object

Which are perhaps more visible with the x-ray visual style applied:

And with the x-ray visual style

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.

2 responses to “Creating the smallest possible sphere around 3D AutoCAD geometry using .NET”

  1. Hi Kean,

    I was trying to imagine what the Badoiu-Clarkson algorithm would look like step by step, so thought why not put together a frame-by-frame video of it in action. And here it is: http://www.youtube.com/watch?v=T0QSDTe_keM

    The video covers 150 iterations, obviously with the FPS increasing as it goes. If I tried to show 10,000 iterations it would have been a pretty long video with not much happening.

    To take the snapshots I borrowed some code from the Screenshot plugin, and then used Windows Movie Maker to upload to YouTube.

    Cheers,
    Art

    p.s. Really cool series of posts.

  2. Kean Walmsley Avatar
    Kean Walmsley

    Really interesting - thanks, Art! 🙂

    That's a great screencast technique... have to think of future uses for it.

    Kean

Leave a Reply

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