Получение конечной точки в ArcSegment с помощью Start X/Y и Start+Sweep Angles
У кого-нибудь есть хороший алгоритм для расчета конечной точки ArcSegment
? Это не круговая дуга - это эллиптическая.
Например, у меня есть эти начальные значения:
- Начальная точка X = 0,251
- Начальная точка Y = 0,928
- Ширина радиуса = 0,436
- Радиус высоты = 0,593
- Начальный угол = 169,51
- Угол развертки = 123,78
Я знаю, что место, где должна находиться моя дуга, находится прямо вокруг X = 0,92 и Y = 0,33 (через другую программу), но мне нужно сделать это в ArcSegment
с указанием конечной точки. Мне просто нужно знать, как рассчитать конечную точку, чтобы она выглядела так:
<ArcSegment Size="0.436,0.593" Point="0.92,0.33" IsLargeArc="False" SweepDirection="Clockwise" />
Кто-нибудь знает хороший способ рассчитать это? (Я не думаю, что имеет значение, что это WPF или любой другой язык, так как математика должна быть такой же).
Вот изображение. В нем известны все значения, кроме конечной точки (оранжевой точки).
РЕДАКТИРОВАТЬ: я обнаружил, что есть рутина называется DrawArc
с перегрузкой в .NET GDI+, которая в значительной степени делает то, что мне нужно (подробнее о "в значительной степени" в секунду).
Чтобы упростить просмотр, возьмем в качестве примера:
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
Эта процедура прямо ставит конечную точку на X=95, Y=55
, Другие процедуры, упомянутые для круглых эллипсов, могут привести к X=85, Y=29
, Если бы был способ 1) Не надо ничего рисовать и 2) иметь e.Graphics.DrawArc
вернуть координаты конечной точки, это то, что мне нужно.
Так что теперь вопрос обретает некоторую ясность - кто-нибудь знает, как e.Graphics.DrawArc
реализовано?
4 ответа
Кто-нибудь знает, как реализован e.Graphics.DrawArc?
Graphics.DrawArc
вызывает нативную функцию GdipDrawArcI
в gdiplus.dll. Эта функция вызывает arc2polybezier
Функция в том же DLL. Похоже, что используется кривая Безье для аппроксимации эллиптической дуги. Чтобы получить ту же самую конечную точку, которую вы ищете, нам придется перепроектировать эту функцию и выяснить, как именно она работает.
К счастью, хорошие люди в Wine уже сделали это для нас.
Вот метод arc2polybezier, грубо переведенный с C на C# (обратите внимание, что, поскольку он был переведен с Wine, этот код лицензирован под 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;
}
}
Используя этот код в качестве руководства, а также немного математики, я написал этот класс калькулятора конечной точки (не 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;
}
}
Вот несколько примеров. Обратите внимание, что первый приведенный вами пример неверен - для этих начальных значений DrawArc()
будет иметь конечную точку (0,58, 0,97), а не (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)
Итак, ваша координата (xEnd, yEnd).
ответ "BlueRaja - Danny Pflughoeft" правильный, но ... он округляет точку радиуса, вместо Point следует использовать PointF :
Радиус PointF = новый PointF ((float)width / 2, (float)height / 2);
Я немного расширил класс, чтобы иметь отправные точки, и еще одну сигнатуру для каждого метода:
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;
}
}