This was an interesting one. I received an email from someone working on a significant BIM project that required external validation of some piping data coming from a competitive system. This system generates SAT files that – when imported into AutoCAD – represent pipes as surfaces rather than native AutoCAD (meaning Plant 3D or MEP) pipe objects.
The challenge was to determine the length of these pipes inside AutoCAD, despite the fact they aren’t pipes at all. Fun! :-)
The first thing I did was to take a look at the DWG data, to see what the pipes look like. Here’s a nice shaded view of a section of pipe:
While these look nice and pipe-like, they’re really just surfaces. When we switch to the 2D wireframe visual style we can see a little more about their representation:
But seeing the pipes shown in this way actually gives some promise: for instance, if we’re able to capture the graphics for this view, then we can offset the single isoline to be at the centre of the pipe. For an arc segment – a bend – this should mean offsetting it by the radius of the defining circle. For a linear segment this should be a simple translation. Which should give us a set of polylines and arcs that could then be joined to create a single centreline (where pipe sections are contiguous, of course).
But first things first, how do we capture the graphics? There’s a fairly common technique in ObjectARX that calls an entity’s WorldDraw function with an object that gets called for the various graphics primitives that represent the object. I wasn’t sure this was possible from .NET, but after a quick Google I found this excellent sample on the AutoCAD DevBlog written by Adam Nagy.
The code provided in that sample allowed me to diagnose the “primitives” that I really need to implement in my own code to capture the relevant graphics in the equivalent of the above wireframe mode (with RegenType set to StandardDisplay and NumberOfIsolines set to 1):
- Circles defined by three points.
- CircularArcs defined by three points.
- Polylines defined by sets of points.
All pretty straightforward, thankfully. I implemented the full boilerplate protocol for the various required classes and then fleshed out the above methods to generate AutoCAD entities.
Once the capture is completed, I can then go back through the collections of objects and offset the Arcs, transform the Polylines and delete the Circles (once we’ve finished with them, of course).
Here’s the complete C# code that does all this (it’s pretty lengthy, but anyway):
using System;
using Autodesk.AutoCAD.Colors;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;
using AcDb = Autodesk.AutoCAD.DatabaseServices;
using AcGi = Autodesk.AutoCAD.GraphicsInterface;
namespace ExtractPipeInformation
{
public class Commands
{
[CommandMethod("CTRLNS")]
public void GetGeometryCmd()
{
var ed = Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument.Editor;
var pso = new PromptSelectionOptions();
pso.MessageForAdding =
"Select surfaces for centerline extraction";
var psr = ed.GetSelection(pso);
if (psr.Status != PromptStatus.OK)
return;
var db = ed.Document.Database;
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
);
foreach (SelectedObject so in psr.Value)
{
var ent =
(Entity)tr.GetObject(so.ObjectId, OpenMode.ForRead);
var wd = new MyWorldDraw();
ent.WorldDraw(wd);
foreach (Polyline3d poly in wd.Polylines)
{
foreach (Circle cir in wd.Circles)
{
var pts = new Point3dCollection();
poly.IntersectWith(
cir, Intersect.OnBothOperands, pts,
IntPtr.Zero, IntPtr.Zero
);
if (pts.Count > 0)
{
var offset = cir.Center - pts[0];
var mat = Matrix3d.Displacement(offset);
poly.TransformBy(mat);
break;
}
}
ms.AppendEntity(poly);
tr.AddNewlyCreatedDBObject(poly, true);
}
foreach (Arc arc in wd.Arcs)
{
foreach (Circle cir in wd.Circles)
{
var pts = new Point3dCollection();
arc.IntersectWith(
cir, Intersect.OnBothOperands, pts,
IntPtr.Zero, IntPtr.Zero
);
if (pts.Count > 0)
{
var offset = cir.Center - pts[0];
var res = arc.GetOffsetCurves(cir.Radius);
foreach (Entity obj in res)
{
ms.AppendEntity(obj);
tr.AddNewlyCreatedDBObject(obj, true);
}
break;
}
}
arc.Dispose();
}
foreach (Circle cir in wd.Circles)
{
cir.Dispose();
}
}
tr.Commit();
}
}
}
public class MyContext : Context
{
public override Database Database
{
get { return HostApplicationServices.WorkingDatabase; }
}
public override bool IsBoundaryClipping
{
get { return false; }
}
public override bool IsPlotGeneration
{
get { return false; }
}
public override bool IsPostScriptOut
{
get { return false; }
}
}
public class MySubEntityTraits : SubEntityTraits
{
short color;
public override short Color
{
get { return color; }
set { color = value; }
}
int drawFlags;
public override int DrawFlags
{
get { return drawFlags; }
set { drawFlags = value; }
}
FillType fillType;
public override FillType FillType
{
get { return fillType; }
set { fillType = value; }
}
ObjectId layer;
public override ObjectId Layer
{
get { return layer; }
set { layer = value; }
}
ObjectId lineType;
public override ObjectId LineType
{
get { return lineType; }
set { lineType = value; }
}
double lineTypeScale;
public override double LineTypeScale
{
get { return lineTypeScale; }
set { lineTypeScale = value; }
}
LineWeight lineWeight;
public override LineWeight LineWeight
{
get { return lineWeight; }
set { lineWeight = value; }
}
Mapper mapper;
public override Mapper Mapper
{
get { return mapper; }
set { mapper = value; }
}
ObjectId material;
public override ObjectId Material
{
get { return material; }
set { material = value; }
}
PlotStyleDescriptor plotStyleDescriptor;
public override PlotStyleDescriptor PlotStyleDescriptor
{
get { return plotStyleDescriptor; }
set { plotStyleDescriptor = value; }
}
bool sectionable;
public override bool Sectionable
{
get { return sectionable; }
set { sectionable = value; }
}
bool selectionOnlyGeometry;
public override bool SelectionOnlyGeometry
{
get { return selectionOnlyGeometry; }
set { selectionOnlyGeometry = value; }
}
IntPtr markerId;
public override void SetSelectionMarker(IntPtr mrkrId)
{
markerId = mrkrId;
}
ShadowFlags shadowFlags;
public override ShadowFlags ShadowFlags
{
get { return shadowFlags; }
set { shadowFlags = value; }
}
double thickness;
public override double Thickness
{
get { return thickness; }
set { thickness = value; }
}
Transparency transparency;
public override Transparency Transparency
{
get { return transparency; }
set { transparency = value; }
}
EntityColor trueColor;
public override EntityColor TrueColor
{
get { return trueColor; }
set { trueColor = value; }
}
ObjectId visualStyle;
public override ObjectId VisualStyle
{
get { return visualStyle; }
set { visualStyle = value; }
}
}
public class MyWorldGeometry : WorldGeometry
{
public DBObjectCollection Polylines = new DBObjectCollection();
public DBObjectCollection Arcs = new DBObjectCollection();
public DBObjectCollection Circles = new DBObjectCollection();
public override void SetExtents(Extents3d extents)
{
}
public override void StartAttributesSegment()
{
}
public override bool Circle(
Point3d center, double radius, Vector3d normal
)
{
return true;
}
public override bool Circle(
Point3d firstPoint, Point3d secondPoint, Point3d thirdPoint
)
{
var ca =
new CircularArc3d(firstPoint, secondPoint, thirdPoint);
Circles.Add(new Circle(ca.Center, ca.Normal, ca.Radius));
return true;
}
public override bool CircularArc(
Point3d center, double radius, Vector3d normal,
Vector3d startVector, double sweepAngle, ArcType arcType
)
{
return true;
}
public override bool CircularArc(
Point3d start, Point3d point, Point3d endingPoint,
ArcType arcType
)
{
var ca = new CircularArc3d(start, point, endingPoint);
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
);
Arcs.Add(arc);
return true;
}
public override bool Draw(Drawable value)
{
return true;
}
public override bool EllipticalArc(
Point3d center, Vector3d normal, double majorAxisLength,
double minorAxisLength, double startDegreeInRads,
double endDegreeInRads, double tiltDegreeInRads,
ArcType arcType
)
{
return true;
}
public override bool Image(
ImageBGRA32 imageSource, Point3d position,
Vector3d u, Vector3d v
)
{
return true;
}
public override bool Image(
ImageBGRA32 imageSource, Point3d position,
Vector3d u, Vector3d v, TransparencyMode transparencyMode
)
{
return true;
}
public override bool Mesh(
int rows, int columns, Point3dCollection points,
EdgeData edgeData, FaceData faceData, VertexData vertexData,
bool bAutoGenerateNormals
)
{
return true;
}
public override Matrix3d ModelToWorldTransform
{
get { return Matrix3d.Identity; }
}
public override bool OwnerDraw(
GdiDrawObject gdiDrawObject, Point3d position,
Vector3d u, Vector3d v
)
{
return true;
}
public override bool PolyPolygon(
UInt32Collection numPolygonPositions,
Point3dCollection polygonPositions,
UInt32Collection numPolygonPoints,
Point3dCollection polygonPoints,
EntityColorCollection outlineColors,
LinetypeCollection outlineTypes,
EntityColorCollection fillColors,
TransparencyCollection fillOpacities
)
{
return true;
}
public override bool PolyPolyline(
PolylineCollection polylineCollection
)
{
return true;
}
public override bool Polygon(Point3dCollection points)
{
return true;
}
public override bool Polyline(
Point3dCollection points, Vector3d normal,
IntPtr subEntityMarker
)
{
var pl = new Polyline3d(Poly3dType.SimplePoly, points, false);
Polylines.Add(pl);
return true;
}
public override bool Polyline(AcGi.Polyline polylineObj)
{
return true;
}
public override bool Polyline(
AcDb.Polyline value, int fromIndex, int segments
)
{
return true;
}
public override bool Polypoint(
Point3dCollection points, Vector3dCollection normals,
IntPtrCollection subentityMarkers
)
{
return true;
}
public override void PopClipBoundary()
{
}
public override bool PopModelTransform()
{
return true;
}
public override bool PushClipBoundary(ClipBoundary boundary)
{
return true;
}
public override bool PushModelTransform(Vector3d normal)
{
return true;
}
public override bool PushModelTransform(Matrix3d matrix)
{
return true;
}
public override Matrix3d PushOrientationTransform(
OrientationBehavior behavior
)
{
return Matrix3d.Identity;
}
public override Matrix3d PushPositionTransform(
PositionBehavior behavior, Point3d offset
)
{
return Matrix3d.Identity;
}
public override Matrix3d PushPositionTransform(
PositionBehavior behavior, Point2d offset
)
{
return Matrix3d.Identity;
}
public override Matrix3d PushScaleTransform(
ScaleBehavior behavior, Point3d extents
)
{
return Matrix3d.Identity;
}
public override Matrix3d PushScaleTransform(
ScaleBehavior behavior, Point2d extents
)
{
return Matrix3d.Identity;
}
public override bool Ray(Point3d point1, Point3d point2)
{
return true;
}
public override bool RowOfDots(
int count, Point3d start, Vector3d step
)
{
return true;
}
public override bool Shell(
Point3dCollection points, IntegerCollection faces,
EdgeData edgeData, FaceData faceData, VertexData vertexData,
bool bAutoGenerateNormals
)
{
return true;
}
public override bool Text(
Point3d position, Vector3d normal, Vector3d direction,
double height, double width, double oblique, string message
)
{
return true;
}
public override bool Text(
Point3d position, Vector3d normal, Vector3d direction,
string message, bool raw, TextStyle textStyle
)
{
return true;
}
public override bool WorldLine(
Point3d startPoint, Point3d endPoint
)
{
return true;
}
public override Matrix3d WorldToModelTransform
{
get
{
return Matrix3d.Identity;
}
}
public override bool Xline(Point3d point1, Point3d point2)
{
return true;
}
}
public class MyWorldDraw : WorldDraw
{
public MyWorldDraw()
{
geometry = new MyWorldGeometry();
context = new MyContext();
subEntityTraits = new MySubEntityTraits();
}
private MyWorldGeometry geometry;
public DBObjectCollection Circles
{
get { return geometry.Circles; }
}
public DBObjectCollection Polylines
{
get { return geometry.Polylines; }
}
public DBObjectCollection Arcs
{
get { return geometry.Arcs; }
}
public override WorldGeometry Geometry
{
get { return geometry; }
}
private Context context;
public override Context Context
{
get { return context; }
}
public override double Deviation(
DeviationType deviationType, Point3d pointOnCurve
)
{
return 0.5;
}
public override bool IsDragging
{
get { return false; }
}
public override int NumberOfIsolines
{
get
{ return 1; }
}
public override Geometry RawGeometry
{
get { return geometry; }
}
public override bool RegenAbort
{
get { return false; }
}
public override RegenType RegenType
{
get { return RegenType.StandardDisplay; }
}
private SubEntityTraits subEntityTraits;
public override SubEntityTraits SubEntityTraits
{
get { return subEntityTraits; }
}
}
}
So what does it give us? If we run the CTRLNS command and select our section of pipe, we see some centreline geometry added to the drawing (in this case in yellow as I’ve made the current layer that colour).
I haven’t gone as far as to analyse the resultant geometry and attempt to join adjacent curves together – this can be done manually via the JOIN command, of course – but that may be something I look at in a future post.
Just having these centrelines should go a long way to simplify the process of measuring the length of the various pipes in the drawing. At least I hope so!