Getting End Point in ArcSegment with Start X/Y and Start+Sweep Angles
Does anyone have a good algorithm for calculating the end point of ArcSegment
? This is not a circular arc - it's an elliptical one.
For example, I have these initial values:
- Start Point X = 0.251
- Start Point Y = 0.928
- Width Radius = 0.436
- Height Radius = 0.593
- Start Angle = 169.51
- Sweep Angle = 123.78
I know the location that my arc should end up at is right around X=0.92 and Y=0.33 (through another program), but I need to do this in an ArcSegment
with specifying the end point. I开发者_Python百科 just need to know how to calculate the end point so it would look like this:
<ArcSegment Size="0.436,0.593" Point="0.92,0.33" IsLargeArc="False" SweepDirection="Clockwise" />
Does anyone know of a good way to calculate this? (I don't suppose it matters that this is WPF or any other language as the math should be the same).
Here is an image. All values are known in it, except for end point (the orange point).
EDIT:
I've found that there is a routine called DrawArc
with an overload in .NET GDI+ that pretty much does what I need (more on the "pretty much" in a sec).
To simplify viewing it, take the following as an example:
Public Sub MyDrawArc(e As PaintEventArgs)
Dim blackPen As New Pen(Color.Black, 2)
Dim x As Single = 0.0F
Dim y As Single = 0.0F
Dim width As Single = 100.0F
Dim height As Single = 200.0F
Dim startAngle As Single = 180.0F
Dim sweepAngle As Single = 135.0F
e.Graphics.DrawArc(blackPen, x, y, width, height, startAngle, sweepAngle)
Dim redPen As New Pen(Color.Red, 2)
e.Graphics.DrawLine(redPen, New Point(0, 55), New Point(95, 55))
End Sub
Private Sub ImageBox_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles ImageBox.Paint
MyDrawArc(e)
End Sub
This routine squarely puts the end point at X=95, Y=55
. Other routines mentioned for circular ellipses would result in X=85, Y=29
. If there was a way to 1) Not have to draw anything and 2) have e.Graphics.DrawArc
return the end-point coordinates, this is what I would need.
So now the question gains some clarity - does anyone know how e.Graphics.DrawArc
is implemented?
Does anyone know how e.Graphics.DrawArc is implemented?
Graphics.DrawArc
calls the native function GdipDrawArcI
in gdiplus.dll. This function calls the arc2polybezier
function in the same dll. It appears to use a bezier curve to approximate an elliptical arc. In order to get the exact same end-point you're looking for, we'd have to reverse-engineer that function and figure out exactly how it works.
Fortunately, the good people at Wine have already done that for us.
Here is the arc2polybezier method, roughly translated from C to C# (note that because this was translated from Wine, this code is licensed under LGPL):
internal class GdiPlus
{
public const int MAX_ARC_PTS = 13;
public static int arc2polybezier(Point[] points, double x1, double y1, double x2, double y2,
double startAngle, double sweepAngle)
{
int i;
double end_angle, start_angle, endAngle;
endAngle = startAngle + sweepAngle;
unstretch_angle(ref startAngle, x2/2.0, y2/2.0);
unstretch_angle(ref endAngle, x2/2.0, y2/2.0);
/* start_angle and end_angle are the iterative variables */
start_angle = startAngle;
for(i = 0; i < MAX_ARC_PTS - 1; i += 3)
{
/* check if we've overshot the end angle */
if(sweepAngle > 0.0)
{
if(start_angle >= endAngle) break;
end_angle = Math.Min(start_angle + Math.PI/2, endAngle);
}
else
{
if(start_angle <= endAngle) break;
end_angle = Math.Max(start_angle - Math.PI/2, endAngle);
}
if(points != null)
{
Point[] returnedPoints = add_arc_part(x1, y1, x2, y2, start_angle, end_angle, i == 0);
//add_arc_part returns a Point[] of size 4
for(int j = 0; j < 4; j++)
points[i + j] = returnedPoints[j];
}
start_angle += Math.PI/2*(sweepAngle < 0.0 ? -1.0 : 1.0);
}
if(i == 0)
return 0;
return i + 1;
}
public static void unstretch_angle(ref double angle, double rad_x, double rad_y)
{
angle = deg2rad(angle);
if(Math.Abs(Math.Cos(angle)) < 0.00001 || Math.Abs(Math.Sin(angle)) < 0.00001)
return;
double stretched = Math.Atan2(Math.Sin(angle)/Math.Abs(rad_y), Math.Cos(angle)/Math.Abs(rad_x));
int revs_off = (int)Math.Round(angle/(2.0*Math.PI), MidpointRounding.AwayFromZero) -
(int)Math.Round(stretched/(2.0*Math.PI), MidpointRounding.AwayFromZero);
stretched += revs_off*Math.PI*2.0;
angle = stretched;
}
public static double deg2rad(double degrees)
{
return Math.PI*degrees/180.0;
}
private static Point[] add_arc_part(double x1, double y1, double x2, double y2,
double start, double end, bool write_first)
{
double center_x,
center_y,
rad_x,
rad_y,
cos_start,
cos_end,
sin_start,
sin_end,
a,
half;
int i;
rad_x = x2/2.0;
rad_y = y2/2.0;
center_x = x1 + rad_x;
center_y = y1 + rad_y;
cos_start = Math.Cos(start);
cos_end = Math.Cos(end);
sin_start = Math.Sin(start);
sin_end = Math.Sin(end);
half = (end - start)/2.0;
a = 4.0/3.0*(1 - Math.Cos(half))/Math.Sin(half);
Point[] pt = new Point[4];
if(write_first)
{
pt[0].X = cos_start;
pt[0].Y = sin_start;
}
pt[1].X = cos_start - a*sin_start;
pt[1].Y = sin_start + a*cos_start;
pt[3].X = cos_end;
pt[3].Y = sin_end;
pt[2].X = cos_end + a*sin_end;
pt[2].Y = sin_end - a*cos_end;
/* expand the points back from the unit circle to the ellipse */
for(i = (write_first ? 0 : 1); i < 4; i ++)
{
pt[i].X = pt[i].X*rad_x + center_x;
pt[i].Y = pt[i].Y*rad_y + center_y;
}
return pt;
}
}
Using this code as a guide, along with a bit of math, I wrote this endpoint calculator class (not LGPL):
using System;
using System.Windows;
internal class DrawArcEndPointCalculator
{
public Point GetFinalPoint(Point startPoint, double width, double height,
double startAngle, double sweepAngle)
{
Point radius = new Point(width / 2.0, height / 2.0);
double endAngle = startAngle + sweepAngle;
int sweepDirection = (sweepAngle < 0 ? -1 : 1);
//Adjust the angles for the radius width/height
startAngle = UnstretchAngle(startAngle, radius);
endAngle = UnstretchAngle(endAngle, radius);
//Determine how many times to add the sweep-angle to the start-angle
int angleMultiplier = (int)Math.Floor(2*sweepDirection*(endAngle - startAngle)/Math.PI) + 1;
angleMultiplier = Math.Min(angleMultiplier, 4);
//Calculate the final resulting angle after sweeping
double calculatedEndAngle = startAngle + angleMultiplier*Math.PI/2*sweepDirection;
calculatedEndAngle = sweepDirection*Math.Min(sweepDirection * calculatedEndAngle, sweepDirection * endAngle);
//Calculate the final point
return new Point
{
X = (Math.Cos(calculatedEndAngle) + 1)*radius.X + startPoint.X,
Y = (Math.Sin(calculatedEndAngle) + 1)*radius.Y + startPoint.Y,
};
}
private double UnstretchAngle(double angle, Point radius)
{
double radians = Math.PI * angle / 180.0;
if(Math.Abs(Math.Cos(radians)) < 0.00001 || Math.Abs(Math.Sin(radians)) < 0.00001)
return radians;
double stretchedAngle = Math.Atan2(Math.Sin(radians) / Math.Abs(radius.Y), Math.Cos(radians) / Math.Abs(radius.X));
int rotationOffset = (int)Math.Round(radians / (2.0 * Math.PI), MidpointRounding.AwayFromZero) -
(int)Math.Round(stretchedAngle / (2.0 * Math.PI), MidpointRounding.AwayFromZero);
return stretchedAngle + rotationOffset * Math.PI * 2.0;
}
}
Here are some examples. Note that the first example you gave is incorrect - for those initial values, DrawArc()
will have an endpoint of (0.58, 0.97), not (0.92, 0.33).
Point startPoint = new Point(0, 0);
double width = 100;
double height = 200;
double startAngle = 180;
double sweepAngle = 135;
DrawArcEndPointCalculator _endPointCalculator = new DrawArcEndPointCalculator();
Point lastPoint = _endPointCalculator.GetFinalPoint(startPoint, width, height, startAngle, sweepAngle);
Console.WriteLine("X = {0}, Y = {1}", lastPoint.X, lastPoint.Y);
//Output: X = 94.7213595499958, Y = 55.2786404500042
startPoint = new Point(0.251, 0.928);
width = 0.436;
height = 0.593;
startAngle = 169.51;
sweepAngle = 123.78;
_endPointCalculator.GetFinalPoint(startPoint, width, height, startAngle, sweepAngle);
//Returns X = 0.579143189905416, Y = 0.968627455618129
Point startPoint = new Point(0, 0);
double width = 20;
double height = 30;
double startAngle = 90;
double sweepAngle = 90;
_endPointCalculator.GetFinalPoint(startPoint, width, height, startAngle, sweepAngle);
//Returns X = 0, Y = 15
1) Given this:
xStart = .25
yStart = .92
startAngle = 169.51
sweepAngle = 123.78
Rx = .436 // this is radius width
Ry = .593 // this is radius height
2) Calculations:
centerX = xStart - Rx * cos(startAngle)
centerY = yStart - Ry * sin(startAngle)
endAngle = startAngle + sweepAngle
xEnd = centerX + Rx * cos(endAngle)
yEnd = centerY + Ry * sin(endAngle)
So, your coordinate is (xEnd, yEnd).
Is this of help:
The Mathematics of ArcSegment
the answer of "BlueRaja - Danny Pflughoeft" is correct but ... it rounds the radius point, a PointF has to be used instead a Point:
PointF radius = new PointF((float)width / 2, (float)height / 2);
I've extended a bit the class in order to have starting points as well, and another signature per method:
public static class ChartHelper
{
public static PointF GetStartingPoint(float x, float y, double width, double height, double startAngle, double sweepAngle)
{
return GetStartingPoint(new PointF(x, y), width, height, startAngle, sweepAngle);
}
public static PointF GetStartingPoint(PointF startPoint, double width, double height, double startAngle, double sweepAngle)
{
PointF radius = new PointF((float)width / 2, (float)height / 2);
//Adjust the angles for the radius width/height
startAngle = UnstretchAngle(startAngle, radius);
//Calculate the starting point
return new PointF
{
X = (float)(Math.Cos(startAngle) + 1) * radius.X + startPoint.X,
Y = (float)(Math.Sin(startAngle) + 1) * radius.Y + startPoint.Y,
};
}
public static PointF GetFinalPoint(float x, float y, double width, double height, double startAngle, double sweepAngle)
{
return GetFinalPoint(new PointF(x, y), width, height, startAngle, sweepAngle);
}
public static PointF GetFinalPoint(PointF startPoint, double width, double height, double startAngle, double sweepAngle)
{
PointF radius = new PointF((float)width / 2, (float)height / 2);
double endAngle = startAngle + sweepAngle;
double sweepDirection = (sweepAngle < 0 ? -1 : 1);
//Adjust the angles for the radius width/height
startAngle = UnstretchAngle(startAngle, radius);
endAngle = UnstretchAngle(endAngle, radius);
//Determine how many times to add the sweep-angle to the start-angle
double angleMultiplier = (double)Math.Floor(2 * sweepDirection * (endAngle - startAngle) / Math.PI) + 1;
angleMultiplier = Math.Min(angleMultiplier, 4);
//Calculate the final resulting angle after sweeping
double calculatedEndAngle = startAngle + angleMultiplier * Math.PI / 2 * sweepDirection;
calculatedEndAngle = sweepDirection * Math.Min(sweepDirection * calculatedEndAngle, sweepDirection * endAngle);
//Calculate the final point
return new PointF
{
X = (float)(Math.Cos(calculatedEndAngle) + 1) * radius.X + startPoint.X,
Y = (float)(Math.Sin(calculatedEndAngle) + 1) * radius.Y + startPoint.Y,
};
}
private static double UnstretchAngle(double angle, PointF radius)
{
double radians = Math.PI * angle / 180.0;
if (Math.Abs(Math.Cos(radians)) < 0.00001 || Math.Abs(Math.Sin(radians)) < 0.00001)
return radians;
double stretchedAngle = Math.Atan2(Math.Sin(radians) / Math.Abs(radius.Y), Math.Cos(radians) / Math.Abs(radius.X));
double rotationOffset = (double)Math.Round(radians / (2.0 * Math.PI), MidpointRounding.AwayFromZero) -
(double)Math.Round(stretchedAngle / (2.0 * Math.PI), MidpointRounding.AwayFromZero);
return stretchedAngle + rotationOffset * Math.PI * 2.0;
}
}
精彩评论