ASCU_ALL/PrintPDF/PdfSharp/Drawing/GeometryHelper.cs

874 lines
36 KiB
C#
Raw Normal View History

2020-09-04 12:49:15 +05:00
#region PDFsharp - A .NET library for processing PDF
//
// Authors:
// Stefan Lange
//
// Copyright (c) 2005-2017 empira Software GmbH, Cologne Area (Germany)
//
// http://www.pdfsharp.com
// http://sourceforge.net/projects/pdfsharp
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#endregion
using System;
using System.Diagnostics;
using System.Collections.Generic;
#if GDI
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
#endif
#if WPF
using System.Windows.Media;
using SysPoint = System.Windows.Point;
using SysSize = System.Windows.Size;
#endif
#if NETFX_CORE || UWP
using Windows.UI.Xaml.Media;
using SysPoint = Windows.Foundation.Point;
using SysSize = Windows.Foundation.Size;
#endif
using PdfSharp.Internal;
// ReSharper disable RedundantNameQualifier
// ReSharper disable CompareOfFloatsByEqualityOperator
namespace PdfSharp.Drawing
{
/// <summary>
/// Helper class for Geometry paths.
/// </summary>
static class GeometryHelper
{
#if WPF || NETFX_CORE
/// <summary>
/// Appends a Bézier segment from a curve.
/// </summary>
public static BezierSegment CreateCurveSegment(XPoint pt0, XPoint pt1, XPoint pt2, XPoint pt3, double tension3)
{
#if !SILVERLIGHT && !NETFX_CORE
return new BezierSegment(
new SysPoint(pt1.X + tension3 * (pt2.X - pt0.X), pt1.Y + tension3 * (pt2.Y - pt0.Y)),
new SysPoint(pt2.X - tension3 * (pt3.X - pt1.X), pt2.Y - tension3 * (pt3.Y - pt1.Y)),
new SysPoint(pt2.X, pt2.Y), true);
#else
BezierSegment bezierSegment = new BezierSegment();
bezierSegment.Point1 = new SysPoint(pt1.X + tension3 * (pt2.X - pt0.X), pt1.Y + tension3 * (pt2.Y - pt0.Y));
bezierSegment.Point2 = new SysPoint(pt2.X - tension3 * (pt3.X - pt1.X), pt2.Y - tension3 * (pt3.Y - pt1.Y));
bezierSegment.Point3 = new SysPoint(pt2.X, pt2.Y);
return bezierSegment;
#endif
}
#endif
#if WPF || NETFX_CORE
/// <summary>
/// Creates a path geometry from a polygon.
/// </summary>
public static PathGeometry CreatePolygonGeometry(SysPoint[] points, XFillMode fillMode, bool closed)
{
PolyLineSegment seg = new PolyLineSegment();
int count = points.Length;
// For correct drawing the start point of the segment must not be the same as the first point.
for (int idx = 1; idx < count; idx++)
seg.Points.Add(new SysPoint(points[idx].X, points[idx].Y));
#if !SILVERLIGHT && !NETFX_CORE
seg.IsStroked = true;
#endif
PathFigure fig = new PathFigure();
fig.StartPoint = new SysPoint(points[0].X, points[0].Y);
fig.Segments.Add(seg);
fig.IsClosed = closed;
PathGeometry geo = new PathGeometry();
geo.FillRule = fillMode == XFillMode.Winding ? FillRule.Nonzero : FillRule.EvenOdd;
geo.Figures.Add(fig);
return geo;
}
#endif
#if WPF || NETFX_CORE
/// <summary>
/// Creates a path geometry from a polygon.
/// </summary>
public static PolyLineSegment CreatePolyLineSegment(SysPoint[] points, XFillMode fillMode, bool closed)
{
PolyLineSegment seg = new PolyLineSegment();
int count = points.Length;
// For correct drawing the start point of the segment must not be the same as the first point.
for (int idx = 1; idx < count; idx++)
seg.Points.Add(new SysPoint(points[idx].X, points[idx].Y));
#if !SILVERLIGHT && !NETFX_CORE
seg.IsStroked = true;
#endif
return seg;
}
#endif
#if WPF || NETFX_CORE
/// <summary>
/// Creates the arc segment from parameters of the GDI+ DrawArc function.
/// </summary>
public static ArcSegment CreateArcSegment(double x, double y, double width, double height, double startAngle,
double sweepAngle, out SysPoint startPoint)
{
// Normalize the angles.
double α = startAngle;
if (α < 0)
α = α + (1 + Math.Floor((Math.Abs(α) / 360))) * 360;
else if (α > 360)
α = α - Math.Floor(α / 360) * 360;
Debug.Assert(α >= 0 && α <= 360);
if (Math.Abs(sweepAngle) >= 360)
sweepAngle = Math.Sign(sweepAngle) * 360;
double β = startAngle + sweepAngle;
if (β < 0)
β = β + (1 + Math.Floor((Math.Abs(β) / 360))) * 360;
else if (β > 360)
β = β - Math.Floor(β / 360) * 360;
if (α == 0 && β < 0)
α = 360;
else if (α == 360 && β > 0)
α = 0;
// Scanling factor.
double δx = width / 2;
double δy = height / 2;
// Center of ellipse.
double x0 = x + δx;
double y0 = y + δy;
double cosα, cosβ, sinα, sinβ;
if (width == height)
{
// Circular arc needs no correction.
α = α * Calc.Deg2Rad;
β = β * Calc.Deg2Rad;
}
else
{
// Elliptic arc needs the angles to be adjusted such that the scaling transformation is compensated.
α = α * Calc.Deg2Rad;
sinα = Math.Sin(α);
if (Math.Abs(sinα) > 1E-10)
{
if (α < Math.PI)
α = Math.PI / 2 - Math.Atan(δy * Math.Cos(α) / (δx * sinα));
else
α = 3 * Math.PI / 2 - Math.Atan(δy * Math.Cos(α) / (δx * sinα));
}
//α = Calc.πHalf - Math.Atan(δy * Math.Cos(α) / (δx * sinα));
β = β * Calc.Deg2Rad;
sinβ = Math.Sin(β);
if (Math.Abs(sinβ) > 1E-10)
{
if (β < Math.PI)
β = Math.PI / 2 - Math.Atan(δy * Math.Cos(β) / (δx * sinβ));
else
β = 3 * Math.PI / 2 - Math.Atan(δy * Math.Cos(β) / (δx * sinβ));
}
//β = Calc.πHalf - Math.Atan(δy * Math.Cos(β) / (δx * sinβ));
}
sinα = Math.Sin(α);
cosα = Math.Cos(α);
sinβ = Math.Sin(β);
cosβ = Math.Cos(β);
startPoint = new SysPoint(x0 + δx * cosα, y0 + δy * sinα);
SysPoint destPoint = new SysPoint(x0 + δx * cosβ, y0 + δy * sinβ);
SysSize size = new SysSize(δx, δy);
bool isLargeArc = Math.Abs(sweepAngle) >= 180;
SweepDirection sweepDirection = sweepAngle > 0 ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
#if !SILVERLIGHT && !NETFX_CORE
bool isStroked = true;
ArcSegment seg = new ArcSegment(destPoint, size, 0, isLargeArc, sweepDirection, isStroked);
#else
ArcSegment seg = new ArcSegment();
seg.Point = destPoint;
seg.Size = size;
seg.RotationAngle = 0;
seg.IsLargeArc = isLargeArc;
seg.SweepDirection = sweepDirection;
// isStroked does not exist in Silverlight 3
#endif
return seg;
}
#endif
/// <summary>
/// Creates between 1 and 5 Béziers curves from parameters specified like in GDI+.
/// </summary>
public static List<XPoint> BezierCurveFromArc(double x, double y, double width, double height, double startAngle, double sweepAngle,
PathStart pathStart, ref XMatrix matrix)
{
List<XPoint> points = new List<XPoint>();
// Normalize the angles.
double α = startAngle;
if (α < 0)
α = α + (1 + Math.Floor((Math.Abs(α) / 360))) * 360;
else if (α > 360)
α = α - Math.Floor(α / 360) * 360;
Debug.Assert(α >= 0 && α <= 360);
double β = sweepAngle;
if (β < -360)
β = -360;
else if (β > 360)
β = 360;
if (α == 0 && β < 0)
α = 360;
else if (α == 360 && β > 0)
α = 0;
// Is it possible that the arc is small starts and ends in same quadrant?
bool smallAngle = Math.Abs(β) <= 90;
β = α + β;
if (β < 0)
β = β + (1 + Math.Floor((Math.Abs(β) / 360))) * 360;
bool clockwise = sweepAngle > 0;
int startQuadrant = Quadrant(α, true, clockwise);
int endQuadrant = Quadrant(β, false, clockwise);
if (startQuadrant == endQuadrant && smallAngle)
AppendPartialArcQuadrant(points, x, y, width, height, α, β, pathStart, matrix);
else
{
int currentQuadrant = startQuadrant;
bool firstLoop = true;
do
{
if (currentQuadrant == startQuadrant && firstLoop)
{
double ξ = currentQuadrant * 90 + (clockwise ? 90 : 0);
AppendPartialArcQuadrant(points, x, y, width, height, α, ξ, pathStart, matrix);
}
else if (currentQuadrant == endQuadrant)
{
double ξ = currentQuadrant * 90 + (clockwise ? 0 : 90);
AppendPartialArcQuadrant(points, x, y, width, height, ξ, β, PathStart.Ignore1st, matrix);
}
else
{
double ξ1 = currentQuadrant * 90 + (clockwise ? 0 : 90);
double ξ2 = currentQuadrant * 90 + (clockwise ? 90 : 0);
AppendPartialArcQuadrant(points, x, y, width, height, ξ1, ξ2, PathStart.Ignore1st, matrix);
}
// Don't stop immediately if arc is greater than 270 degrees.
if (currentQuadrant == endQuadrant && smallAngle)
break;
smallAngle = true;
if (clockwise)
currentQuadrant = currentQuadrant == 3 ? 0 : currentQuadrant + 1;
else
currentQuadrant = currentQuadrant == 0 ? 3 : currentQuadrant - 1;
firstLoop = false;
} while (true);
}
return points;
}
/// <summary>
/// Calculates the quadrant (0 through 3) of the specified angle. If the angle lies on an edge
/// (0, 90, 180, etc.) the result depends on the details how the angle is used.
/// </summary>
static int Quadrant(double φ, bool start, bool clockwise)
{
Debug.Assert(φ >= 0);
if (φ > 360)
φ = φ - Math.Floor(φ / 360) * 360;
int quadrant = (int)(φ / 90);
if (quadrant * 90 == φ)
{
if ((start && !clockwise) || (!start && clockwise))
quadrant = quadrant == 0 ? 3 : quadrant - 1;
}
else
quadrant = clockwise ? ((int)Math.Floor(φ / 90)) % 4 : (int)Math.Floor(φ / 90);
return quadrant;
}
/// <summary>
/// Appends a Bézier curve for an arc within a full quadrant.
/// </summary>
static void AppendPartialArcQuadrant(List<XPoint> points, double x, double y, double width, double height, double α, double β, PathStart pathStart, XMatrix matrix)
{
Debug.Assert(α >= 0 && α <= 360);
Debug.Assert(β >= 0);
if (β > 360)
β = β - Math.Floor(β / 360) * 360;
Debug.Assert(Math.Abs(α - β) <= 90);
// Scanling factor.
double δx = width / 2;
double δy = height / 2;
// Center of ellipse.
double x0 = x + δx;
double y0 = y + δy;
// We have the following quarters:
// |
// 2 | 3
// ----+-----
// 1 | 0
// |
// If the angles lie in quarter 2 or 3, their values are subtracted by 180 and the
// resulting curve is reflected at the center. This algorithm works as expected (simply tried out).
// There may be a mathematically more elegant solution...
bool reflect = false;
if (α >= 180 && β >= 180)
{
α -= 180;
β -= 180;
reflect = true;
}
double cosα, cosβ, sinα, sinβ;
if (width == height)
{
// Circular arc needs no correction.
α = α * Calc.Deg2Rad;
β = β * Calc.Deg2Rad;
}
else
{
// Elliptic arc needs the angles to be adjusted such that the scaling transformation is compensated.
α = α * Calc.Deg2Rad;
sinα = Math.Sin(α);
if (Math.Abs(sinα) > 1E-10)
α = Math.PI / 2 - Math.Atan(δy * Math.Cos(α) / (δx * sinα));
β = β * Calc.Deg2Rad;
sinβ = Math.Sin(β);
if (Math.Abs(sinβ) > 1E-10)
β = Math.PI / 2 - Math.Atan(δy * Math.Cos(β) / (δx * sinβ));
}
double κ = 4 * (1 - Math.Cos((α - β) / 2)) / (3 * Math.Sin((β - α) / 2));
sinα = Math.Sin(α);
cosα = Math.Cos(α);
sinβ = Math.Sin(β);
cosβ = Math.Cos(β);
//XPoint pt1, pt2, pt3;
if (!reflect)
{
// Calculation for quarter 0 and 1.
switch (pathStart)
{
case PathStart.MoveTo1st:
points.Add(matrix.Transform(new XPoint(x0 + δx * cosα, y0 + δy * sinα)));
break;
case PathStart.LineTo1st:
points.Add(matrix.Transform(new XPoint(x0 + δx * cosα, y0 + δy * sinα)));
break;
case PathStart.Ignore1st:
break;
}
points.Add(matrix.Transform(new XPoint(x0 + δx * (cosα - κ * sinα), y0 + δy * (sinα + κ * cosα))));
points.Add(matrix.Transform(new XPoint(x0 + δx * (cosβ + κ * sinβ), y0 + δy * (sinβ - κ * cosβ))));
points.Add(matrix.Transform(new XPoint(x0 + δx * cosβ, y0 + δy * sinβ)));
}
else
{
// Calculation for quarter 2 and 3.
switch (pathStart)
{
case PathStart.MoveTo1st:
points.Add(matrix.Transform(new XPoint(x0 - δx * cosα, y0 - δy * sinα)));
break;
case PathStart.LineTo1st:
points.Add(matrix.Transform(new XPoint(x0 - δx * cosα, y0 - δy * sinα)));
break;
case PathStart.Ignore1st:
break;
}
points.Add(matrix.Transform(new XPoint(x0 - δx * (cosα - κ * sinα), y0 - δy * (sinα + κ * cosα))));
points.Add(matrix.Transform(new XPoint(x0 - δx * (cosβ + κ * sinβ), y0 - δy * (sinβ - κ * cosβ))));
points.Add(matrix.Transform(new XPoint(x0 - δx * cosβ, y0 - δy * sinβ)));
}
}
/// <summary>
/// Creates between 1 and 5 Béziers curves from parameters specified like in WPF.
/// </summary>
public static List<XPoint> BezierCurveFromArc(XPoint point1, XPoint point2, XSize size,
double rotationAngle, bool isLargeArc, bool clockwise, PathStart pathStart)
{
// See also http://www.charlespetzold.com/blog/blog.xml from January 2, 2008:
// http://www.charlespetzold.com/blog/2008/01/Mathematics-of-ArcSegment.html
double δx = size.Width;
double δy = size.Height;
Debug.Assert(δx * δy > 0);
double factor = δy / δx;
bool isCounterclockwise = !clockwise;
// Adjust for different radii and rotation angle.
XMatrix matrix = new XMatrix();
matrix.RotateAppend(-rotationAngle);
matrix.ScaleAppend(δy / δx, 1);
XPoint pt1 = matrix.Transform(point1);
XPoint pt2 = matrix.Transform(point2);
// Get info about chord that connects both points.
XPoint midPoint = new XPoint((pt1.X + pt2.X) / 2, (pt1.Y + pt2.Y) / 2);
XVector vect = pt2 - pt1;
double halfChord = vect.Length / 2;
// Get vector from chord to center.
XVector vectRotated;
// (comparing two Booleans here!)
if (isLargeArc == isCounterclockwise)
vectRotated = new XVector(-vect.Y, vect.X);
else
vectRotated = new XVector(vect.Y, -vect.X);
vectRotated.Normalize();
// Distance from chord to center.
double centerDistance = Math.Sqrt(δy * δy - halfChord * halfChord);
if (double.IsNaN(centerDistance))
centerDistance = 0;
// Calculate center point.
XPoint center = midPoint + centerDistance * vectRotated;
// Get angles from center to the two points.
double α = Math.Atan2(pt1.Y - center.Y, pt1.X - center.X);
double β = Math.Atan2(pt2.Y - center.Y, pt2.X - center.X);
// (another comparison of two Booleans!)
if (isLargeArc == (Math.Abs(β - α) < Math.PI))
{
if (α < β)
α += 2 * Math.PI;
else
β += 2 * Math.PI;
}
// Invert matrix for final point calculation.
matrix.Invert();
double sweepAngle = β - α;
// Let the algorithm of GDI+ DrawArc to Bézier curves do the rest of the job
return BezierCurveFromArc(center.X - δx * factor, center.Y - δy, 2 * δx * factor, 2 * δy,
α / Calc.Deg2Rad, sweepAngle / Calc.Deg2Rad, pathStart, ref matrix);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// The code below comes from WPF source code, because I was not able to convert an arc
// to a series of Bezier curves exactly the way WPF renders the arc. I tested my own code
// with the MinBar Test Suite from QualityLogic and could not find out why it does not match.
// My Bezier curves came very close to the arc, but in some cases they do simply not match.
// So I gave up and use the WPF code.
#if WPF || NETFX_CORE
// ReSharper disable InconsistentNaming
const double FUZZ = 1e-6; // Relative 0
// ReSharper restore InconsistentNaming
//+-------------------------------------------------------------------------------------------------
//
// Function: GetArcAngle
//
// Synopsis: Get the number of Bezier arcs, and sine & cosine of each
//
// Notes: This is a private utility used by ArcToBezier
// We break the arc into pieces so that no piece will span more than 90 degrees.
// The input points are on the unit circle
//
//-------------------------------------------------------------------------------------------------
public static void
GetArcAngle(
XPoint startPoint, // Start point
XPoint endPoint, // End point
bool isLargeArc, // Choose the larger of the 2 possible arcs if TRUE
//SweepDirection sweepDirection, // Direction n which to sweep the arc.
bool isClockwise,
out double cosArcAngle, // Cosine of a the sweep angle of one arc piece
out double sinArcAngle, // Sine of a the sweep angle of one arc piece
out int pieces) // Out: The number of pieces
{
double angle;
// The points are on the unit circle, so:
cosArcAngle = startPoint.X * endPoint.X + startPoint.Y * endPoint.Y;
sinArcAngle = startPoint.X * endPoint.Y - startPoint.Y * endPoint.X;
if (cosArcAngle >= 0)
{
if (isLargeArc)
{
// The angle is between 270 and 360 degrees, so
pieces = 4;
}
else
{
// The angle is between 0 and 90 degrees, so
pieces = 1;
return; // We already have the cosine and sine of the angle
}
}
else
{
if (isLargeArc)
{
// The angle is between 180 and 270 degrees, so
pieces = 3;
}
else
{
// The angle is between 90 and 180 degrees, so
pieces = 2;
}
}
// We have to chop the arc into the computed number of pieces. For cPieces=2 and 4 we could
// have uses the half-angle trig formulas, but for pieces=3 it requires solving a cubic
// equation; the performance difference is not worth the extra code, so we'll get the angle,
// divide it, and get its sine and cosine.
Debug.Assert(pieces > 0);
angle = Math.Atan2(sinArcAngle, cosArcAngle);
if (isClockwise)
{
if (angle < 0)
angle += Math.PI * 2;
}
else
{
if (angle > 0)
angle -= Math.PI * 2;
}
angle /= pieces;
cosArcAngle = Math.Cos(angle);
sinArcAngle = Math.Sin(angle);
}
/******************************************************************************\
*
* Function Description:
*
* Get the distance from a circular arc's endpoints to the control points of the
* Bezier arc that approximates it, as a fraction of the arc's radius.
*
* Since the result is relative to the arc's radius, it depends strictly on the
* arc's angle. The arc is assumed to be of 90 degrees of less, so the angle is
* determined by the cosine of that angle, which is derived from rDot = the dot
* product of two radius vectors. We need the Bezier curve that agrees with
* the arc's points and tangents at the ends and midpoint. Here we compute the
* distance from the curve's endpoints to its control points.
*
* Since we are looking for the relative distance, we can work on the unit
* circle. Place the center of the circle at the origin, and put the X axis as
* the bisector between the 2 vectors. Let a be the angle between the vectors.
* Then the X coordinates of the 1st & last points are cos(a/2). Let x be the X
* coordinate of the 2nd & 3rd points. At t=1/2 we have a point at (1,0).
* But the terms of the polynomial there are all equal:
*
* (1-t)^3 = t*(1-t)^2 = 2^2*(1-t) = t^3 = 1/8,
*
* so from the Bezier formula there we have:
*
* 1 = (1/8) * (cos(a/2) + 3x + 3x + cos(a/2)),
* hence
* x = (1 - cos(a/2)) / 3
*
* The X difference between that and the 1st point is:
*
* DX = x - cos(a/2) = 4(1 - cos(a/2)) / 3.
*
* But DX = distance / sin(a/2), hence the distance is
*
* dist = (4/3)*(1 - cos(a/2)) / sin(a/2).
*
* Created: 5/29/2001 [....]
*
/*****************************************************************************/
public static double
GetBezierDistance( // Return the distance as a fraction of the radius
double dot, // In: The dot product of the two radius vectors
double radius) // In: The radius of the arc's circle (optional=1)
{
double radSquared = radius * radius; // Squared radius
Debug.Assert(dot >= -radSquared * .1); // angle < 90 degrees
Debug.Assert(dot <= radSquared * 1.1); // as dot product of 2 radius vectors
double dist = 0; // Acceptable fallback value
/* Rather than the angle a, we are given rDot = R^2 * cos(a), so we
multiply top and bottom by R:
dist = (4/3)*(R - Rcos(a/2)) / Rsin(a/2)
and use some trig:
__________
cos(a/2) = \/1 + cos(a) / 2
________________ __________
R*cos(a/2) = \/R^2 + R^2 cos(a) / 2 = \/R^2 + rDot / 2 */
double cos = (radSquared + dot) / 2; // =(R*cos(a))^2
if (cos < 0)
return dist;
// __________________
// R*sin(a/2) = \/R^2 - R^2 cos(a/2)
double sin = radSquared - cos; // =(R*sin(a))^2
if (sin <= 0)
return dist;
sin = Math.Sqrt(sin); // = R*cos(a)
cos = Math.Sqrt(cos); // = R*sin(a)
dist = 4 * (radius - cos) / 3;
if (dist <= sin * FUZZ)
dist = 0;
else
dist = 4 * (radius - cos) / sin / 3;
return dist;
}
//+-------------------------------------------------------------------------------------------------
//
// Function: ArcToBezier
//
// Synopsis: Compute the Bezier approximation of an arc
//
// Notes: This utilitycomputes the Bezier approximation for an elliptical arc as it is defined
// in the SVG arc spec. The ellipse from which the arc is carved is axis-aligned in its
// own coordinates, and defined there by its x and y radii. The rotation angle defines
// how the ellipse's axes are rotated relative to our x axis. The start and end points
// define one of 4 possible arcs; the sweep and large-arc flags determine which one of
// these arcs will be chosen. See SVG spec for details.
//
// Returning pieces = 0 indicates a line instead of an arc
// pieces = -1 indicates that the arc degenerates to a point
//
//--------------------------------------------------------------------------------------------------
public static PointCollection ArcToBezier(double xStart, double yStart, double xRadius, double yRadius, double rotationAngle,
bool isLargeArc, bool isClockwise, double xEnd, double yEnd, out int pieces)
{
double cosArcAngle, sinArcAngle, xCenter, yCenter, r, bezDist;
XVector vecToBez1, vecToBez2;
XMatrix matToEllipse;
double fuzz2 = FUZZ * FUZZ;
bool isZeroCenter = false;
pieces = -1;
// In the following, the line segment between between the arc's start and
// end points is referred to as "the chord".
// Transform 1: Shift the origin to the chord's midpoint
double x = (xEnd - xStart) / 2;
double y = (yEnd - yStart) / 2;
double halfChord2 = x * x + y * y; // (half chord length)^2
// Degenerate case: single point
if (halfChord2 < fuzz2)
{
// The chord degeneartes to a point, the arc will be ignored
return null;
}
// Degenerate case: straight line
if (!AcceptRadius(halfChord2, fuzz2, ref xRadius) || !AcceptRadius(halfChord2, fuzz2, ref yRadius))
{
// We have a zero radius, add a straight line segment instead of an arc
pieces = 0;
return null;
}
if (xRadius == 0 || yRadius == 0)
{
// We have a zero radius, add a straight line segment instead of an arc
pieces = 0;
return null;
}
// Transform 2: Rotate to the ellipse's coordinate system
rotationAngle = -rotationAngle * Calc.Deg2Rad;
double cos = Math.Cos(rotationAngle);
double sin = Math.Sin(rotationAngle);
r = x * cos - y * sin;
y = x * sin + y * cos;
x = r;
// Transform 3: Scale so that the ellipse will become a unit circle
x /= xRadius;
y /= yRadius;
// We get to the center of that circle along a verctor perpendicular to the chord
// from the origin, which is the chord's midpoint. By Pythagoras, the length of that
// vector is sqrt(1 - (half chord)^2).
halfChord2 = x * x + y * y; // now in the circle coordinates
if (halfChord2 > 1)
{
// The chord is longer than the circle's diameter; we scale the radii uniformly so
// that the chord will be a diameter. The center will then be the chord's midpoint,
// which is now the origin.
r = Math.Sqrt(halfChord2);
xRadius *= r;
yRadius *= r;
xCenter = yCenter = 0;
isZeroCenter = true;
// Adjust the unit-circle coordinates x and y
x /= r;
y /= r;
}
else
{
// The length of (-y,x) or (x,-y) is sqrt(rHalfChord2), and we want a vector
// of length sqrt(1 - rHalfChord2), so we'll multiply it by:
r = Math.Sqrt((1 - halfChord2) / halfChord2);
//if (isLargeArc != (eSweepDirection == SweepDirection.Clockwise))
if (isLargeArc != isClockwise)
// Going to the center from the origin=chord-midpoint
{
// in the direction of (-y, x)
xCenter = -r * y;
yCenter = r * x;
}
else
{
// in the direction of (y, -x)
xCenter = r * y;
yCenter = -r * x;
}
}
// Transformation 4: shift the origin to the center of the circle, which then becomes
// the unit circle. Since the chord's midpoint is the origin, the start point is (-x, -y)
// and the endpoint is (x, y).
XPoint ptStart = new XPoint(-x - xCenter, -y - yCenter);
XPoint ptEnd = new XPoint(x - xCenter, y - yCenter);
// Set up the matrix that will take us back to our coordinate system. This matrix is
// the inverse of the combination of transformation 1 thru 4.
matToEllipse = new XMatrix(cos * xRadius, -sin * xRadius,
sin * yRadius, cos * yRadius,
(xEnd + xStart) / 2, (yEnd + yStart) / 2);
if (!isZeroCenter)
{
// Prepend the translation that will take the origin to the circle's center
matToEllipse.OffsetX += (matToEllipse.M11 * xCenter + matToEllipse.M21 * yCenter);
matToEllipse.OffsetY += (matToEllipse.M12 * xCenter + matToEllipse.M22 * yCenter);
}
// Get the sine & cosine of the angle that will generate the arc pieces
GetArcAngle(ptStart, ptEnd, isLargeArc, isClockwise, out cosArcAngle, out sinArcAngle, out pieces);
// Get the vector to the first Bezier control point
bezDist = GetBezierDistance(cosArcAngle, 1);
//if (eSweepDirection == SweepDirection.Counterclockwise)
if (!isClockwise)
bezDist = -bezDist;
vecToBez1 = new XVector(-bezDist * ptStart.Y, bezDist * ptStart.X);
PointCollection result = new PointCollection();
// Add the arc pieces, except for the last
for (int idx = 1; idx < pieces; idx++)
{
// Get the arc piece's endpoint
XPoint ptPieceEnd = new XPoint(ptStart.X * cosArcAngle - ptStart.Y * sinArcAngle, ptStart.X * sinArcAngle + ptStart.Y * cosArcAngle);
vecToBez2 = new XVector(-bezDist * ptPieceEnd.Y, bezDist * ptPieceEnd.X);
result.Add(matToEllipse.Transform(ptStart + vecToBez1));
result.Add(matToEllipse.Transform(ptPieceEnd - vecToBez2));
result.Add(matToEllipse.Transform(ptPieceEnd));
// Move on to the next arc
ptStart = ptPieceEnd;
vecToBez1 = vecToBez2;
}
// Last arc - we know the endpoint
vecToBez2 = new XVector(-bezDist * ptEnd.Y, bezDist * ptEnd.X);
result.Add(matToEllipse.Transform(ptStart + vecToBez1));
result.Add(matToEllipse.Transform(ptEnd - vecToBez2));
result.Add(new XPoint(xEnd, yEnd));
return result;
}
/// <summary>
/// Gets a value indicating whether radius large enough compared to the chord length.
/// </summary>
/// <param name="halfChord2">(1/2 chord length)squared </param>
/// <param name="fuzz2">Squared fuzz.</param>
/// <param name="radius">The radius to accept (or not).</param>
static bool AcceptRadius(double halfChord2, double fuzz2, ref double radius)
{
Debug.Assert(halfChord2 >= fuzz2); // Otherewise we have no guarantee that the radius is not 0, and we need to divide by the radius
bool accept = radius * radius > halfChord2 * fuzz2;
if (accept)
{
if (radius < 0)
radius = 0;
}
return accept;
}
#endif
}
}