8

有没有人有一个计算终点的好算法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

4 回答 4

11

有谁知道 e.Graphics.DrawArc 是如何实现的?

Graphics.DrawArc调用GdipDrawArcIgdiplus.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
于 2011-03-29T19:14:59.833 回答
2
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)。

于 2011-03-28T16:50:12.060 回答
1

这有帮助吗:
ArcSegment 的数学

于 2011-03-29T20:37:57.453 回答
1

“BlueRaja - Danny Pflughoeft”的答案是正确的,但是......它围绕半径点,必须使用PointF而不是Point

PointF 半径 = new 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;
    }
}
于 2021-04-14T15:07:08.977 回答