Dot-Net

使用起始 X/Y 和起始+掃角在 ArcSegment 中獲取端點

  • April 14, 2021

有沒有人有一個計算終點的好算法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是如何實現的嗎?

有誰知道 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

引用自:https://stackoverflow.com/questions/5441061