Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 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). image depicting arc


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?

like image 306
Stan Avatar asked Mar 26 '11 07:03

Stan


4 Answers

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
like image 160
BlueRaja - Danny Pflughoeft Avatar answered Nov 04 '22 01:11

BlueRaja - Danny Pflughoeft


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).

like image 20
Loki Kriasus Avatar answered Nov 04 '22 02:11

Loki Kriasus


Is this of help:
The Mathematics of ArcSegment

like image 40
publicgk Avatar answered Nov 04 '22 00:11

publicgk


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;
    }
}
like image 44
Angelo Cresta Avatar answered Nov 04 '22 00:11

Angelo Cresta