To follow on from yesterday’s post, today we’re taking a look at a more interactive – and iterative – approach to getting the length of a pipe (defined by a surface generated from an imported SAT file, we’re not talking about native Plant 3D objects). This is the second task discussed in the introductory post in this series.
We’re going to add a CTRLINES2 command that asks the user to select pipe section after pipe section, and will only generate the centreline for a newly-selected section if it’s contiguous to the section of pipe that’s being “managed” (i.e. whose length is being counted). The command will report the total length of the pipe via the command-line after each new section is selected.
You can either paste the code from today’s post into that from yesterday’s, or you can just download the updated file.
To help collect our “section” data (and this refers to a length of pipe rather than a cross-section), we’re going to add a new class:
public class PipeSection
{
public PipeSection()
{
_segments = new List<Curve>();
_start = Point3d.Origin;
_end = Point3d.Origin;
}
private List<Curve> _segments;
public List<Curve> Segments
{
get { return _segments; }
set { _segments = value; }
}
private Point3d _start;
public Point3d StartPoint
{
get { return _start; }
set { _start = value; }
}
private Point3d _end;
public Point3d EndPoint
{
get { return _end; }
set { _end = value; }
}
internal double GetLength()
{
double length = 0.0;
foreach (var segment in _segments)
{
length +=
segment.GetDistanceAtParameter(
segment.EndParam - segment.StartParam
);
}
return length;
}
internal void WriteToBlock(
Transaction tr, BlockTableRecord btr, int color
)
{
foreach (var ent in _segments)
{
btr.AppendEntity(ent);
tr.AddNewlyCreatedDBObject(ent, true);
ent.ColorIndex = color;
}
}
internal void Clear()
{
foreach (var ent in _segments)
{
ent.Dispose();
}
_segments.Clear();
}
}
Then we have the command definition itself (which belongs to the Commands class, of course):
[CommandMethod("CTRLNS2")]
public void GetGeometryCmd()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
// Ask the user to select the first object (we're
// not actually limiting selection to only surfaces)
var peo =
new PromptEntityOptions(
"\nSelect first object for centerline extraction"
);
var per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
bool cont = true;
double total = 0.0;
var start = Point3d.Origin;
var end = Point3d.Origin;
bool first = true;
while (cont)
{
using (var tr = db.TransactionManager.StartTransaction())
{
// Get the modelspace for write
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
var ms =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite
);
// Get the selected object, extract it's primitives,
// then try to link them into sections
var ent =
(Entity)tr.GetObject(per.ObjectId, OpenMode.ForRead);
var ents = ExtractPathPrimitives(ent);
var sections = LinkPrimitivesIntoSections(ents);
// Loop through the sections and attempt to stitch
// them with any that have been generated already
foreach (var section in sections)
{
bool connected = false;
var newStart = start;
var newEnd = end;
if (first)
{
newStart = section.StartPoint;
newEnd = section.EndPoint;
first = false;
connected = true;
}
else
{
if (SamePoint(start, section.StartPoint))
{
connected = true;
newStart = section.EndPoint;
}
else if (SamePoint(start, section.EndPoint))
{
connected = true;
newStart = section.StartPoint;
}
else if (SamePoint(end, section.StartPoint))
{
connected = true;
newEnd = section.EndPoint;
}
else if (SamePoint(end, section.EndPoint))
{
connected = true;
newEnd = section.StartPoint;
}
}
if (!connected)
{
ed.WriteMessage(
"\nSkipping non-contiguous section."
);
section.Clear();
}
else
{
start = newStart;
end = newEnd;
total += section.GetLength();
ed.WriteMessage("\nTotal pipe length: {0}", total);
section.WriteToBlock(tr, ms, 1);
}
}
tr.Commit();
}
// Change the prompt and ask again, looping if appropriate
peo.Message = "\nSelect next object";
per = ed.GetEntity(peo);
cont = per.Status == PromptStatus.OK;
}
}
And then – also in the Commands class – we’ll add a few helper functions to stitch segments into one or more sections:
private List<PipeSection> LinkPrimitivesIntoSections(
List<Curve> ents
)
{
var sections = new List<PipeSection>();
while (ents.Count > 0)
{
sections.Add(LinkSection(ents));
}
return sections;
}
private static PipeSection LinkSection(List<Curve> ents)
{
var section = new PipeSection();
// Use a flag to see whether any new segments were added in
// the last pass
bool foundNew = true;
bool first = true;
while (foundNew)
{
foundNew = false;
// Loop through the entities, looking for any with the
// same start- or end-point (but not both)
for (int i = 0; i < ents.Count; i++)
{
var cur = ents[i];
bool connected = false;
var newStart = section.StartPoint;
var newEnd = section.EndPoint;
// The first curve in our list gets special treatment
if (first)
{
newStart = cur.StartPoint;
newEnd = cur.EndPoint;
connected = true;
first = false;
}
else
{
// The others we check more carefully
if (
SamePoint(section.StartPoint, cur.StartPoint) &&
!SamePoint(section.EndPoint, cur.EndPoint)
)
{
connected = true;
newStart = cur.EndPoint;
}
else if (
SamePoint(section.StartPoint, cur.EndPoint) &&
!SamePoint(section.EndPoint, cur.StartPoint)
)
{
connected = true;
newStart = cur.StartPoint;
}
else if (
SamePoint(section.EndPoint, cur.StartPoint) &&
!SamePoint(section.StartPoint, cur.EndPoint)
)
{
connected = true;
newEnd = cur.EndPoint;
}
else if (
SamePoint(section.EndPoint, cur.EndPoint) &&
!SamePoint(section.StartPoint, cur.StartPoint)
)
{
connected = true;
newEnd = cur.StartPoint;
}
}
// If we found a connection...
if (connected)
{
// Update the section start- and end-points
section.StartPoint = newStart;
section.EndPoint = newEnd;
// Remove the curve from the list and add it to our
// section's segments
ents.RemoveAt(i--);
section.Segments.Add(cur);
foundNew = true;
}
}
}
return section;
}
This code still isn’t checking for overlap, so it’s admittedly a little fragile: it will often tell you that the newly-selected section isn’t contiguous, for instance. To really be useable it would need to handle overlapping segments more elegantly. If that were done, then I think the approach would rival the one shown yesterday (i.e. CTRLINES, OVERKILL then GSL). For now yesterday’s still seems the simplest – and ultimately most effective – approach to work with.
I’d be curious to get feedback from anyone who actually tries the code shown in the last two posts and – ideally – gets the chance to compare the results with those created manually. I do think it could prove to be very useful, but no doubt some tweaks will be needed – I’ve only dealt with the edge cases shown in a small sample of piping data, for instance: there may well be others that this code works less well against.