// Copyright © 2003-2004, Luc Maisonobe // 2015 - Alexey Rozanov - Adaptations for Avalonia and oval center computations // All rights reserved. // // Redistribution and use in source and binary forms, with // or without modification, are permitted provided that // the following conditions are met: // // Redistributions of source code must retain the // above copyright notice, this list of conditions and // the following disclaimer. // Redistributions in binary form must reproduce the // above copyright notice, this list of conditions and // the following disclaimer in the documentation // and/or other materials provided with the // distribution. // Neither the names of spaceroots.org, spaceroots.com // nor the names of their contributors may be used to // endorse or promote products derived from this // software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND // CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // C#/WPF/Avalonia adaptation by Alexey Rozanov , 2015. // I do not mind if anyone would find this adaptation useful, but // please retain the above disclaimer made by the original class // author Luc Maisonobe. He worked really hard on this subject, so // please respect him by at least keeping the above disclaimer intact // if you use his code. // // Commented out some unused values calculations. // These are not supposed to be removed from the source code, // as these may be helpful for debugging. // // Adapted from http://www.spaceroots.org/documents/ellipse/EllipticalArc.java using System; using Avalonia.Media; using Avalonia.Platform; namespace Avalonia.RenderHelpers { static class ArcToHelper { /// /// This class represents an elliptical arc on a 2D plane. /// /// This class is adapted for use with WPF StreamGeometryContext, and needs to be created explicitly /// for each particular arc. /// /// Some helpers /// /// It can handle ellipses which are not aligned with the x and y reference axes of the plane, /// as well as their parts. /// /// Another improvement is that this class can handle degenerated cases like for example very /// flat ellipses(semi-minor axis much smaller than semi-major axis) and drawing of very small /// parts of such ellipses at very high magnification scales.This imply monitoring the drawing /// approximation error for extremely small values.Such cases occur for example while drawing /// orbits of comets near the perihelion. /// /// When the arc does not cover the complete ellipse, the lines joining the center of the /// ellipse to the endpoints can optionally be included or not in the outline, hence allowing /// to use it for pie-charts rendering. If these lines are not included, the curve is not /// naturally closed. /// public sealed class EllipticalArc { private const double TwoPi = 2 * Math.PI; /// /// Coefficients for error estimation while using quadratic Bezier curves for approximation, /// 0 ≤ b/a ≤ 0.25 /// private static readonly double[][][] Coeffs2Low = { new[] { new[] {3.92478, -13.5822, -0.233377, 0.0128206}, new[] {-1.08814, 0.859987, 3.62265E-4, 2.29036E-4}, new[] {-0.942512, 0.390456, 0.0080909, 0.00723895}, new[] {-0.736228, 0.20998, 0.0129867, 0.0103456} }, new[] { new[] {-0.395018, 6.82464, 0.0995293, 0.0122198}, new[] {-0.545608, 0.0774863, 0.0267327, 0.0132482}, new[] {0.0534754, -0.0884167, 0.012595, 0.0343396}, new[] {0.209052, -0.0599987, -0.00723897, 0.00789976} } }; /// /// Coefficients for error estimation while using quadratic Bezier curves for approximation, /// 0.25 ≤ b/a ≤ 1 /// private static readonly double[][][] Coeffs2High = { new[] { new[] {0.0863805, -11.5595, -2.68765, 0.181224}, new[] {0.242856, -1.81073, 1.56876, 1.68544}, new[] {0.233337, -0.455621, 0.222856, 0.403469}, new[] {0.0612978, -0.104879, 0.0446799, 0.00867312} }, new[] { new[] {0.028973, 6.68407, 0.171472, 0.0211706}, new[] {0.0307674, -0.0517815, 0.0216803, -0.0749348}, new[] {-0.0471179, 0.1288, -0.0781702, 2.0}, new[] {-0.0309683, 0.0531557, -0.0227191, 0.0434511} } }; /// /// Safety factor to convert the "best" error approximation into a "max bound" error /// private static readonly double[] Safety2 = { 0.02, 2.83, 0.125, 0.01 }; /// /// Coefficients for error estimation while using cubic Bezier curves for approximation, /// 0.25 ≤ b/a ≤ 1 /// private static readonly double[][][] Coeffs3Low = { new[] { new[] {3.85268, -21.229, -0.330434, 0.0127842}, new[] {-1.61486, 0.706564, 0.225945, 0.263682}, new[] {-0.910164, 0.388383, 0.00551445, 0.00671814}, new[] {-0.630184, 0.192402, 0.0098871, 0.0102527} }, new[] { new[] {-0.162211, 9.94329, 0.13723, 0.0124084}, new[] {-0.253135, 0.00187735, 0.0230286, 0.01264}, new[] {-0.0695069, -0.0437594, 0.0120636, 0.0163087}, new[] {-0.0328856, -0.00926032, -0.00173573, 0.00527385} } }; /// /// Coefficients for error estimation while using cubic Bezier curves for approximation, /// 0.25 ≤ b/a ≤ 1 /// private static readonly double[][][] Coeffs3High = { new[] { new[] {0.0899116, -19.2349, -4.11711, 0.183362}, new[] {0.138148, -1.45804, 1.32044, 1.38474}, new[] {0.230903, -0.450262, 0.219963, 0.414038}, new[] {0.0590565, -0.101062, 0.0430592, 0.0204699} }, new[] { new[] {0.0164649, 9.89394, 0.0919496, 0.00760802}, new[] {0.0191603, -0.0322058, 0.0134667, -0.0825018}, new[] {0.0156192, -0.017535, 0.00326508, -0.228157}, new[] {-0.0236752, 0.0405821, -0.0173086, 0.176187} } }; /// /// Safety factor to convert the "best" error approximation into a "max bound" error /// private static readonly double[] Safety3 = { 0.0010, 4.98, 0.207, 0.0067 }; /// /// Abscissa of the center of the ellipse /// internal double Cx; /// /// Ordinate of the center of the ellipse /// internal double Cy; /// /// Semi-major axis /// internal double A; /// /// Semi-minor axis /// internal double B; /// /// Orientation of the major axis with respect to the x axis /// internal double Theta; /// /// Pre-calculated cosine value for the major-axis-to-X orientation (Theta) /// private readonly double _cosTheta; /// /// Pre-calculated sine value for the major-axis-to-X orientation (Theta) /// private readonly double _sinTheta; /// /// Start angle of the arc /// internal double Eta1; /// /// End angle of the arc /// internal double Eta2; /// /// Abscissa of the start point /// internal double X1; /// /// Ordinate of the start point /// internal double Y1; /// /// Abscissa of the end point /// internal double X2; /// /// Ordinate of the end point /// internal double Y2; /// /// Abscissa of the first focus /// internal double FirstFocusX; /// /// Ordinate of the first focus /// internal double FirstFocusY; /// /// Abscissa of the second focus /// internal double SecondFocusX; /// /// Ordinate of the second focus /// internal double SecondFocusY; /// /// Abscissa of the leftmost point of the arc /// private double _xLeft; /// /// Ordinate of the highest point of the arc /// private double _yUp; /// /// Horizontal width of the arc /// private double _width; /// /// Vertical height of the arc /// private double _height; /// /// Indicator for center to endpoints line inclusion /// internal bool IsPieSlice; /// /// Maximal degree for Bezier curve approximation /// private int _maxDegree; /// /// Default flatness for Bezier curve approximation /// private double _defaultFlatness; /// /// Indicator for semi-major axis significance (compared to semi-minor one). /// Computed by dividing the (A-B) difference by the value of A. /// This indicator is used for an early escape in intersection test /// internal double F; /// /// Indicator used for an early escape in intersection test /// internal double E2; /// /// Indicator used for an early escape in intersection test /// internal double G; /// /// Indicator used for an early escape in intersection test /// internal double G2; /// /// Builds an elliptical arc composed of the full unit circle around (0,0) /// public EllipticalArc() { Cx = 0; Cy = 0; A = 1; B = 1; Theta = 0; Eta1 = 0; Eta2 = TwoPi; _cosTheta = 1; _sinTheta = 0; IsPieSlice = false; _maxDegree = 3; _defaultFlatness = 0.5; ComputeFocii(); ComputeEndPoints(); ComputeBounds(); ComputeDerivedFlatnessParameters(); } /// /// Builds an elliptical arc from its canonical geometrical elements /// /// Center of the ellipse /// Semi-major axis /// Semi-minor axis /// Orientation of the major axis with respect to the x axis /// Start angle of the arc /// End angle of the arc /// If true, the lines between the center of the ellipse /// and the endpoints are part of the shape (it is pie slice like) public EllipticalArc(Point center, double a, double b, double theta, double lambda1, double lambda2, bool isPieSlice) : this(center.X, center.Y, a, b, theta, lambda1, lambda2, isPieSlice) { } /// /// Builds an elliptical arc from its canonical geometrical elements /// /// Abscissa of the center of the ellipse /// Ordinate of the center of the ellipse /// Semi-major axis /// Semi-minor axis /// Orientation of the major axis with respect to the x axis /// Start angle of the arc /// End angle of the arc /// If true, the lines between the center of the ellipse /// and the endpoints are part of the shape (it is pie slice like) public EllipticalArc(double cx, double cy, double a, double b, double theta, double lambda1, double lambda2, bool isPieSlice) { Cx = cx; Cy = cy; A = a; B = b; Theta = theta; IsPieSlice = isPieSlice; Eta1 = Math.Atan2(Math.Sin(lambda1) / b, Math.Cos(lambda1) / a); Eta2 = Math.Atan2(Math.Sin(lambda2) / b, Math.Cos(lambda2) / a); _cosTheta = Math.Cos(theta); _sinTheta = Math.Sin(theta); _maxDegree = 3; _defaultFlatness = 0.5; // half a pixel Eta2 -= TwoPi * Math.Floor((Eta2 - Eta1) / TwoPi); //make sure we have eta1 <= eta2 <= eta1 + 2 PI // the preceding correction fails if we have exactly eta2-eta1 == 2*PI // it reduces the interval to zero length if (lambda2 - lambda1 > Math.PI && Eta2 - Eta1 < Math.PI) { Eta2 += TwoPi; } ComputeFocii(); ComputeEndPoints(); ComputeBounds(); ComputeDerivedFlatnessParameters(); } /// /// Build a full ellipse from its canonical geometrical elements /// /// Center of the ellipse /// Semi-major axis /// Semi-minor axis /// Orientation of the major axis with respect to the x axis public EllipticalArc(Point center, double a, double b, double theta) : this(center.X, center.Y, a, b, theta) { } /// /// Build a full ellipse from its canonical geometrical elements /// /// Abscissa of the center of the ellipse /// Ordinate of the center of the ellipse /// Semi-major axis /// Semi-minor axis /// Orientation of the major axis with respect to the x axis public EllipticalArc(double cx, double cy, double a, double b, double theta) { Cx = cx; Cy = cy; A = a; B = b; Theta = theta; IsPieSlice = false; Eta1 = 0; Eta2 = TwoPi; _cosTheta = Math.Cos(theta); _sinTheta = Math.Sin(theta); _maxDegree = 3; _defaultFlatness = 0.5; //half a pixel ComputeFocii(); ComputeEndPoints(); ComputeBounds(); ComputeDerivedFlatnessParameters(); } /// /// Sets the maximal degree allowed for Bezier curve approximation. /// /// Maximal allowed degree (must be between 1 and 3) /// Thrown if maxDegree is not between 1 and 3 public void SetMaxDegree(int maxDegree) { if (maxDegree < 1 || maxDegree > 3) { throw new ArgumentException(@"maxDegree must be between 1 and 3", nameof(maxDegree)); } _maxDegree = maxDegree; } /// /// Sets the default flatness for Bezier curve approximation /// /// default flatness (must be greater than 1e-10) /// Thrown if defaultFlatness is lower than 1e-10 public void SetDefaultFlatness(double defaultFlatness) { if (defaultFlatness < 1.0E-10) { throw new ArgumentException(@"defaultFlatness must be greater than 1.0e-10", nameof(defaultFlatness)); } _defaultFlatness = defaultFlatness; } /// /// Computes the locations of the focii /// private void ComputeFocii() { double d = Math.Sqrt(A * A - B * B); double dx = d * _cosTheta; double dy = d * _sinTheta; FirstFocusX = Cx - dx; FirstFocusY = Cy - dy; SecondFocusX = Cx + dx; SecondFocusY = Cy + dy; } /// /// Computes the locations of the endpoints /// private void ComputeEndPoints() { double aCosEta1 = A * Math.Cos(Eta1); double bSinEta1 = B * Math.Sin(Eta1); X1 = Cx + aCosEta1 * _cosTheta - bSinEta1 * _sinTheta; Y1 = Cy + aCosEta1 * _sinTheta + bSinEta1 * _cosTheta; double aCosEta2 = A * Math.Cos(Eta2); double bSinEta2 = B * Math.Sin(Eta2); X2 = Cx + aCosEta2 * _cosTheta - bSinEta2 * _sinTheta; Y2 = Cy + aCosEta2 * _sinTheta + bSinEta2 * _cosTheta; } /// /// Computes the bounding box /// private void ComputeBounds() { double bOnA = B / A; double etaXMin; double etaXMax; double etaYMin; double etaYMax; if (Math.Abs(_sinTheta) < 0.1) { double tanTheta = _sinTheta / _cosTheta; if (_cosTheta < 0) { etaXMin = -Math.Atan(tanTheta * bOnA); etaXMax = etaXMin + Math.PI; etaYMin = 0.5 * Math.PI - Math.Atan(tanTheta / bOnA); etaYMax = etaYMin + Math.PI; } else { etaXMax = -Math.Atan(tanTheta * bOnA); etaXMin = etaXMax - Math.PI; etaYMax = 0.5 * Math.PI - Math.Atan(tanTheta / bOnA); etaYMin = etaYMax - Math.PI; } } else { double invTanTheta = _cosTheta / _sinTheta; if (_sinTheta < 0) { etaXMax = 0.5 * Math.PI + Math.Atan(invTanTheta / bOnA); etaXMin = etaXMax - Math.PI; etaYMin = Math.Atan(invTanTheta * bOnA); etaYMax = etaYMin + Math.PI; } else { etaXMin = 0.5 * Math.PI + Math.Atan(invTanTheta / bOnA); etaXMax = etaXMin + Math.PI; etaYMax = Math.Atan(invTanTheta * bOnA); etaYMin = etaYMax - Math.PI; } } etaXMin -= TwoPi * Math.Floor((etaXMin - Eta1) / TwoPi); etaYMin -= TwoPi * Math.Floor((etaYMin - Eta1) / TwoPi); etaXMax -= TwoPi * Math.Floor((etaXMax - Eta1) / TwoPi); etaYMax -= TwoPi * Math.Floor((etaYMax - Eta1) / TwoPi); _xLeft = etaXMin <= Eta2 ? Cx + A * Math.Cos(etaXMin) * _cosTheta - B * Math.Sin(etaXMin) * _sinTheta : Math.Min(X1, X2); _yUp = etaYMin <= Eta2 ? Cy + A * Math.Cos(etaYMin) * _sinTheta + B * Math.Sin(etaYMin) * _cosTheta : Math.Min(Y1, Y2); _width = (etaXMax <= Eta2 ? Cx + A * Math.Cos(etaXMax) * _cosTheta - B * Math.Sin(etaXMax) * _sinTheta : Math.Max(X1, X2)) - _xLeft; _height = (etaYMax <= Eta2 ? Cy + A * Math.Cos(etaYMax) * _sinTheta + B * Math.Sin(etaYMax) * _cosTheta : Math.Max(Y1, Y2)) - _yUp; } /// /// Computes the flatness parameters used in intersection tests /// private void ComputeDerivedFlatnessParameters() { F = (A - B) / A; E2 = F * (2.0 - F); G = 1.0 - F; G2 = G * G; } /// /// Computes the value of a rational function. /// This method handles rational functions where the numerator is quadratic /// and the denominator is linear /// /// Abscissa for which the value should be computed /// Coefficients array of the rational function /// private static double RationalFunction(double x, double[] c) { return (x * (x * c[0] + c[1]) + c[2]) / (x + c[3]); } /// /// Estimate the approximation error for a sub-arc of the instance /// /// Degree of the Bezier curve to use (1, 2 or 3) /// Start angle of the sub-arc /// End angle of the sub-arc /// Upper bound of the approximation error between the Bezier curve and the real ellipse public double EstimateError(int degree, double etaA, double etaB) { if (degree < 1 || degree > _maxDegree) throw new ArgumentException($"degree should be between {1} and {_maxDegree}", nameof(degree)); double eta = 0.5 * (etaA + etaB); if (degree < 2) { //start point double aCosEtaA = A * Math.Cos(etaA); double bSinEtaA = B * Math.Sin(etaA); double xA = Cx + aCosEtaA * _cosTheta - bSinEtaA * _sinTheta; double yA = Cy + aCosEtaA * _sinTheta + bSinEtaA * _cosTheta; //end point double aCosEtaB = A * Math.Cos(etaB); double bSinEtaB = B * Math.Sin(etaB); double xB = Cx + aCosEtaB * _cosTheta - bSinEtaB * _sinTheta; double yB = Cy + aCosEtaB * _sinTheta + bSinEtaB * _cosTheta; //maximal error point double aCosEta = A * Math.Cos(eta); double bSinEta = B * Math.Sin(eta); double x = Cx + aCosEta * _cosTheta - bSinEta * _sinTheta; double y = Cy + aCosEta * _sinTheta + bSinEta * _cosTheta; double dx = xB - xA; double dy = yB - yA; return Math.Abs(x * dy - y * dx + xB * yA - xA * yB) / Math.Sqrt(dx * dx + dy * dy); } else { double x = B / A; double dEta = etaB - etaA; double cos2 = Math.Cos(2 * eta); double cos4 = Math.Cos(4 * eta); double cos6 = Math.Cos(6 * eta); // select the right coefficients set according to degree and b/a double[][][] coeffs; double[] safety; if (degree == 2) { coeffs = x < 0.25 ? Coeffs2Low : Coeffs2High; safety = Safety2; } else { coeffs = x < 0.25 ? Coeffs3Low : Coeffs3High; safety = Safety3; } double c0 = RationalFunction(x, coeffs[0][0]) + cos2 * RationalFunction(x, coeffs[0][1]) + cos4 * RationalFunction(x, coeffs[0][2]) + cos6 * RationalFunction(x, coeffs[0][3]); double c1 = RationalFunction(x, coeffs[1][0]) + cos2 * RationalFunction(x, coeffs[1][1]) + cos4 * RationalFunction(x, coeffs[1][2]) + cos6 * RationalFunction(x, coeffs[1][3]); return RationalFunction(x, safety) * A * Math.Exp(c0 + c1 * dEta); } } /// /// Get the elliptical arc point for a given angular parameter /// /// Angular parameter for which point is desired /// The desired elliptical arc point location public Point PointAt(double lambda) { double eta = Math.Atan2(Math.Sin(lambda) / B, Math.Cos(lambda) / A); double aCosEta = A * Math.Cos(eta); double bSinEta = B * Math.Sin(eta); Point p = new Point(Cx + aCosEta * _cosTheta - bSinEta * _sinTheta, Cy + aCosEta * _sinTheta + bSinEta * _cosTheta); return p; } /// /// Tests if the specified coordinates are inside the closed shape formed by this arc. /// If this is not a pie, then a shape derived by adding a closing chord is considered. /// /// Abscissa of the test point /// Ordinate of the test point /// True if the specified coordinates are inside the closed shape of this arc public bool Contains(double x, double y) { // position relative to the focii double dx1 = x - FirstFocusX; double dy1 = y - FirstFocusY; double dx2 = x - SecondFocusX; double dy2 = y - SecondFocusY; if (dx1 * dx1 + dy1 * dy1 + dx2 * dx2 + dy2 * dy2 > 4 * A * A) { // the point is outside of the ellipse return false; } if (IsPieSlice) { // check the location of the test point with respect to the // angular sector counted from the center of the ellipse double dxC = x - Cx; double dyC = y - Cy; double u = dxC * _cosTheta + dyC * _sinTheta; double v = dyC * _cosTheta - dxC * _sinTheta; double eta = Math.Atan2(v / B, u / A); eta -= TwoPi * Math.Floor((eta - Eta1) / TwoPi); return eta <= Eta2; } // check the location of the test point with respect to the // chord joining the start and end points double dx = X2 - X1; double dy = Y2 - Y1; return x * dy - y * dx + X2 * Y1 - X1 * Y2 >= 0; } /// /// Tests if a line segment intersects the arc /// /// abscissa of the first point of the line segment /// ordinate of the first point of the line segment /// abscissa of the second point of the line segment /// ordinate of the second point of the line segment /// true if the two line segments intersect private bool IntersectArc(double xA, double yA, double xB, double yB) { double dx = xA - xB; double dy = yA - yB; double l = Math.Sqrt(dx * dx + dy * dy); if (l < 1.0E-10 * A) { // too small line segment, we consider it doesn't intersect anything return false; } double cz = (dx * _cosTheta + dy * _sinTheta) / l; double sz = (dy * _cosTheta - dx * _sinTheta) / l; // express position of the first point in canonical frame dx = xA - Cx; dy = yA - Cy; double u = dx * _cosTheta + dy * _sinTheta; double v = dy * _cosTheta - dx * _sinTheta; double u2 = u * u; double v2 = v * v; double g2U2Ma2 = G2 * (u2 - A * A); //double g2U2Ma2Mv2 = g2U2Ma2 - v2; double g2U2Ma2Pv2 = g2U2Ma2 + v2; // compute intersections with the ellipse along the line // as the roots of a 2nd degree polynom : c0 k^2 - 2 c1 k + c2 = 0 double c0 = 1.0 - E2 * cz * cz; double c1 = G2 * u * cz + v * sz; double c2 = g2U2Ma2Pv2; double c12 = c1 * c1; double c0C2 = c0 * c2; if (c12 < c0C2) { // the line does not intersect the ellipse at all return false; } double k = c1 >= 0 ? (c1 + Math.Sqrt(c12 - c0C2)) / c0 : c2 / (c1 - Math.Sqrt(c12 - c0C2)); if (k >= 0 && k <= l) { double uIntersect = u - k * cz; double vIntersect = v - k * sz; double eta = Math.Atan2(vIntersect / B, uIntersect / A); eta -= TwoPi * Math.Floor((eta - Eta1) / TwoPi); if (eta <= Eta2) { return true; } } k = c2 / (k * c0); if (k >= 0 && k <= l) { double uIntersect = u - k * cz; double vIntersect = v - k * sz; double eta = Math.Atan2(vIntersect / B, uIntersect / A); eta -= TwoPi * Math.Floor((eta - Eta1) / TwoPi); if (eta <= Eta2) { return true; } } return false; } /// /// Tests if two line segments intersect /// /// Abscissa of the first point of the first line segment /// Ordinate of the first point of the first line segment /// Abscissa of the second point of the first line segment /// Ordinate of the second point of the first line segment /// Abscissa of the first point of the second line segment /// Ordinate of the first point of the second line segment /// Abscissa of the second point of the second line segment /// Ordinate of the second point of the second line segment /// true if the two line segments intersect private static bool Intersect(double x1, double y1, double x2, double y2, double xA, double yA, double xB, double yB) { // elements of the equation of the (1, 2) line segment double dx12 = x2 - x1; double dy12 = y2 - y1; double k12 = x2 * y1 - x1 * y2; // elements of the equation of the (A, B) line segment double dxAb = xB - xA; double dyAb = yB - yA; double kAb = xB * yA - xA * yB; // compute relative positions of endpoints versus line segments double pAvs12 = xA * dy12 - yA * dx12 + k12; double pBvs12 = xB * dy12 - yB * dx12 + k12; double p1VsAb = x1 * dyAb - y1 * dxAb + kAb; double p2VsAb = x2 * dyAb - y2 * dxAb + kAb; return pAvs12 * pBvs12 <= 0 && p1VsAb * p2VsAb <= 0; } /// /// Tests if a line segment intersects the outline /// /// Abscissa of the first point of the line segment /// Ordinate of the first point of the line segment /// Abscissa of the second point of the line segment /// Ordinate of the second point of the line segment /// true if the two line segments intersect private bool IntersectOutline(double xA, double yA, double xB, double yB) { if (IntersectArc(xA, yA, xB, yB)) { return true; } if (IsPieSlice) { return Intersect(Cx, Cy, X1, Y1, xA, yA, xB, yB) || Intersect(Cx, Cy, X2, Y2, xA, yA, xB, yB); } return Intersect(X1, Y1, X2, Y2, xA, yA, xB, yB); } /// /// Tests if the interior of a closed path derived from this arc entirely contains the specified rectangular area. /// The closed path is derived with respect to the IsPieSlice value. /// /// Abscissa of the upper-left corner of the test rectangle /// Ordinate of the upper-left corner of the test rectangle /// Width of the test rectangle /// Height of the test rectangle /// true if the interior of a closed path derived from this arc entirely contains the specified rectangular area; false otherwise public bool Contains(double x, double y, double w, double h) { double xPlusW = x + w; double yPlusH = y + h; return Contains(x, y) && Contains(xPlusW, y) && Contains(x, yPlusH) && Contains(xPlusW, yPlusH) && !IntersectOutline(x, y, xPlusW, y) && !IntersectOutline(xPlusW, y, xPlusW, yPlusH) && !IntersectOutline(xPlusW, yPlusH, x, yPlusH) && !IntersectOutline(x, yPlusH, x, y); } /// /// Tests if a specified Point2D is inside the boundary of a closed path derived from this arc. /// The closed path is derived with respect to the IsPieSlice value. /// /// Test point /// true if the specified point is inside a closed path derived from this arc public bool Contains(Point p) { return Contains(p.X, p.Y); } /// /// Tests if the interior of a closed path derived from this arc entirely contains the specified Rectangle2D. /// The closed path is derived with respect to the IsPieSlice value. /// /// Test rectangle /// True if the interior of a closed path derived from this arc entirely contains the specified Rectangle2D; false otherwise public bool Contains(Rect r) { return Contains(r.X, r.Y, r.Width, r.Height); } /// /// Returns an integer Rectangle that completely encloses the closed path derived from this arc. /// The closed path is derived with respect to the IsPieSlice value. /// public Rect GetBounds() { return new Rect(_xLeft, _yUp, _width, _height); } /// /// Builds the arc outline using given StreamGeometryContext and default (max) Bezier curve degree and acceptable error of half a pixel (0.5) /// /// A StreamGeometryContext to output the path commands to public void BuildArc(IStreamGeometryContextImpl path) { BuildArc(path, _maxDegree, _defaultFlatness, true); } /// /// Builds the arc outline using given StreamGeometryContext /// /// A StreamGeometryContext to output the path commands to /// degree of the Bezier curve to use /// acceptable error /// if true, a new figure will be started in the specified StreamGeometryContext public void BuildArc(IStreamGeometryContextImpl path, int degree, double threshold, bool openNewFigure) { if (degree < 1 || degree > _maxDegree) throw new ArgumentException($"degree should be between {1} and {_maxDegree}", nameof(degree)); // find the number of Bezier curves needed bool found = false; int n = 1; double dEta; double etaB; while (!found && n < 1024) { dEta = (Eta2 - Eta1) / n; if (dEta <= 0.5 * Math.PI) { etaB = Eta1; found = true; for (int i = 0; found && i < n; ++i) { double etaA = etaB; etaB += dEta; found = EstimateError(degree, etaA, etaB) <= threshold; } } n = n << 1; } dEta = (Eta2 - Eta1) / n; etaB = Eta1; double cosEtaB = Math.Cos(etaB); double sinEtaB = Math.Sin(etaB); double aCosEtaB = A * cosEtaB; double bSinEtaB = B * sinEtaB; double aSinEtaB = A * sinEtaB; double bCosEtaB = B * cosEtaB; double xB = Cx + aCosEtaB * _cosTheta - bSinEtaB * _sinTheta; double yB = Cy + aCosEtaB * _sinTheta + bSinEtaB * _cosTheta; double xBDot = -aSinEtaB * _cosTheta - bCosEtaB * _sinTheta; double yBDot = -aSinEtaB * _sinTheta + bCosEtaB * _cosTheta; /* This controls the drawing in case of pies if (openNewFigure) { if (IsPieSlice) { path.BeginFigure(new Point(Cx, Cy), false, false); path.LineTo(new Point(xB, yB), true, true); } else { path.BeginFigure(new Point(xB, yB), false, false); } } else { //path.LineTo(new Point(xB, yB), true, true); } */ //otherwise we're supposed to be already at the (xB,yB) double t = Math.Tan(0.5 * dEta); double alpha = Math.Sin(dEta) * (Math.Sqrt(4 + 3 * t * t) - 1) / 3; for (int i = 0; i < n; ++i) { //double etaA = etaB; double xA = xB; double yA = yB; double xADot = xBDot; double yADot = yBDot; etaB += dEta; cosEtaB = Math.Cos(etaB); sinEtaB = Math.Sin(etaB); aCosEtaB = A * cosEtaB; bSinEtaB = B * sinEtaB; aSinEtaB = A * sinEtaB; bCosEtaB = B * cosEtaB; xB = Cx + aCosEtaB * _cosTheta - bSinEtaB * _sinTheta; yB = Cy + aCosEtaB * _sinTheta + bSinEtaB * _cosTheta; xBDot = -aSinEtaB * _cosTheta - bCosEtaB * _sinTheta; yBDot = -aSinEtaB * _sinTheta + bCosEtaB * _cosTheta; if (degree == 1) { path.LineTo(new Point(xB, yB)); } else if (degree == 2) { double k = (yBDot * (xB - xA) - xBDot * (yB - yA)) / (xADot * yBDot - yADot * xBDot); path.QuadraticBezierTo(new Point(xA + k * xADot, yA + k * yADot), new Point(xB, yB)); } else { path.CubicBezierTo( new Point(xA + alpha * xADot, yA + alpha * yADot), new Point(xB - alpha * xBDot, yB - alpha * yBDot), new Point(xB, yB) ); } } if (IsPieSlice) { path.LineTo(new Point(Cx, Cy)); } } /// /// Calculates the angle between two vectors /// /// Vector V1 /// Vector V2 /// The signed angle between v2 and v1 static double GetAngle(Vector v1, Vector v2) { var scalar = v1 * v2; return Math.Atan2(v1.X * v2.Y - v2.X * v1.Y, scalar); } /// /// Simple matrix used for rotate transforms. /// At some point I did not trust the WPF Matrix struct, and wrote my own simple one -_- /// This is supposed to be replaced with proper WPF Matrices everywhere /// private readonly struct SimpleMatrix { private readonly double _a, _b, _c, _d; public SimpleMatrix(double a, double b, double c, double d) { _a = a; _b = b; _c = c; _d = d; } public static Point operator *(SimpleMatrix m, Point p) { return new Point(m._a * p.X + m._b * p.Y, m._c * p.X + m._d * p.Y); } } /// /// ArcTo Helper for StreamGeometryContext /// /// Target path /// Start point /// End point /// Ellipse radii /// Ellipse theta (angle measured from the abscissa) /// Large Arc Indicator /// Clockwise direction flag public static void BuildArc(IStreamGeometryContextImpl path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise) { // var orthogonalizer = new RotateTransform(-theta); var orth = new SimpleMatrix(Math.Cos(theta), Math.Sin(theta), -Math.Sin(theta), Math.Cos(theta)); var rest = new SimpleMatrix(Math.Cos(theta), -Math.Sin(theta), Math.Sin(theta), Math.Cos(theta)); // var restorer = orthogonalizer.Inverse; // if(restorer == null) throw new InvalidOperationException("Can't get a restorer!"); Point p1S = orth * (new Point((p1.X - p2.X) / 2, (p1.Y - p2.Y) / 2)); double rx = size.Width; double ry = size.Height; double rx2 = rx * rx; double ry2 = ry * ry; double y1S2 = p1S.Y * p1S.Y; double x1S2 = p1S.X * p1S.X; double numerator = rx2*ry2 - rx2*y1S2 - ry2*x1S2; double denominator = rx2*y1S2 + ry2*x1S2; if (Math.Abs(denominator) < 1e-8) { path.LineTo(p2); return; } if ((numerator / denominator) < 0) { double lambda = x1S2/rx2 + y1S2/ry2; double lambdaSqrt = Math.Sqrt(lambda); if (lambda > 1) { rx *= lambdaSqrt; ry *= lambdaSqrt; rx2 = rx*rx; ry2 = ry*ry; numerator = rx2 * ry2 - rx2 * y1S2 - ry2 * x1S2; if (numerator < 0) numerator = 0; denominator = rx2 * y1S2 + ry2 * x1S2; } } double multiplier = Math.Sqrt(numerator / denominator); Point mulVec = new Point(rx * p1S.Y / ry, -ry * p1S.X / rx); int sign = (clockwise != isLargeArc) ? 1 : -1; Point cs = new Point(mulVec.X * multiplier * sign, mulVec.Y * multiplier * sign); Vector translation = new Vector((p1.X + p2.X) / 2, (p1.Y + p2.Y) / 2); Point c = rest * (cs) + translation; // See "http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter" to understand // how the ellipse center is calculated // from here, W3C recommendations from the above link make less sense than Darth Vader pouring // some sea water in a water filter while standing in the water confused // Therefore, we are on our own with our task of finding out lambda1 and lambda2 // matching our points p1 and p2. // Fortunately it is not so difficult now, when we already know the ellipse centre. // We eliminate the offset, making our ellipse zero-centered, then we eliminate the theta, // making its Y and X axes the same as global axes. Then we can easily get our angles using // good old school formula for angles between vectors. // We should remember that this class expects true angles, and not the t-values for ellipse equation. // To understand how t-values are obtained, one should see Etas calculation in the constructor code. var p1NoOffset = orth * (p1-c); var p2NoOffset = orth * (p2-c); // if the arc is drawn clockwise, we swap start and end points var revisedP1 = clockwise ? p1NoOffset : p2NoOffset; var revisedP2 = clockwise ? p2NoOffset : p1NoOffset; var thetaStart = GetAngle(new Vector(1, 0), revisedP1); var thetaEnd = GetAngle(new Vector(1, 0), revisedP2); // Uncomment this to draw a pie // path.LineTo(c, true, true); // path.LineTo(clockwise ? p1 : p2, true,true); path.LineTo(clockwise ? p1 : p2); var arc = new EllipticalArc(c.X, c.Y, rx, ry, theta, thetaStart, thetaEnd, false); arc.BuildArc(path, arc._maxDegree, arc._defaultFlatness, false); //uncomment this to draw a pie //path.LineTo(c, true, true); } /// /// Tests if the interior of the closed path derived from this arc intersects the interior of a specified rectangular area. /// The closed path is derived with respect to the IsPieSlice value. /// public bool Intersects(double x, double y, double w, double h) { double xPlusW = x + w; double yPlusH = y + h; return Contains(x, y) || Contains(xPlusW, y) || Contains(x, yPlusH) || Contains(xPlusW, yPlusH) || IntersectOutline(x, y, xPlusW, y) || IntersectOutline(xPlusW, y, xPlusW, yPlusH) || IntersectOutline(xPlusW, yPlusH, x, yPlusH) || IntersectOutline(x, yPlusH, x, y); } /// /// Tests if the interior of the closed path derived from this arc intersects the interior of a specified rectangular area. /// The closed path is derived with respect to the IsPieSlice value. /// public bool Intersects(Rect r) { return Intersects(r.X, r.Y, r.Width, r.Height); } } public static void ArcTo(IStreamGeometryContextImpl streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*Math.PI/180, isLargeArc, sweepDirection == SweepDirection.Clockwise); } } }