Creating an AutoCAD polyline from an exploded region using .NET

Thanks to Philippe Leefsma, from our DevTech team in Europe, for providing the code used as the basis for this post. I took Philippe's code and enhanced it to support arcs and to check for disconnected segments (which in theory should never happen, but it's better to be safe than to loop infinitely :-).

When you explode a region in AutoCAD, the resultant geometry is in the form of lines and arcs. The following technique shows how to take the lines and arcs returned by the Explode() function (which doesn't perform the equivalent of the EXPLODE command in AutoCAD, remember: it just returns the exploded geometry corresponding to the objects upon which it was called, they do not get added to the database and neither is the source entity erased) and use them to construct an equivalent Polyline object.

It's interesting code for a number of reasons:

  • It loops through and connects segments that may not be listed in sequence
  • It determines the bulge factor needed to make a Polyline segment geometrically equivalent to an Arc object
    • This is calculated as the tangent of a quarter of the included angle

Here's the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using System;

namespace RegionConversion

{

  public class Commands

  {

    [CommandMethod("RTP")]

    static public void RegionToPolyline()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

      PromptEntityOptions peo =

        new PromptEntityOptions("\nSelect a region:");

      peo.SetRejectMessage("\nMust be a region.");

      peo.AddAllowedClass(typeof(Region), true);

      PromptEntityResult per =

        ed.GetEntity(peo);

      if (per.Status != PromptStatus.OK)

        return;

      Transaction tr =

        doc.TransactionManager.StartTransaction();

      using (tr)

      {

        BlockTable bt =

          (BlockTable)tr.GetObject(

            db.BlockTableId,

            OpenMode.ForRead);

        BlockTableRecord btr =

          (BlockTableRecord)tr.GetObject(

            bt[BlockTableRecord.ModelSpace],

            OpenMode.ForRead);

        Region reg =

          tr.GetObject(

            per.ObjectId,

            OpenMode.ForRead) as Region;

        if (reg != null)

        {

          // Explode Region -> collection of Curves

          DBObjectCollection cvs =

            new DBObjectCollection();

          reg.Explode(cvs);

          // Create a plane to convert 3D coords

          // into Region coord system

          Plane pl =

            new Plane(new Point3d(0, 0, 0), reg.Normal);

          // The resulting Polyline

          Polyline p = new Polyline();

          // Set common entity properties from the Region

          p.SetPropertiesFrom(reg);

          // For initial Curve take the first in the list

          Curve cv1 = cvs[0] as Curve;

          p.AddVertexAt(

            p.NumberOfVertices,

            cv1.StartPoint.Convert2d(pl),

            BulgeFromCurve(cv1, false), 0, 0

          );

          p.AddVertexAt(

            p.NumberOfVertices,

            cv1.EndPoint.Convert2d(pl),

            0, 0, 0

          );

          cvs.Remove(cv1);

          // The next point to look for

          Point3d nextPt = cv1.EndPoint;

          // Find the line that is connected to

          // the next point

          // If for some reason the lines returned were not

          // connected, we could loop endlessly.

          // So we store the previous curve count and assume

          // that if this count has not been decreased by

          // looping completely through the segments once,

          // then we should not continue to loop.

          // Hopefully this will never happen, as the curves

          // should form a closed loop, but anyway...

          // Set the previous count as artificially high,

          // so that we loop once, at least.

          int prevCnt = cvs.Count + 1;

          while (cvs.Count > 0 && cvs.Count < prevCnt)

          {

            prevCnt = cvs.Count;

            foreach (Curve cv in cvs)

            {

              // If one end of the curve connects with the

              // point we're looking for...

              if (cv.StartPoint == nextPt ||

                  cv.EndPoint == nextPt)

              {

                // Calculate the bulge for the curve and

                // set it on the previous vertex

                double bulge =

                  BulgeFromCurve(cv, cv.EndPoint == nextPt);

                p.SetBulgeAt(p.NumberOfVertices - 1, bulge);

                // Reverse the points, if needed

                if (cv.StartPoint == nextPt)

                  nextPt = cv.EndPoint;

                else

                  // cv.EndPoint == nextPt

                  nextPt = cv.StartPoint;

                // Add out new vertex (bulge will be set next

                // time through, as needed)

                p.AddVertexAt(

                  p.NumberOfVertices,

                  nextPt.Convert2d(pl),

                  0, 0, 0

                );

                // Remove our curve from the list, which

                // decrements the count, of course

                cvs.Remove(cv);

                break;

              }

            }

          }

          if (cvs.Count >= prevCnt)

          {

            p.Dispose();

            ed.WriteMessage(

              "\nError connecting segments."

            );

          }

          else

          {

            // Once we have added all the Polyline's vertices,

            // transform it to the original region's plane

            p.TransformBy(Matrix3d.PlaneToWorld(pl));

            // Append our new Polyline to the database

            btr.UpgradeOpen();

            btr.AppendEntity(p);

            tr.AddNewlyCreatedDBObject(p, true);

            // Finally we erase the original region

            reg.UpgradeOpen();

            reg.Erase();

          }

        }

        tr.Commit();

      }

    }

    // Helper function to calculate the bulge for arcs

    private static double BulgeFromCurve(

      Curve cv,

      bool clockwise

    )

    {

      double bulge = 0.0;

      Arc a = cv as Arc;

      if (a != null)

      {

        double newStart;

        // The start angle is usually greater than the end,

        // as arcs are all counter-clockwise.

        // (If it isn't it's because the arc crosses the

        // 0-degree line, and we can subtract 2PI from the

        // start angle.)

        if (a.StartAngle > a.EndAngle)

          newStart = a.StartAngle - 8 * Math.Atan(1);

        else

          newStart = a.StartAngle;

        // Bulge is defined as the tan of

        // one fourth of the included angle

        bulge = Math.Tan((a.EndAngle - newStart) / 4);

        // If the curve is clockwise, we negate the bulge

        if (clockwise)

          bulge = -bulge;

      }

      return bulge;

    }

  }

}

To really put the code through its paces, try creating a Region in arbitrary 3D space, defined by a closed Polyline containing both arc and lines segments. The RTP command should replace the selected Region with a Polyine of the same shape.

I've done my best to anticipate as much as I can in the above code - my hope being that it will work on any Region - but if I've missed a case, be sure to let me know.

Update:

The above code doesn't take care of more complex regions and neither does it dispose of the temporary curves properly (thanks for ali for pointing out this second issue). Rather than fix this post, I've made the changes in an update to the next post, which is an evolution of this one.

 

8 responses to “Creating an AutoCAD polyline from an exploded region using .NET”

  1. Fernando Malard Avatar

    Kean,

    Problem 1:

    If the region has a hole the routine probably will fail once you cannot created either a disconnected polyline or a multi-polyline inside AutoCAD.

    Problem 2:

    If you have a composed region with disconnected loops the explode will return 2 new regions at it first call. The routine should repeat the explode until it does not find any AcDbRegion entity anymore.

    Problem 3:

    Depending on how the region is built you may end up with two or more coincident curve segments. The polyline build routine should be able to ignore this duplicated curves.

    I have faced these problems with a routine I have made to deal with regions but I have not checked if this sample will face the same problems but probably it will! 🙂

    Regards,
    Fernando.

  2. Excellent - thanks, Fernando!

    I knew there had to be some cases I'd missed - I'll update the code and post an update later in the week.

    Kean

  3. Hi Kean!

    Do you really want to learn how to type fast?
    Many years ago, I got a software to teach typing of an unusual way (to me).
    I never found it again, so I wrote my own.
    Don't worry about, because it's not a virus.
    If you like, I can send to you the source code (VB.NET)
    Enjoy!

    rapidshare.com/files...

  4. Interesting - thank you, Carlos!

    Kean

  5. Hi Kean,
    I'm not sure why, but your coordinate transformation are not inverse. When I transform the polyline back, it's deformed, so I replaced the Convert2d(pl) method with TransformBy(Matrix3d.WorldToPlane(pl)) and it works good.
    It returns Point3d, so you also have to transform it to Point2d.

  6. Hi Matus,

    It's a long time since I posted this, so I'm surprised there's a problem that hasn't yet been reported.

    Before I build it into an app to re-test, it would be good to know exactly the sequence of operations I need to follow to make it fail. Maybe you can send me instructions - ideally with a DWG - by email?

    Cheers,

    Kean

  7. Hi Kean,

    I'm not sure, but I have suspected that somewhere in this method I get this message after the debug ends.

    "Forgot to call Dispose? (Autodesk.AutoCAD.DatabaseServices.Line): DisposableWrapper"

    Is it something to do with the cvs[] elements not being disposed?

    Thanks,

  8. Kean Walmsley Avatar

    Hi ali,

    You're quite right: I somehow missed that.

    I'll post an update to the next post, as that's an evolution of the code in this one.

    Regards,

    Kean

Leave a Reply to Fernando Malard Cancel reply

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