December 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      










« Creating the smallest possible circle around 2D AutoCAD geometry using .NET | Main | Creating the smallest possible sphere around 3D AutoCAD geometry using .NET »

February 11, 2011

Gathering points defining 3D AutoCAD geometry using .NET

After revealing the purpose for collecting points from 2D geometry in the last post, this post extends the 2D collection code to work with additional 3D objects. I don’t know whether it’s exhaustive or not – I’ve added more specific support for Solid3d and Surface objects – but I have no doubt people will let me know if I’ve missed anything, over time.

The good news is that the previous approach of exploding complex entities and processing their components means that many types of standard solid – such as boxes, cones and pyramids – will get adequately captured using this approach, as they have faces that comfortably get decomposed down to primitive objects that are handled by the existing code. The same is true of SubDMesh objects and even – to some degree – surfaces (although for these we are likely to only get the defining curves, rather than points along the surface itself).

To get more information from Surface objects was reasonably easy: the code creates NURBS surfaces from the surface and processes each of these by getting points inside the u and v parameter space. In addition to any enclosing curves that get processed as the Surface is exploded, of course. There may be a more straightforward way, but this appears to work, at least.

For Solid3ds it was a little more complex. I took some F# code from this previous post as a basis, to fire rays in random directions from the centroid of the solid and pick up the points at which the objects intersect. It’s not a guaranteed way to get points defining the solid, but with a couple of thousand points being generated by a thousand intersection operations, it should be representative enough. And that’s once again in addition to any curves or faces extracted from the solid as it’s exploded.

It should be said that none of these collection operations have been optimized for performance – this is mostly about getting lots of points to help generate an enclosing sphere, later on. A simple optimization would be to not perform the ray-firing technique on solids that we know are handled well by the explode approach (although that may require the use of COM to get the solid type, if memory serves me correctly). Anyway – this isn’t a definitive approach, this is more about sharing some code that I hope will prove useful to people who wish to take it further.

Here’s the updated C# code – the main changes are at the beginning of the CollectPoints() function, where I’ve added the handling of Solid3d and Surface objects. The function;s protocol has also be changed, slightly – rather than returning a collection of points from each call, we just pass the collection around and populate it directly. Not a big change, but it saved a couple of foreach loops that simply copy points from one place to another. Oh, and I was also forgetting to call Dispose() on the results of the Explode() operation, which only became apparent as a problem when dealing with larger models.

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 PointGathering

{

  public class Commands

  {

    [CommandMethod("GP", CommandFlags.UsePickSet)]

    public void GatherPoints()

    {

      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 entities 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;

 

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

          );

 

        foreach (SelectedObject so in psr.Value)

        {

          Entity ent =

            (Entity)tr.GetObject(

              so.ObjectId,

              OpenMode.ForRead

            );

 

          // Collect the points for each selected entity

 

          Point3dCollection entPts = new Point3dCollection();

          CollectPoints(tr, ent, entPts);

 

          // Add a physical DBPoint at each Point3d

 

          foreach(Point3d pt in entPts)

          {

            DBPoint dbp = new DBPoint(pt);

            btr.AppendEntity(dbp);

            tr.AddNewlyCreatedDBObject(dbp, true);

          }

        }

        tr.Commit();

      }

    }

 

    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)

              )

            );

          }

        }

      }

    }

  }

} 

Here’s the updated GP command in action with a selection of 3D geometry.

The basic geometry:

Our 3D geometry

With our points:

With our collected points

And with PDMODE set to 2:

And with PDMODE set to 2

In the next post we’ll extend this code that little bit further to create an enclosing sphere, as we did for enclosing circles last time.

blog comments powered by Disqus

Feed/Share

10 Random Posts