//+------------------------------------------------------------------------------------------------- // // 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> /// Multiplies a point with a matrix. /// </summary> public static XPoint Multiply(XPoint point, XMatrix matrix) { return matrix.Transform(point); }
/// <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); }
internal static void TransformRect(ref XRect rect, ref XMatrix matrix) { if (!rect.IsEmpty) { XMatrixTypes types = matrix.type; if (types != XMatrixTypes.Identity) { if ((types & XMatrixTypes.Scaling) != XMatrixTypes.Identity) { rect.x *= matrix.m11; rect.y *= matrix.m22; rect.width *= matrix.m11; rect.height *= matrix.m22; if (rect.width < 0) { rect.x += rect.width; rect.width = -rect.width; } if (rect.height < 0) { rect.y += rect.height; rect.height = -rect.height; } } if ((types & XMatrixTypes.Translation) != XMatrixTypes.Identity) { rect.x += matrix.offsetX; rect.y += matrix.offsetY; } if (types == XMatrixTypes.Unknown) { XPoint point = matrix.Transform(rect.TopLeft); XPoint point2 = matrix.Transform(rect.TopRight); XPoint point3 = matrix.Transform(rect.BottomRight); XPoint point4 = matrix.Transform(rect.BottomLeft); rect.x = Math.Min(Math.Min(point.X, point2.X), Math.Min(point3.X, point4.X)); rect.y = Math.Min(Math.Min(point.Y, point2.Y), Math.Min(point3.Y, point4.Y)); rect.width = Math.Max(Math.Max(point.X, point2.X), Math.Max(point3.X, point4.X)) - rect.x; rect.height = Math.Max(Math.Max(point.Y, point2.Y), Math.Max(point3.Y, point4.Y)) - rect.y; } } } }
/// <summary> /// /// </summary> /// <param name="gfx"></param> /// <param name="line"></param> /// <param name="dx"></param> /// <param name="dy"></param> /// <param name="db"></param> /// <param name="r"></param> public void Draw(object gfx, Kaliber3D.Render.XLine line, double dx, double dy, ImmutableArray<Kaliber3D.Render.ShapeProperty> db, Kaliber3D.Render.Record r) { if (!line.IsStroked) return; var _gfx = gfx as XGraphics; XPen strokeLine = ToXPen(line.Style, _scaleToPage); XSolidBrush fillStartArrow = ToXSolidBrush(line.Style.StartArrowStyle.Fill); XPen strokeStartArrow = ToXPen(line.Style.StartArrowStyle, _scaleToPage); XSolidBrush fillEndArrow = ToXSolidBrush(line.Style.EndArrowStyle.Fill); XPen strokeEndArrow = ToXPen(line.Style.EndArrowStyle, _scaleToPage); double _x1 = line.Start.X + dx; double _y1 = line.Start.Y + dy; double _x2 = line.End.X + dx; double _y2 = line.End.Y + dy; Kaliber3D.Render.XLine.SetMaxLength(line, ref _x1, ref _y1, ref _x2, ref _y2); double x1 = _scaleToPage(_x1); double y1 = _scaleToPage(_y1); double x2 = _scaleToPage(_x2); double y2 = _scaleToPage(_y2); var sas = line.Style.StartArrowStyle; var eas = line.Style.EndArrowStyle; double a1 = Math.Atan2(y1 - y2, x1 - x2) * 180.0 / Math.PI; double a2 = Math.Atan2(y2 - y1, x2 - x1) * 180.0 / Math.PI; var t1 = new XMatrix(); var c1 = new XPoint(x1, y1); t1.RotateAtPrepend(a1, c1); var t2 = new XMatrix(); var c2 = new XPoint(x2, y2); t2.RotateAtPrepend(a2, c2); XPoint pt1; XPoint pt2; double radiusX1 = sas.RadiusX; double radiusY1 = sas.RadiusY; double sizeX1 = 2.0 * radiusX1; double sizeY1 = 2.0 * radiusY1; switch (sas.ArrowType) { default: case Kaliber3D.Render.ArrowType.None: { pt1 = new XPoint(x1, y1); } break; case Kaliber3D.Render.ArrowType.Rectangle: { pt1 = t1.Transform(new XPoint(x1 - sizeX1, y1)); var rect = new XRect(x1 - sizeX1, y1 - radiusY1, sizeX1, sizeY1); _gfx.Save(); _gfx.RotateAtTransform(a1, c1); DrawRectangleInternal(_gfx, fillStartArrow, strokeStartArrow, sas.IsStroked, sas.IsFilled, ref rect); _gfx.Restore(); } break; case Kaliber3D.Render.ArrowType.Ellipse: { pt1 = t1.Transform(new XPoint(x1 - sizeX1, y1)); _gfx.Save(); _gfx.RotateAtTransform(a1, c1); var rect = new XRect(x1 - sizeX1, y1 - radiusY1, sizeX1, sizeY1); DrawEllipseInternal(_gfx, fillStartArrow, strokeStartArrow, sas.IsStroked, sas.IsFilled, ref rect); _gfx.Restore(); } break; case Kaliber3D.Render.ArrowType.Arrow: { pt1 = t1.Transform(new XPoint(x1, y1)); var p11 = t1.Transform(new XPoint(x1 - sizeX1, y1 + sizeY1)); var p21 = t1.Transform(new XPoint(x1, y1)); var p12 = t1.Transform(new XPoint(x1 - sizeX1, y1 - sizeY1)); var p22 = t1.Transform(new XPoint(x1, y1)); DrawLineInternal(_gfx, strokeStartArrow, sas.IsStroked, ref p11, ref p21); DrawLineInternal(_gfx, strokeStartArrow, sas.IsStroked, ref p12, ref p22); } break; } double radiusX2 = eas.RadiusX; double radiusY2 = eas.RadiusY; double sizeX2 = 2.0 * radiusX2; double sizeY2 = 2.0 * radiusY2; switch (eas.ArrowType) { default: case Kaliber3D.Render.ArrowType.None: { pt2 = new XPoint(x2, y2); } break; case Kaliber3D.Render.ArrowType.Rectangle: { pt2 = t2.Transform(new XPoint(x2 - sizeX2, y2)); var rect = new XRect(x2 - sizeX2, y2 - radiusY2, sizeX2, sizeY2); _gfx.Save(); _gfx.RotateAtTransform(a2, c2); DrawRectangleInternal(_gfx, fillEndArrow, strokeEndArrow, eas.IsStroked, eas.IsFilled, ref rect); _gfx.Restore(); } break; case Kaliber3D.Render.ArrowType.Ellipse: { pt2 = t2.Transform(new XPoint(x2 - sizeX2, y2)); _gfx.Save(); _gfx.RotateAtTransform(a2, c2); var rect = new XRect(x2 - sizeX2, y2 - radiusY2, sizeX2, sizeY2); DrawEllipseInternal(_gfx, fillEndArrow, strokeEndArrow, eas.IsStroked, eas.IsFilled, ref rect); _gfx.Restore(); } break; case Kaliber3D.Render.ArrowType.Arrow: { pt2 = t2.Transform(new XPoint(x2, y2)); var p11 = t2.Transform(new XPoint(x2 - sizeX2, y2 + sizeY2)); var p21 = t2.Transform(new XPoint(x2, y2)); var p12 = t2.Transform(new XPoint(x2 - sizeX2, y2 - sizeY2)); var p22 = t2.Transform(new XPoint(x2, y2)); DrawLineInternal(_gfx, strokeEndArrow, eas.IsStroked, ref p11, ref p21); DrawLineInternal(_gfx, strokeEndArrow, eas.IsStroked, ref p12, ref p22); } break; } _gfx.DrawLine(strokeLine, pt1, pt2); }
public static XVector Multiply(XVector vector, XMatrix matrix) { return matrix.Transform(vector); }
private static XPoint DrawLineArrowInternal(XGraphics gfx, XPen pen, XSolidBrush brush, double x, double y, double angle, Core2D.Style.ArrowStyle style) { XPoint pt; var rt = new XMatrix(); var c = new XPoint(x, y); rt.RotateAtPrepend(angle, c); double rx = style.RadiusX; double ry = style.RadiusY; double sx = 2.0 * rx; double sy = 2.0 * ry; switch (style.ArrowType) { default: case Core2D.Style.ArrowType.None: { pt = new XPoint(x, y); } break; case Core2D.Style.ArrowType.Rectangle: { pt = rt.Transform(new XPoint(x - sx, y)); var rect = new XRect(x - sx, y - ry, sx, sy); gfx.Save(); gfx.RotateAtTransform(angle, c); DrawRectangleInternal(gfx, brush, pen, style.IsStroked, style.IsFilled, ref rect); gfx.Restore(); } break; case Core2D.Style.ArrowType.Ellipse: { pt = rt.Transform(new XPoint(x - sx, y)); gfx.Save(); gfx.RotateAtTransform(angle, c); var rect = new XRect(x - sx, y - ry, sx, sy); DrawEllipseInternal(gfx, brush, pen, style.IsStroked, style.IsFilled, ref rect); gfx.Restore(); } break; case Core2D.Style.ArrowType.Arrow: { pt = rt.Transform(new XPoint(x, y)); var p11 = rt.Transform(new XPoint(x - sx, y + sy)); var p21 = rt.Transform(new XPoint(x, y)); var p12 = rt.Transform(new XPoint(x - sx, y - sy)); var p22 = rt.Transform(new XPoint(x, y)); DrawLineInternal(gfx, pen, style.IsStroked, ref p11, ref p21); DrawLineInternal(gfx, pen, style.IsStroked, ref p12, ref p22); } break; } return pt; }
void AddCapToPath(Path path, PathFigure figure, double length, double lineWidthHalf, LineCap lineCap, XMatrix matrix) { // sketch: // 1. create Transform that make a horizontal line with start in 0,0 // 2. create a Polygon with the shape of the line including its caps // 3. render the shape with the brush of the pen //PolyLineSegment seg; switch (lineCap) { case LineCap.Flat: matrix.Transform(new XPoint(length + lineWidthHalf, -lineWidthHalf)); break; case LineCap.Square: matrix.Transform(new XPoint(length + lineWidthHalf, -lineWidthHalf)); break; case LineCap.Round: break; case LineCap.Triangle: break; } }