After introducing the series in the last post, today we’re going to look at one potential approach for this problem: we will extract and create centrelines for surface objects created after an SAT from an external piping system has been imported into AutoCAD as well as providing a basic command to count their lengths.
The code has evolved somewhat since we first saw it in this previous post: it now handles a number of previously problematic scenarios, such as the centrelines for pipe flanges being captured – it now discards polylines that intersect any cross-section circles that get captured, as these will clearly not constitute pipe centrelines – as well as dealing with a number of edge cases, such as the bends of polylines being approximated as polylines rather than arcs.
The accuracy of the results of this code hasn’t been validated, but it should nonetheless be a useful first step for anyone wishing to extend it into something useable in a production environment.
One of the first things I did with the project was to extract the “graphics capture” code and place it into its own source file. The only change over the previous version was the addition of its own namespace and a change in the deviation it returns (it was previously 0.5 – a value I inherited from Adam’s original sample – and I changed it to be a more appropriate 0.01).
The other source file this now code depends on was created by copying the code for the LongOperationManager from this previous post (adding it to a Utils namespace). I was happy to see the code still working properly – in our case to control AutoCAD’s progress meter to indicate how much of the extraction operation has completed – as the original post is now over six years old.
Here’s the C# code defining the updated CTRLINES command and the new GSL (for “Get Selection Length”) command:
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using AcDb = Autodesk.AutoCAD.DatabaseServices;
namespace ExtractPipeInformation
{
public class Commands
{
[CommandMethod("CTRLNS")]
public void ExtractCenterlines()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
var db = doc.Database;
// Ask the user to select the various surfaces
var pso = new PromptSelectionOptions();
pso.MessageForAdding =
"\nSelect surfaces for geometry extraction";
var psr = ed.GetSelection(pso);
if (psr.Status != PromptStatus.OK)
return;
// Start a transaction and open the modelspace for write
using (var tr = db.TransactionManager.StartTransaction())
{
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
var ms =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite
);
bool commit = true;
int count = 0;
// We'll use the progress meter via our special class
var lom =
new Utils.LongOperationManager("Creating centerlines");
using (lom)
{
lom.SetTotalOperations(psr.Value.Count);
foreach (SelectedObject so in psr.Value)
{
var source =
(Entity)tr.GetObject(so.ObjectId, OpenMode.ForRead);
var ents = ExtractPathPrimitives(source);
if (!lom.Tick())
{
commit = false;
ed.WriteMessage("\nOperation cancelled.");
break;
}
foreach (var ent in ents)
{
ms.AppendEntity(ent);
tr.AddNewlyCreatedDBObject(ent, true);
ent.ColorIndex = 1; // Results will be red
}
count += ents.Count;
}
}
if (commit)
{
ed.WriteMessage("\nGenerated {0} entities.", count);
tr.Commit();
}
}
}
[CommandMethod("GSL")]
public void GetSelectionLength()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
var db = doc.Database;
var tvs =
new TypedValue[1] {
new TypedValue((int)DxfCode.Start, "ARC,3DPOLYLINE")
};
var sf = new SelectionFilter(tvs);
var psr = ed.GetSelection();
if (psr.Status != PromptStatus.OK)
return;
double totalLength = 0.0;
int counted = 0;
using (var tr = db.TransactionManager.StartTransaction())
{
foreach (SelectedObject so in psr.Value)
{
var cur =
tr.GetObject(so.ObjectId, OpenMode.ForRead) as Curve;
if (cur != null)
{
totalLength +=
cur.GetDistanceAtParameter(
cur.EndParam - cur.StartParam
);
counted++;
}
}
tr.Commit();
}
ed.WriteMessage(
"\nCounted {0} objects with a total length of {1}.",
counted, totalLength
);
}
private static List<Curve> ExtractPathPrimitives(
Entity ent, bool includeCircles = false)
{
var ents = new List<Curve>();
if (ent is AcDb.Surface)
{
var wd = new GraphicsCapture.MyWorldDraw();
ent.WorldDraw(wd);
if (wd.Circles.Count != 0)
{
// Gather the min, max and average cross-section radii
// (for many pipe sections these will be the same)
double minRad = 0, maxRad = 0, avgRad = 0;
for (int i = 0; i < wd.Circles.Count; i++)
{
var cir = (Circle)wd.Circles[i];
double rad = cir.Radius;
if (i == 0)
{
minRad = maxRad = avgRad = rad;
}
else
{
minRad = Math.Min(minRad, rad);
maxRad = Math.Max(maxRad, rad);
avgRad += rad;
}
}
avgRad /= wd.Circles.Count;
// Extract and transform our polylines
foreach (Curve line in wd.Polylines)
{
var cur = ProcessPolyline(line, wd.Circles, maxRad);
if (cur != null)
{
ents.Add(cur);
}
}
// Extract and offset our arcs
foreach (Arc arc in wd.Arcs)
{
var cur = ProcessArc(arc, wd.Circles, avgRad);
if (cur != null)
{
ents.Add(cur);
}
}
// If debugging, we may set the "includeCircles" flag
// to generate circles, too. Otherwise we delete them
foreach (Circle cir in wd.Circles)
{
if (includeCircles)
ents.Add(cir);
else
cir.Dispose();
}
}
}
return ents;
}
private static Curve ProcessPolyline(
Curve line, DBObjectCollection circles, double maxRad
)
{
// Let's start by counting the vertices
var pl = (Polyline3d)line;
int vertexCount = 0;
foreach (var vid in pl)
{
vertexCount++;
}
// Anything with this many vertices is likely to be
// an approximation of a circle, so ignore it
if (vertexCount > 10)
{
line.Dispose();
return null;
}
var centers = new Point3dCollection();
double radius = 0;
bool first = true, differentRadii = false;
var offset = new Vector3d();
// Collect the centers of the various intersecting circles.
// These will be used in case the circles have different
// radii (we check this also). If they all have the same
// radius, we just use a standard displacement transformation
// to offset the polyline
foreach (Circle cir in circles)
{
var pts = new Point3dCollection();
line.IntersectWith(
cir, Intersect.OnBothOperands, pts,
IntPtr.Zero, IntPtr.Zero
);
if (pts.Count > 0)
{
// Add the centerpoint to our list
centers.Add(cir.Center);
// For the first circle we find intersecting, get its
// radius and the vector from the intersection to
// the center
if (first)
{
radius = cir.Radius;
offset = cir.Center - pts[0];
first = false;
}
else
{
// Check whether we have a different radius
// from before
differentRadii =
differentRadii ||
Math.Abs(radius - cir.Radius) >
Tolerance.Global.EqualPoint;
}
}
}
if (!first)
{
// We know at least one circle intersected the polyline
if (vertexCount <= 2)
{
// If we had different radii, create a new polyline
// through the various centerpoints
if (differentRadii)
{
line.Dispose();
var newline =
new Polyline3d(Poly3dType.SimplePoly, centers, false);
return newline;
}
else
{
// If the same radius was used thoughout, just offset
// the existing polyline
var mat = Matrix3d.Displacement(offset);
line.TransformBy(mat);
// If the resulting line intersects any circles,
// it's clearly not a centerline
if (IntersectsAny(line, circles))
{
line.Dispose();
return null;
}
return line;
}
}
else // vertexCount > 2 (but <= 10)
{
// Assume this is a polyline approximating a bend.
// We get the arc defined by the start-, mid- and end-
// points and process that instead
var ca =
new CircularArc3d(
line.StartPoint,
line.GetPointAtParameter(
(line.EndParam - line.StartParam) / 2
),
line.EndPoint
);
double angle =
ca.ReferenceVector.AngleOnPlane(
new Plane(ca.Center, ca.Normal)
);
var arc =
new Arc(
ca.Center,
ca.Normal,
ca.Radius,
ca.StartAngle + angle,
ca.EndAngle + angle
);
return ProcessArc(arc, circles, radius);
}
}
return null;
}
private static Curve ProcessArc(
Arc arc, DBObjectCollection circles, double defRad
)
{
Curve res = null;
// Check whether any circles intersect the arc
foreach (Circle cir in circles)
{
var pts = new Point3dCollection();
arc.IntersectWith(
cir, Intersect.OnBothOperands, pts,
IntPtr.Zero, IntPtr.Zero
);
// If so, we get the offset curves (adjusted by the radius)
// and set the first to be returned
if (pts.Count > 0)
{
var offres = arc.GetOffsetCurves(cir.Radius);
if (offres.Count > 0)
{
// Get the first curve (probably the only one)
// and dispose of any others
res = (Curve)offres[0];
for (int i = 1; i < offres.Count; i++)
{
offres[i].Dispose();
}
break;
}
}
}
// Even if there was no intersection, let's use
// the default radius passed in
if (res == null)
{
var curs = arc.GetOffsetCurves(defRad);
if (curs.Count > 0)
{
// Get the first curve (probably the only one)
// and dispose of any others
res = (Curve)curs[0];
for (int i = 1; i < curs.Count; i++)
{
curs[i].Dispose();
}
}
}
arc.Dispose();
return res;
}
private static bool IntersectsAny(
Curve cur, DBObjectCollection ents
)
{
var pts = new Point3dCollection();
foreach (Entity ent in ents)
{
cur.IntersectWith(
ent, Intersect.OnBothOperands, pts,
IntPtr.Zero, IntPtr.Zero
);
if (pts.Count > 0)
return true;
}
return false;
}
private static bool SamePoint(Point3d pt1, Point3d pt2)
{
return pt1.DistanceTo(pt2) < 0.0001;
}
}
}
Running the CTRLINES command generates the centrelines for our “pipes”. The code currently makes them red, but this is easy to change. Here’s a quick GIF showing the source pipes, the wireframe view with the centrelines, and then the centrelines on their own:
The GSL command can perform a very rudimentary count of the length of the various curves selected by the user. It doesn’t do anything very clever – such as checking for contiguity or overlapping lines – so I’d recommend running OVERKILL first to at least get rid of any overlap.
This approach seems to be fairly workable, all things considered. You might also choose to GROUP the centrelines together to make it easier to select them in another drawing session – the GSL command works just fine if a group is selected as input.
In the next post we’ll take a look at a slightly different take on this problem, where the user will be asked to select pipes one after the other, and the code attempts to stitch them together (or at least validate that they are contiguous). It remains to be seen whether it’ll end up being more interesting than today’s approach, but we’ll give it a try.