#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 { /// /// Helper class for Geometry paths. /// static class GeometryHelper { #if WPF || NETFX_CORE /// /// Appends a Bézier segment from a curve. /// 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 /// /// Creates a path geometry from a polygon. /// 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 /// /// Creates a path geometry from a polygon. /// 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 /// /// Creates the arc segment from parameters of the GDI+ DrawArc function. /// 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 /// /// Creates between 1 and 5 Béziers curves from parameters specified like in GDI+. /// public static List BezierCurveFromArc(double x, double y, double width, double height, double startAngle, double sweepAngle, PathStart pathStart, ref XMatrix matrix) { List points = new List(); // 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; } /// /// 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. /// 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; } /// /// Appends a Bézier curve for an arc within a full quadrant. /// static void AppendPartialArcQuadrant(List 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β))); } } /// /// Creates between 1 and 5 Béziers curves from parameters specified like in WPF. /// public static List 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; } /// /// Gets a value indicating whether radius large enough compared to the chord length. /// /// (1/2 chord length)squared /// Squared fuzz. /// The radius to accept (or not). 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 } }