【问题标题】:Getting End Point in ArcSegment with Start X/Y and Start+Sweep Angles使用起始 X/Y 和起始+扫角在 ArcSegment 中获取端点
【发布时间】:2011-07-23 09:36:59
【问题描述】:

有没有人有一个很好的算法来计算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 with an overload in .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返回终点坐标,这就是我所需要的。 p>

所以现在问题变得更清楚了 - 有人知道e.Graphics.DrawArc 是如何实现的吗?

【问题讨论】:

  • 你指的是StreamGeometryContext.ArcTo吗?因为我真的找不到扫描角...
  • @Markus Hütter:抱歉,我被告知初始值为ArcTo,但在 GDI+ 文档中查找该值和ArcAngle 时,看起来不像是究竟是什么。我已经对问题进行了重大修改以反映真实的问题。
  • @Stan 你能告诉我们起始角和扫角到底是什么吗?
  • @Markus Hütter:我已经更新了一张图片 - 希望这将有助于可视化我正在尝试做的事情,因为我拥有当前的值。
  • @Stan:计算终点的“其他程序”是什么?

标签: .net wpf geometry drawing geometric-arc


【解决方案1】:

“BlueRaja - Danny Pflughoeft”的答案是正确的,但是......它围绕半径点,必须使用 PointF 代替 Point: p>

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;
    }
}

【讨论】:

    【解决方案2】:

    这是有帮助的吗:
    The Mathematics of ArcSegment

    【讨论】:

      【解决方案3】:

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

      Graphics.DrawArc 调用 gdiplus.dll 中的原生函数 GdipDrawArcI。此函数调用同一 dll 中的 arc2polybezier 函数。它似乎使用贝塞尔曲线来近似椭圆弧。为了获得您正在寻找的精确相同的端点,我们必须对该函数进行逆向工程并弄清楚它是如何工作的。

      还好Wine的好人有already done that for us

      这里是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),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
      

      【讨论】:

      【解决方案4】:
      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)。

      【讨论】:

      • 感谢您尝试 Loki,不幸的是,这是圆弧的计算,而不是椭圆弧。结果是xEnd=0.851yEnd=0.267,这不是正确的坐标(如果Rx和Ry相同,就是这个计算)。
      • 那是椭圆弧的计算。您可以通过运行这个简单的程序来检查这一点,该程序会生成一个带有椭圆的位图,该椭圆是根据上述公式计算得出的:pastebin.com/kV0vWB5q。你用什么程序来计算预期位置?我认为这可能是错误的。
      • 它肯定会返回椭圆路径上的弧,但不幸的是它不是椭圆弧。请参阅上面的进一步编辑。
      猜你喜欢
      • 1970-01-01
      • 2019-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-02
      相关资源
      最近更新 更多