I'm looking for an algorithm that allows me to create rounded corners from a polygon.
I have an array of points that represents the polygon (outlined in red) and on output I want an array of points that represents the polygon with rounded corners (outlined in black).
I would also like to have a way to control the radius of each corner.
I tried to use Bézier curves and subdivision but it's not what I'm looking for. Bézier curves and subdivision are smoothing the polygon.
What I want is to only make the corners rounded.
Does somebody know any good algorithm for doing that?
I'm working with C# but the code has to be independent from any .NET libraries.
Polygons can have any number of sides and angles, but the sides can never be curved. The segments are called the sides of the polygons, and the points where the segments intersect are called vertices.
You are looking for an arc tangent to two connected line segments, of a given radius, given by some sequential array of points. The algorithm to find this arc is as follows:
For each segment, construct a normal vector.
If you are working in 2d, you can just subtract the two endpoints to get a tangent vector (X, Y). In that case, normal vectors will be plus or minus (-Y, X). Normalize the normal vector to length one. Finally, choose the direction with a positive dot product with the tangent vector of the next segment. (See update below).
If you are working in 3d not 2d, to get the normal, cross the tangent vectors of the two segments at the vertex you wish to round to get a perpendicular vector to the plane of the lines. If the perpendicular has length zero, the segments are parallel and no round can be required. Otherwise, normalize it, then cross the perpendicular with the tangent to get the normal.)
Using the normal vectors, offset each line segment towards the interior of the polygon by your desired radius. To offset a segment, offset its endpoints using the normal vector N you just computed, like so: P' = P + r * N (a linear combination).
Intersect the two offset lines to find the center. (This works because a radius vector of a circle is always perpendicular to its tangent.)
To find the point at which the circle intersects each segment, offset the circle center backwards to each original segment. These will be the endpoints of your arc.
Make sure the arc endpoints are inside each segment, otherwise you will be creating a self-intersecting polygon.
Create an arc through both endpoints with center & radius you determined.
I don't have any proper drafting software at hand, but this diagram sort of shows the idea:
At this point you will need either to introduce classes to represent a figure consisting of line and arc segments, or polygonize the arc to an appropriate accuracy and add all the segments to the polygon.
Update: I have updated the image, labeling the points P1, P2, and P3, and normal vectors Norm12 and Norm23. The normalized normals are unique only up to flipping direction, and you should choose the flips as follows:
The dot product of Norm12 with (P3 - P2) must be positive. If it is negative, multiple Norm12 by -1.0. If it is zero, the points are collinear and no rounded corner need be created. This is because you want to offset towards P3.
The dot product of Norm23 with (P1 - P2) must also be positive since you are offsetting towards P1.
0. You have a corner:
1. You know the coordinates of corner points, let it be P1, P2 and P:
2. Now you can get vectors from points and angle between vectors:
angle = atan(PY - P1Y, PX - P1X) - atan(PY - P2Y, PX - P2X)
3. Get the length of segment between angular point and the points of intersection with the circle.
segment = PC1 = PC2 = radius / |tan(angle / 2)|
4. Here you need to check the length of segment and the minimal length from PP1 and PP2:
Length of PP1:
PP1 = sqrt((PX - P1X)2 + (PY - P1Y)2)
Length of PP2:
PP2 = sqrt((PX - P2X)2 + (PY - P2Y)2)
If segment > PP1 or segment > PP2 then you need to decrease the radius:
min = Min(PP1, PP2) (for polygon is better to divide this value by 2) segment > min ? segment = min radius = segment * |tan(angle / 2)|
5. Get the length of PO:
PO = sqrt(radius2 + segment2)
6. Get the C1X and C1Y by the proportion between the coordinates of the vector, length of vector and the length of the segment:
Proportion:
(PX - C1X) / (PX - P1X) = PC1 / PP1
So:
C1X = PX - (PX - P1X) * PC1 / PP1
The same for C1Y:
C1Y = PY - (PY - P1Y) * PC1 / PP1
7. Get the C2X and C2Y by the same way:
C2X = PX - (PX - P2X) * PC2 / PP2 C2Y = PY - (PY - P2Y) * PC2 / PP2
8. Now you can use the addition of vectors PC1 and PC2 to find the centre of circle by the same way by proportion:
(PX - OX) / (PX - CX) = PO / PC (PY - OY) / (PY - CY) = PO / PC
Here:
CX = C1X + C2X - PX CY = C1Y + C2Y - PY PC = sqrt((PX - CX)2 + (PY - CY)2)
Let:
dx = PX - CX = PX * 2 - C1X - C2X dy = PY - CY = PY * 2 - C1Y - C2Y
So:
PC = sqrt(dx2 + dy2) OX = PX - dx * PO / PC OY = PY - dy * PO / PC
9. Here you can draw an arc. For this you need to get start angle and end angle of arc:
Found it here:
startAngle = atan((C1Y - OY) / (C1X - OX)) endAngle = atan((C2Y - OY) / (C2X - OX))
10. At last you need to get a sweep angle and make some checks for it:
sweepAngle = endAngle - startAngle
If sweepAngle < 0 then swap startAngle and endAngle, and invert sweepAngle:
sweepAngle < 0 ? sweepAngle = - sweepAngle startAngle = endAngle
Check if sweepAngle > 180 degrees:
sweepAngle > 180 ? sweepAngle = 180 - sweepAngle
11. And now you can draw a rounded corner:
private void DrawRoundedCorner(Graphics graphics, PointF angularPoint, PointF p1, PointF p2, float radius) { //Vector 1 double dx1 = angularPoint.X - p1.X; double dy1 = angularPoint.Y - p1.Y; //Vector 2 double dx2 = angularPoint.X - p2.X; double dy2 = angularPoint.Y - p2.Y; //Angle between vector 1 and vector 2 divided by 2 double angle = (Math.Atan2(dy1, dx1) - Math.Atan2(dy2, dx2)) / 2; // The length of segment between angular point and the // points of intersection with the circle of a given radius double tan = Math.Abs(Math.Tan(angle)); double segment = radius / tan; //Check the segment double length1 = GetLength(dx1, dy1); double length2 = GetLength(dx2, dy2); double length = Math.Min(length1, length2); if (segment > length) { segment = length; radius = (float)(length * tan); } // Points of intersection are calculated by the proportion between // the coordinates of the vector, length of vector and the length of the segment. var p1Cross = GetProportionPoint(angularPoint, segment, length1, dx1, dy1); var p2Cross = GetProportionPoint(angularPoint, segment, length2, dx2, dy2); // Calculation of the coordinates of the circle // center by the addition of angular vectors. double dx = angularPoint.X * 2 - p1Cross.X - p2Cross.X; double dy = angularPoint.Y * 2 - p1Cross.Y - p2Cross.Y; double L = GetLength(dx, dy); double d = GetLength(segment, radius); var circlePoint = GetProportionPoint(angularPoint, d, L, dx, dy); //StartAngle and EndAngle of arc var startAngle = Math.Atan2(p1Cross.Y - circlePoint.Y, p1Cross.X - circlePoint.X); var endAngle = Math.Atan2(p2Cross.Y - circlePoint.Y, p2Cross.X - circlePoint.X); //Sweep angle var sweepAngle = endAngle - startAngle; //Some additional checks if (sweepAngle < 0) { startAngle = endAngle; sweepAngle = -sweepAngle; } if (sweepAngle > Math.PI) sweepAngle = Math.PI - sweepAngle; //Draw result using graphics var pen = new Pen(Color.Black); graphics.Clear(Color.White); graphics.SmoothingMode = SmoothingMode.AntiAlias; graphics.DrawLine(pen, p1, p1Cross); graphics.DrawLine(pen, p2, p2Cross); var left = circlePoint.X - radius; var top = circlePoint.Y - radius; var diameter = 2 * radius; var degreeFactor = 180 / Math.PI; graphics.DrawArc(pen, left, top, diameter, diameter, (float)(startAngle * degreeFactor), (float)(sweepAngle * degreeFactor)); } private double GetLength(double dx, double dy) { return Math.Sqrt(dx * dx + dy * dy); } private PointF GetProportionPoint(PointF point, double segment, double length, double dx, double dy) { double factor = segment / length; return new PointF((float)(point.X - dx * factor), (float)(point.Y - dy * factor)); }
To get points of arc you can use this:
//One point for each degree. But in some cases it will be necessary // to use more points. Just change a degreeFactor. int pointsCount = (int)Math.Abs(sweepAngle * degreeFactor); int sign = Math.Sign(sweepAngle); PointF[] points = new PointF[pointsCount]; for (int i = 0; i < pointsCount; ++i) { var pointX = (float)(circlePoint.X + Math.Cos(startAngle + sign * (double)i / degreeFactor) * radius); var pointY = (float)(circlePoint.Y + Math.Sin(startAngle + sign * (double)i / degreeFactor) * radius); points[i] = new PointF(pointX, pointY); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With