TheSwamp
Code Red => .NET => Topic started by: Chumplybum on November 05, 2008, 10:10:45 PM
-
hi all, i'm trying to find the centroid of a closed polyline.
i've read a few lisp examples that can do this (including one on the swamp http://www.theswamp.org/index.php?topic=18725.0 (http://www.theswamp.org/index.php?topic=18725.0)) but haven't been able to transform this code to .net. From the lisp examples it seems that the polyline is converted to a region and then the centriod is found from the region, however from what i can see there is no centroid property (although there is a centroid property under solid3dmassproperties).
So i couldn't convert lisp to .net and went off and found the formula for the centroid of a polyline (http://en.wikipedia.org/wiki/Centroid (http://en.wikipedia.org/wiki/Centroid)) as well as a vb.net sample (http://www.vb-helper.com/howto_net_polygon_centroid.html (http://www.vb-helper.com/howto_net_polygon_centroid.html)) after slightly modifying the code i've come up with the following...
' For polylines
Dim pl As AADS.Polyline = TryCast(context.PickedObject, AADS.Polyline)
If pl Is Nothing OrElse pl.Closed = False Then Return
Dim RunningX As Long
Dim RunningY As Long
Dim second_factor As Long
For Counter As Integer = 0 To pl.NumberOfVertices - 2
second_factor = pl.GetPoint2dAt(Counter).X * pl.GetPoint2dAt(Counter + 1).Y - pl.GetPoint2dAt(Counter + 1).X * pl.GetPoint2dAt(Counter).Y
RunningX += (pl.GetPoint2dAt(Counter).X + pl.GetPoint2dAt(Counter + 1).X) * second_factor
RunningY += (pl.GetPoint2dAt(Counter).Y + pl.GetPoint2dAt(Counter + 1).Y) * second_factor
Next
Dim X, Y As Long
' Divide by 6 times the polygon's area.
Dim polygon_area As Single = pl.Area
X = RunningX / (6 * pl.Area)
Y = RunningY / (6 * pl.Area)
' If the values are negative, the polygon is
' oriented counterclockwise. Reverse the signs.
If X < 0 Then
X = -X
Y = -Y
End If
Dim CentroidPoint As AAG.Point3d = New AAG.Point3d(X, Y, pl.Elevation)
the code works for a rectangle that is drawn at 0,0... however, when the polyline moves away from 0,0 the code produces some crazy results and if the polyline is in a different quadrant about the usc it produces some more crazy results
does anyone know of a better / easier way to find the centroid, or a problem with my code???
any help would be great
cheers, Mark
-
You have to do the calculations relative to the object you're trying to measure. The code would
look something like this (sorry I don't have VB skills)
dim ptRef as Point2d
dim pt1 as Point2d
dim pt2 as Point2d
dim second_factor as double
dim drX as double
dim drY as double
For Counter as Integer = 0 To pl.NumberOfVertices - 2
pt1 = pl.GetPoint2dAt(Counter)
Pt2 = pl.GetPoint2dAt(counter + 1)
if i == 0 then
ptRef = pt1
endif
second_factor = (pt1.x - ptRef.x) * (pt2.y - ptRef.y) - (pt2.x - ptRef.x) * (pt1.y - ptRef.y);
drX += ((pt1.x - ptRef.x) + (pt2.x - ptRef.x)) * b0;
drY += ((pt1.y - ptRef.y) + (pt2.y - ptRef.y)) * b0;
Next
x = drX / (6 * pl.area) + ptRef.x
y = drY / (6 * pl.area) + ptRef.y
With this there is no need to reverse sign at the end. Your example code is declaring some variables
as Long when they should be doubles (or whatever VB calls them). Was your code returning
a float or double for those computations? If they are then I would turn on the Option strict to get a
little tighter control over the VB.
Paul
-
It looks to me like you are missing one side of the polygon -- the one from the last point to the first point. I note that the sample code at http://www.vb-helper.com/howto_net_polygon_centroid.html makes a copy of the first point at the end, but I don't see where you do that.
-
You could also used TF.net http://code.google.com/p/tf-net/ (http://code.google.com/p/tf-net/) and let it do the work for you.
C# code, but I'm sure you can convert it
using System;
using System.Linq;
using System.Text;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Topology.Geometries;
using Topology.IO.Dwg;
namespace TF_Test
{
public class TF_Test
{
[CommandMethod("TFCentroid")]
public void FindCentroid()
{
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptEntityOptions opts = new PromptEntityOptions("\nSelect Entity to Query: ");
opts.AllowNone = true;
opts.AllowObjectOnLockedLayer = true;
opts.SetRejectMessage("\nDon't pick that kind ");
opts.AddAllowedClass(typeof(Polyline), false);
//Select the object on-screen
PromptEntityResult res = ed.GetEntity(opts);
if (res.Status == PromptStatus.OK)
{
ObjectId ObjID = res.ObjectId;
try
{
using (Transaction trans = db.TransactionManager.StartTransaction())
{
Entity ent = (Entity)trans.GetObject(ObjID, OpenMode.ForRead, false);
DwgReader reader = new DwgReader();
DwgWriter writer = new DwgWriter();
Geometry geometry = (Geometry)reader.ReadGeometry(ent);
IPoint centroid = geometry.Centroid;
ed.WriteMessage("\nCentroid is " + centroid.ToString());
}//using
}//end try
catch (System.Exception ex)
{
ed.WriteMessage("Error Reading Centroid: " + ex.Message);
}
}// if
}
}
-
thanks pkohut for your help / code...
i've amended my code to suit your changes (see below), however, the code only seems to produce an accurate result if the polyline is drawn counterclockwise (see attached images, the polyline has been mirrod to get counterclockwise vs clockwise)
imports AADS = AutoDesk.AutoCAD.DatabaseServices
imports AAG = Autodesk.AutoCAD.Geometry
Public Shared Function GetCentroid(ByVal polyline As AADS.Polyline) As AAG.Point3d
Dim ReferencePoint As AAG.Point2d
Dim RunningX As Double
Dim RunningY As Double
Dim second_factor As Double
Dim Point1 As AAG.Point2d
Dim Point2 As AAG.Point2d
For Counter As Integer = 0 To polyline.NumberOfVertices - 2
Point1 = polyline.GetPoint2dAt(Counter)
Point2 = polyline.GetPoint2dAt(Counter + 1)
If Counter = 0 Then ReferencePoint = Point1
second_factor = (Point1.X - ReferencePoint.X) * (Point2.Y - ReferencePoint.Y) - (Point2.X - ReferencePoint.X) * (Point1.Y - ReferencePoint.Y)
RunningX += ((Point1.X - ReferencePoint.X) + (Point2.X - ReferencePoint.X)) * second_factor
RunningY += ((Point1.Y - ReferencePoint.Y) + (Point2.Y - ReferencePoint.Y)) * second_factor
Next
Dim X, Y As Double
X = RunningX / (6 * polyline.Area) + ReferencePoint.X
Y = RunningY / (6 * polyline.Area) + ReferencePoint.Y
Return New AAG.Point3d(X, Y, polyline.Elevation)
End Function
-
The easiest way to fix this is to keep a running total of second_factor during the loop. Then
once out of the loop if the summation is negative then you'll need to multiply your X and Y
calculations with -1.0. FYI, the area of the polyline is the abs(summation) * 0.5 (straight
line segments only).
Cathy mentions possibly missing the final segment. You'll need to check if the first and last
vertex are the same or not. You already know the polyline is closed, so the final segment
may be interpolated across the final vertex to the first vertex, and you would miss that.
Paul
-
Need to correct a sentence in my last post from:
Then
once out of the loop if the summation is negative then you'll need to multiply your X and Y
calculations with -1.0.
to:
Then once out of the loop if the summation is negative then you'll need to multiply
RunningX and RunningY with -1.0. Here's an example:
dim dVal as double
dVal = 1.0
if summation < 0.0 then dVal = -1.0
X = (RunningX * dVal) / (6 * polyline.Area) + ReferencePoint.X
Y = (RunningY * dVal) / (6 * polyline.Area) + ReferencePoint.Y
Paul
-
awesome... thanks pkohut, that fixed it up
cheers, Mark
-
Hi,
It was quite difficult for me (I'm a complete beginer with .NET), but it was a fine exercice.
Here's a translation (C#) of the LISP routines I gave here (http://www.theswamp.org/index.php?topic=18725.0).
All comments are welcome.
It works with arc polylines.
EDIT: added a testing command and a LISP function.
The "pline_centroid" command draws a point on the selected polyline centroid.
(pline_centroid (car (entsel))) returns the WCS coordinates of the centroid.
using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
namespace PolylineCentroid
{
// Main class
public class Centroid
{
public Point3d GetCentroid(Polyline pl)
{
Point2d p0 = pl.GetPoint2dAt(0);
Point2d cen = new Point2d(0.0, 0.0);
double area = 0.0;
double bulge = pl.GetBulgeAt(0);
int last = pl.NumberOfVertices - 1;
double tmpArea;
Point2d tmpPoint;
if (bulge != 0.0)
{
double[] datas = getArcGeom(pl, bulge, 0, 1);
area = datas[0];
cen = new Point2d(datas[1], datas[2]) * datas[0];
}
for (int i = 1; i < last; i++)
{
tmpArea = triangleAlgebricArea(p0, pl.GetPoint2dAt(i), pl.GetPoint2dAt(i + 1));
tmpPoint = triangleCentroid(p0, pl.GetPoint2dAt(i), pl.GetPoint2dAt(i + 1));
cen += (tmpPoint * tmpArea).GetAsVector();
area += tmpArea;
bulge = pl.GetBulgeAt(i);
if (bulge != 0.0)
{
double[] datas = getArcGeom(pl, bulge, i, i + 1);
area += datas[0];
cen += new Vector2d(datas[1], datas[2]) * datas[0];
}
}
bulge = pl.GetBulgeAt(last);
if (bulge != 0.0)
{
double[] datas = getArcGeom(pl, bulge, last, 0);
area += datas[0];
cen += new Vector2d(datas[1], datas[2]) * datas[0];
}
cen = cen.DivideBy(area);
Point3d result = new Point3d(cen.X, cen.Y, pl.Elevation);
return result.TransformBy(Matrix3d.PlaneToWorld(pl.Normal));
}
public double[] GetArcGeom(Polyline pl, double bulge, int index1, int index2)
{
CircularArc2d arc = (pl.GetArcSegment2dAt(index1));
double arcRadius = arc.Radius;
Point2d arcCenter = arc.Center;
double arcAngle = 4.0 * Math.Atan(bulge);
double tmpArea = arcAlgebricArea(arcRadius, arcAngle);
Point2d tmpPoint = arcCentroid(pl.GetPoint2dAt(index1), pl.GetPoint2dAt(index2), arcCenter, tmpArea);
return new double[3] { tmpArea, tmpPoint.X, tmpPoint.Y };
}
public Point2d TriangleCentroid(Point2d p0, Point2d p1, Point2d p2)
{
return (p0 + p1.GetAsVector() + p2.GetAsVector()) / 3.0;
}
public double TriangleAlgebricArea(Point2d p0, Point2d p1, Point2d p2)
{
return (((p1.X - p0.X) * (p2.Y - p0.Y)) - ((p2.X - p0.X) * (p1.Y - p0.Y))) / 2.0;
}
public Point2d ArcCentroid(Point2d start, Point2d end, Point2d cen, double tmpArea)
{
double chord = start.GetDistanceTo(end);
double angle = angleFromTo(start, end);
return polar2d(cen, angle - (Math.PI / 2.0), (chord * chord * chord) / (12.0 * tmpArea));
}
public double ArcAlgebricArea(double rad, double ang)
{
return rad * rad * (ang - Math.Sin(ang)) / 2.0;
}
public double AngleFromTo(Point2d p1, Point2d p2)
{
return (p2 - p1).Angle;
}
public Point2d Polar2d(Point2d org, double angle, double distance)
{
return new Point2d(org.X + distance, org.Y).RotateBy(angle, org);
}
}
// Testing command
public class MyClass
{
[CommandMethod("pline_centroid")]
public void centroid()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptEntityOptions opts = new PromptEntityOptions("\nSelect a polyline: ");
opts.AllowNone = true;
opts.AllowObjectOnLockedLayer = true;
opts.SetRejectMessage("\nEntité non valide.");
opts.AddAllowedClass(typeof(Polyline), false);
PromptEntityResult pline = ed.GetEntity(opts);
if (pline.Status == PromptStatus.OK)
{
ObjectId ObjID = pline.ObjectId;
try
{
using (Transaction trans = db.TransactionManager.StartTransaction())
{
Polyline ent = (Polyline)trans.GetObject(ObjID, OpenMode.ForRead, false);
Centroid c = new Centroid();
Point3d cen = c.GetCentroid(ent);
DBPoint pt = new DBPoint(cen);
BlockTable bTable = (BlockTable)trans.GetObject(db.BlockTableId, OpenMode.ForRead);
BlockTableRecord mSpace = (BlockTableRecord)trans.GetObject(bTable[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
mSpace.AppendEntity(pt);
trans.AddNewlyCreatedDBObject(pt, true);
trans.Commit();
}
}
catch (System.Exception ex)
{
ed.WriteMessage("Error: " + ex.Message);
}
}
}
}
// LISP function
public class LispCentoid
{
[LispFunction("pline_centroid")]
public static object centroid(ResultBuffer buff)
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
if (buff == null)
{
ed.WriteMessage("Error: none argument.\n");
return false;
}
TypedValue[] args = buff.AsArray();
if (args[0].TypeCode != (int)LispDataType.ObjectId)
{
ed.WriteMessage("Error: incorrect argument type: lentityp {0}.\n", args[0].Value);
return false;
}
ObjectId objID = (ObjectId)args[0].Value;
try
{
using (Transaction trans = db.TransactionManager.StartTransaction())
{
Entity ent = (Entity)trans.GetObject(objID, OpenMode.ForRead, false);
string entType = ent.GetType().ToString().Substring(34);
if (entType == "Polyline")
{
Polyline pl = (Polyline)trans.GetObject(objID, OpenMode.ForRead, false);
Centroid c = new Centroid();
return c.GetCentroid(pl);
}
else
{
if (entType.Contains("DB")) entType = entType.Substring(2);
ed.WriteMessage("Error: incorrect entity type: " + entType + "\n");
return null;
}
}
}
catch (System.Exception ex)
{
ed.WriteMessage("\nError: " + ex.Message + "\n");
return false;
}
}
}
}
-
I edited the Centroid class code so that some methods, as angleFromTo, polar2d, triangleAlgebricArea, triangleCentroid and so on, which may be usefull are more easily readable.
-
Excellent gile!
-
Thanks Daniel..
-
nice one, thanks gile
-
Ya Gile looks good. Since your a self admitted beginner at .Net
may I suggest making all those "int" doubles into doubles
instead of letting the compiler upcast them for you.
return (p0 + p1.GetAsVector() + p2.GetAsVector()) / 3
to
return (p0 + p1.GetAsVector() + p2.GetAsVector()) / 3.0
if(bulge != 0)
to
if(bulge != 0.0)
The compiler will happily up cast for you, but that isn't something
you should let it do. Make your intent known to the compiler
and it will help you when you make a mistake. It also helps
you and others with a visual clue about your intent.
Later,
Paul
-
Thanks Paul,
It makes sense, I edit the code according to your suggestion.
-
In a similar fashion to the Polyline to Region process mentioned earlier, here an alternate method of retrieving centroids. Sure it’s cheating and ridiculously heavy handed, but isn’t that truly the American way. Just kidding. :-)
Granted, it is probably even slower still than the Poly/Region method – it does, however, allow for the inclusion of “Curved” Polys, Splines, and Regions.
[CommandMethod("EntCen")]
static public void centroid()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptEntityOptions opts = new PromptEntityOptions("\nSelect a planar entity: ");
opts.AllowNone = true;
opts.AllowObjectOnLockedLayer = true;
opts.SetRejectMessage("\nEntity not valid.");
opts.AddAllowedClass(typeof(Polyline), false);
opts.AddAllowedClass(typeof(Polyline2d), false);
opts.AddAllowedClass(typeof(Region), false);
opts.AddAllowedClass(typeof(Spline), false);
PromptEntityResult per = ed.GetEntity(opts);
if (per.Status == PromptStatus.OK)
{
ObjectId ObjID = per.ObjectId;
try
{
using (Transaction trans = db.TransactionManager.StartTransaction())
{
Entity ent = (Entity)trans.GetObject(ObjID, OpenMode.ForRead, false);
CentroidViaSol c = new CentroidViaSol(ent);
if (c.Success)
{
Point3d cen = c.DerivedCentroid;
DBPoint pt = new DBPoint(cen);
BlockTable bTable = (BlockTable)trans.GetObject(db.BlockTableId, OpenMode.ForRead);
BlockTableRecord cSpace = (BlockTableRecord)trans.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
cSpace.AppendEntity(pt);
trans.AddNewlyCreatedDBObject(pt, true);
trans.Commit();
}
}
}
catch (System.Exception ex)
{
ed.WriteMessage("Error: " + ex.Message);
}
}
}
// New Class //////////////
class CentroidViaSol
{
//Fields
public Boolean Success;
private Region m_region;
private Solid3d m_derivedSolid;
private Vector3d m_normal;
private Point3d m_centroid;
private DBObjectCollection m_entCol;
//Constructor ///////////
public CentroidViaSol(Entity PlanarEnt)
{
m_entCol = new DBObjectCollection();
string entType = PlanarEnt.GetType().ToString();
switch (entType)
{
case "Autodesk.AutoCAD.DatabaseServices.Polyline":
Polyline poly = PlanarEnt as Polyline;
if (poly.Closed)
{
m_entCol.Add(poly);
CreateRegion();
}
break;
case "Autodesk.AutoCAD.DatabaseServices.Polyline2d":
Polyline2d poly2d = PlanarEnt as Polyline2d;
if (poly2d.Closed)
{
m_entCol.Add(poly2d);
CreateRegion();
}
break;
case "Autodesk.AutoCAD.DatabaseServices.Spline":
Spline entSpline = PlanarEnt as Spline;
if (entSpline.IsPlanar && entSpline.Closed)
{
m_entCol.Add(entSpline);
CreateRegion();
}
break;
case "Autodesk.AutoCAD.DatabaseServices.Region":
m_region = (Region)PlanarEnt;
Success = true;
break;
}
if (!m_region.IsNull)
{
m_normal = m_region.Normal;
m_derivedSolid = new Solid3d();
m_derivedSolid.Extrude(m_region, 2.0, 0.0);
m_centroid = m_derivedSolid.MassProperties.Centroid;
m_centroid = m_centroid - m_normal;
}
}
//Properties //////////
public Point3d DerivedCentroid
{
get { return m_centroid; }
}
//Internal ////////////
private void CreateRegion()
{
try
{
m_region = (Region)(Region.CreateFromCurves(m_entCol)[0]);
Success = true;
}
catch
{
Success = false;
}
}
}
-
or.....
http://www.theswamp.org/index.php?topic=9351.msg120423#msg120423
-
In a similar fashion to the Polyline to Region process mentioned earlier, here an alternate method of retrieving centroids. Sure it’s cheating and ridiculously heavy handed, but isn’t that truly the American way. Just kidding. :-)
Granted, it is probably slower still than the Poly/Region method – it does, however, allow for the inclusion of “Curved” Polys, Splines, and Regions.
if (poly.Closed)
{
poly.Explode(m_entCol);
CreateRegion();
}
}
Why you need the explode method?
-
Why you need the explode method?
I admit I did not confirm that it was actually needed but the docs seemed to imply that it was.
And, I should also admit that despite the aforementioned "Heavyhandedness", I think the methodology interesting. Your link demos the more conventional method of dealing with regions, splines and splined polys.
-
Why you need the explode method?
I admit I did not confirm that it was actually needed but the docs seemed to imply that it was.
And, I should also admit that despite the aforementioned "Heavyhandedness", I think the methodology interesting. Your link demos the more conventional method of dealing with regions, splines and splined polys.
Sean,
If you can, and have the ObjectARX refererence help (part of the SDK) have a look at AcDbRegion::createFromCurves appears that they change it?.... what version of autocad you are using?
Below it is a quote from the a2009 ARX SDK docs....
Description
This static member function creates a set of AcDbRegion objects from the closed loops represented by the curves contained in the curveSegments array. The newly created region objects are returned in the regions array.
The curveSegments array must contain only pointers to AcDbLine, AcDbArc, AcDbEllipse, AcDbCircle, AcDbSpline, AcDb3dPolyline, or AcDbPolyline objects.
-
Sean,
If you can, and have the ObjectARX refererence help (part of the SDK) have a look at AcDbRegion::createFromCurves appears that they change it?.... what version of autocad you are using?
Cool! It appears that the Autocad 2009: Managed Class Reference Guide may have a misprint. I'll eliminate that explode call. It's not much by my "alternate method" needs every bit of optimization it can get. :wink:
-
Sean,
If you can, and have the ObjectARX refererence help (part of the SDK) have a look at AcDbRegion::createFromCurves appears that they change it?.... what version of autocad you are using?
Cool! It appears that the Autocad 2009: Managed Class Reference Guide may have a misprint. I'll eliminate that explode call. It's not much by my "alternate method" needs every bit of optimization it can get. :wink:
Good.
You can port the arx function (per the link I posted) to C# - and avoid all the hassle :)
-
I am indeed looking into it. It would appear, and correct me again if I’m wrong, that there is no direct way to get at a region’s mass props via the Managed API. The link you posted shows that it’s all accessible via a port. Thanks for the pointer.
-
I am indeed looking into it. It would appear, and correct me again if I’m wrong, that there is no direct way to get at a region’s mass props via the Managed API. The link you posted shows that it’s all accessible via a port. Thanks for the pointer.
a quick review to the docs they have it in:
Solid3dMassProperties Properties
-
Hmm, I can see how to use that with a solid but couldn’t see any hook into that with a region.
Neither can I avoid an exception with the example below:
Brep brp = new Brep(m_region);
foreach (Autodesk.AutoCAD.BoundaryRepresentation.Face fce in brp.Faces)
{
double ar = fce.GetArea(); //works okay
double pl = fce.GetPerimeterLength(); //works okay
MassProperties mp = fce.GetMassProperties(); //Exception
}
-
Hmm, I can see how to use that with a solid but couldn’t see any hook into that with a region.
Neither can I avoid an exception with the example below:
Brep brp = new Brep(m_region);
foreach (Autodesk.AutoCAD.BoundaryRepresentation.Face fce in brp.Faces)
{
double ar = fce.GetArea(); //works okay
double pl = fce.GetPerimeterLength(); //works okay
MassProperties mp = fce.GetMassProperties(); //Exception
}
Do you need the Faces ?
- Can you cast As Entity the Region and pass that to the Brep(Entity); ?
Sorry, do not have my tools or code here at the office....
-
Do you need the Faces ?
- Can you cast As Entity the Region and pass that to the Brep(Entity); ?
Sorry, do not have my tools or code here at the office....
No need to apologize. Exploring the various possibilities is kind entertaining. I know, I need a life. :|
I’ve tried various levels of Breps. I’ve also alternated using the region as entity and as region for the Brep constructor. The best I was able to do was avoid the exception during the MassProperties assignment, but everything set at 0. Actually, the RadiiOfGyration had “NaN” values – whatever that means.
I’ll keep looking. Something seems likely – given that the info is available via both COM and ARX.