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.