Getting End Point in ArcSegment with Start X/Y and

2019-06-17 02:51发布

问题:

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 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?

回答1:

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


回答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)

So, your coordinate is (xEnd, yEnd).



回答3:

Is this of help:
The Mathematics of ArcSegment