Title: Draw a rounded polygon in C#
This post uses techniques described in my post Connect two line segments with a circular arc in C# to draw a rounded polygon. The earlier post uses mathematics to find a circular arc to join two line segments. This post adapts that method to connect a series of segments that make up a polygon.
Arc Changes
This example makes a couple of changes to the previous example's circular arc code. First, it moves the earlier arc calculation code into a static Arcs class. The new example calls that class's methods to find the arcs that it needs to draw a rounded polygon.
The new example also modifies several key methods so they return a Boolean value indicating whether they succeeded. The previous example's versions of the FindArcWithRadius, FindArcFromSegments, and FindIntersection methods threw exceptions if they could not find arcs or intersections. The new example uses the methods' Boolean return values to ignore cases where the method cannot find the appropriate arcs.
For example, when you're drawing a polygon, you might move the mouse back to the polygon's previous point. The methods cannot find an intersection that involves a line segment with the same start and end point. The new example simply ignores that zerolength segment.
Note that while you are drawing the polygon, the program should not draw a closed polygon. Instead it should draw an open curve as shown in the picture on the right.
RoundedPolyline
The key to the program is the following RoundedPolyline method. This method takes as inputs a list of points that define the polygon (or open curve) and returns a GraphicsPath object that uses line segments and circular arcs to draw the rounded polygon.
// Convert an array of points into a GraphicsPath
// that connects the points with segments joined
// by circular arcs.
private GraphicsPath RoundedPolyline(
List<PointF> point_list,
int radius, bool is_closed)
{
// Remove adjacent duplicates from the list.
point_list = RemoveDuplicates(point_list);
int num_points = point_list.Count;
if (num_points < 2) return null;
// Convert into an array.
PointF[] points = point_list.ToArray();
// segments[i] is the segment from points[i] to points[i + 1];
SegmentInfo[] segments = new SegmentInfo[num_points];
// Initially the segments are the polygon's sides.
for (int i = 0; i < num_points; i++)
{
int j = (i + 1) % num_points;
segments[i] = new SegmentInfo(points[i], points[j]);
}
// arcs[i] is the arc at points[i].
ArcInfo[] arcs = new ArcInfo[num_points];
// Get arc and segment info between the points.
for (int i = 0; i < num_points; i++)
{
// Find the arc at points[i].
int j = i  1;
if (j < 0) j += num_points;
PointF s1p1 = points[j];
PointF s1p2 = points[i];
PointF s2p1 = points[(i + 1) % num_points];
PointF s2p2 = points[i];
RectangleF rect;
float start_angle, sweep_angle;
PointF s1_far, s1_close, s2_far, s2_close;
// Find the arc.
if (Arcs.FindArcWithRadius(s1p1, s1p2, s2p1, s2p2, radius,
out rect, out start_angle, out sweep_angle,
out s1_far, out s1_close, out s2_far, out s2_close))
{
// Save the arc info.
arcs[i] = new ArcInfo(rect, start_angle, sweep_angle);
// Update the adjacent segment infos.
j = i  1;
if (j < 0) j += num_points;
segments[j].EndPoint = s1_close;
segments[i].StartPoint = s2_close;
}
}
// If the path should not be closed,
// reset the first segment's start point
// and the secondtolast segment's end point.
if (!is_closed)
{
segments[0].StartPoint = points[0];
segments[num_points  2].EndPoint =
points[num_points  1];
}
// Create the GraphicsPath.
GraphicsPath path = new GraphicsPath();
// Add the middle segments and arcs.
for (int i = 0; i < num_points  1; i++)
{
// Add the arc at points[i].
if (is_closed  i > 0)
{
path.AddArc(arcs[i].Rect,
arcs[i].StartAngle, arcs[i].SweepAngle);
}
// Add the segment between points[i] and points[i + 1];
path.AddLine(segments[i].StartPoint, segments[i].EndPoint);
}
// If the path should be closed, add the final arc and segment.
if (is_closed)
{
// Add the final arc.
path.AddArc(arcs[num_points  1].Rect,
arcs[num_points  1].StartAngle,
arcs[num_points  1].SweepAngle);
// Add the final segment;
path.AddLine(
segments[num_points  1].StartPoint,
segments[num_points  1].EndPoint);
// Close the path.
path.CloseFigure();
}
return path;
}
The method first uses the following RemoveDuplicates helper method to make a copy of the list of points that contains no adjacent duplicate points. (Because the methods that make circular arcs cannot make an arc with a zerolength segment.)
// Copy a list of points into a new list
// with no adjacent duplicates.
private List RemoveDuplicates(List original_list)
{
// Make the result list.
List new_list = new List();
// Keep track of the last item we added.
// Initially compare the first item with the last one.
int num_items = original_list.Count;
T last_item = original_list[num_items  1];
// Loop through the items.
foreach (T item in original_list)
{
// If this is not the same as the previous item, add it.
if (!item.Equals(last_item))
{
new_list.Add(item);
last_item = item;
}
}
return new_list;
}
This is a generic method, so it can remove adjacent duplicates from a list of any kind of object. The method takes as a parameter a list of generic objects with type T and returns a similar list. It first creates the new list and saves a reference to the last item in the list in variable last_item. It then loops through the original list. If an item is not the same as the previously added item stored in last_item, the code adds it to the new list.
After it removes any duplicate points, the the RoundedPolygon method returns if the list contains fewer than two points. If the list contains at least two points, then the code converts it into an array.
Next the method creates an array of SegmentInfo objects named segments. The SegmentInfo class simply contains StartPoint and EndPoint fields. It's a straightforward class so I won't show it here. Download the example to see the details.
The code then loops through the points and creates a SegmentInfo object connecting each point to the one that follow it. For example, segments[i] represents a segment that connects points[i] and points[i + 1]. (Wrapping around to the beginning to connect the last point to the first.)
Now the method creates an array of ArcInfo objects named arcs. The ArcInfo class simply holds a RectangleF to define and arc's ellipse, its start angle, and its sweep angle. Those are the values that we will later need to pass into GraphicsPath methods to define an arc. Like the SegmentInfo class, this one is straightforward so I won't show it here. Download the example to see it.
The arcs[i] entry will hold the arc at point i in the polygon. To define the arcs, and to update the segments so they end where the arcs begin, the program loops over the indices of the points that define the polygon.
For each point the code finds the end points of the polygon's edges that start and end at this point. For example, suppose we are considering point i and that point is is not at the beginning or end of the array of points. Then the edge leading into that point is points[i  1] > points[i] and the edge leading out of that point is points[i] > points[i + 1].
After it finds the points that define the segments adjacent to this point, the method calls the FindArcWithRadius method to find the arc connecting the two segments. If the arc exists, the code saves its information in the corresponding arcs entry. It also updates the end points of the segments adjacent to the arc so they end and begin where the arc begins and ends respectively.
After finding the arcs and updating the segments, the method checks whether the arc should be closed. If the rounded polygon should be closed, then the segments and arcs arrays are finished.
If the rounded polygon should not be closed, then the code resets the starting end point for the first segment so it starts at the first input point. It also updates the secondtolast segment so it ends at the last input point.
The picture on the right shows where the program makes those adjustments. The first point is marked with a red dot. The red line shows where the first segment was updated so it starts at that point.
The last point is marked with a blue dot. The blue line shows where the secondtolast segment was updated so it ends at that point.
After it adjusts the segments if necessary, the method is ready to draw the rounded polygon. It first creates a Graphics method object to hold the rounded polygon. It then loops through the segments and arcs adding them to the path. If the path should be closed or the looping variable i is greater than zero, then the loop adds the corresponding arc to the path. That lets it skip first arc if the path should be open. After adding the arc, the code adds the following segment to the path.
The loop stops before it adds the final arc and segment. If the rounded polygon should be closed, then the code draws them. The code also closes the path's figure so it knows that the path should be closed.
Finally the method returns the GraphicsPath.
Paint
Whenever you move the mouse, click the mouse, or change the radius text box, the program refreshes its PictureBox and the following Paint event handler redraws the rounded polygon.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(picCanvas.BackColor);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (Drawing && (Points.Count >= 2))
{
GraphicsPath path =
RoundedPolyline(Points, Radius, false);
if (path != null)
{
using (Pen pen = new Pen(Color.Red, 3))
{
e.Graphics.DrawPath(pen, path);
}
}
}
else if (!Drawing && (Points.Count >= 3))
{
GraphicsPath path =
RoundedPolyline(Points, Radius, true);
if (path != null)
{
e.Graphics.FillPath(Brushes.LightGreen, path);
using (Pen pen = new Pen(Color.Green, 3))
{
e.Graphics.DrawPath(pen, path);
}
}
}
}
This code first clears the PictureBox with its background color and prepares to draw smoothly.
It then checks the Drawing variable to see if you are currently drawing a rounded polygon. If you are drawing and have already defined at least two points, then the code should draw an open curve. In that case the program calls RoundedPolyline passing false into its is_closed parameter. If the returned path is not null, the program draws it with a thick red pen.
If you are not drawing and you have previously defined at least three points, then a rounded polygon is defined so the program should draw it as a closed curve. In that case the program calls RoundedPolyline passing true into its is_closed parameter. If the returned path is not null, the program fills it in light green and outlines it with a thick green pen.
Conclusion
The program works pretty well, although there are still cases where you can make it produce strange results. If the rounded polygon has two points that are too close together for the given radius value, then the arcs at those points may extend beyond their corresponding segments and stick out as shown in the picture on the right. You can avoid that by ensuring that the points are not too close together for the given radius.
Download the example to experiment with it and to see additional details.
