Skip to content

Commit

Permalink
Merge pull request #144 from derBobo/master
Browse files Browse the repository at this point in the history
Implenting addArc feature
  • Loading branch information
tocsoft authored Sep 9, 2021
2 parents d31cc33 + 0087497 commit 22c2a27
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 2 deletions.
171 changes: 171 additions & 0 deletions src/ImageSharp.Drawing/Shapes/EllipticalArcLineSegment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.Numerics;

namespace SixLabors.ImageSharp.Drawing
{
/// <summary>
/// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc
/// </summary>
/// <seealso cref="ILineSegment" />
public sealed class EllipticalArcLineSegment : ILineSegment
{
private const float MinimumSqrDistance = 1.75f;
private readonly PointF[] linePoints;
private readonly float x;
private readonly float y;
private readonly float radiusX;
private readonly float radiusY;
private readonly float rotation;
private readonly float startAngle;
private readonly float sweepAngle;
private readonly Matrix3x2 transformation;

/// <summary>
/// Initializes a new instance of the <see cref="EllipticalArcLineSegment"/> class.
/// </summary>
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
/// <param name="radiusX">X radius of the ellipsis.</param>
/// <param name="radiusY">Y radius of the ellipsis.</param>
/// <param name="rotation">The rotation of (<paramref name="radiusX"/> to the X-axis and (<paramref name="radiusY"/> to the Y-axis, measured in degrees clockwise.</param>
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
/// <param name="transformation">The Tranformation matrix, that should be used on the arc.</param>
public EllipticalArcLineSegment(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle, Matrix3x2 transformation)
{
Guard.MustBeGreaterThanOrEqualTo(radiusX, 0, nameof(radiusX));
Guard.MustBeGreaterThanOrEqualTo(radiusY, 0, nameof(radiusY));
this.x = x;
this.y = y;
this.radiusX = radiusX;
this.radiusY = radiusY;
this.rotation = rotation % 360;
this.startAngle = startAngle % 360;
this.transformation = transformation;
this.sweepAngle = sweepAngle;
if (sweepAngle > 360)
{
this.sweepAngle = 360;
}

if (sweepAngle < -360)
{
this.sweepAngle = -360;
}

this.linePoints = this.GetDrawingPoints();
this.EndPoint = this.linePoints[this.linePoints.Length - 1];
}

/// <summary>
/// Gets the end point.
/// </summary>
/// <value>
/// The end point.
/// </value>
public PointF EndPoint { get; }

/// <summary>
/// Transforms the current LineSegment using specified matrix.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A line segment with the matrix applied to it.</returns>
public EllipticalArcLineSegment Transform(Matrix3x2 matrix)
{
if (matrix.IsIdentity)
{
return this;
}

return new EllipticalArcLineSegment(this.x, this.y, this.radiusX, this.radiusY, this.rotation, this.startAngle, this.sweepAngle, Matrix3x2.Multiply(this.transformation, matrix));
}

/// <summary>
/// Transforms the current LineSegment using specified matrix.
/// </summary>
/// <param name="matrix">The matrix.</param>
/// <returns>A line segment with the matrix applied to it.</returns>
ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix);

private PointF[] GetDrawingPoints()
{
var points = new List<PointF>()
{
this.CalculatePoint(this.startAngle)
};
if (this.sweepAngle < 0)
{
for (float i = this.startAngle; i > this.startAngle + this.sweepAngle; i--)
{
float end = i - 1;
if (end <= this.startAngle + this.sweepAngle)
{
end = this.startAngle + this.sweepAngle;
}

points.AddRange(this.GetDrawingPoints(i, end, 0));
}
}
else
{
for (float i = this.startAngle; i < this.startAngle + this.sweepAngle; i++)
{
float end = i + 1;
if (end >= this.startAngle + this.sweepAngle)
{
end = this.startAngle + this.sweepAngle;
}

points.AddRange(this.GetDrawingPoints(i, end, 0));
}
}

return points.ToArray();
}

private List<PointF> GetDrawingPoints(float start, float end, int depth)
{
if (depth > 1000)
{
return new List<PointF>();
}

var points = new List<PointF>();

PointF startP = this.CalculatePoint(start);
PointF endP = this.CalculatePoint(end);
if ((new Vector2(endP.X, endP.Y) - new Vector2(startP.X, startP.Y)).LengthSquared() < MinimumSqrDistance)
{
points.Add(endP);
}
else
{
float mid = start + ((end - start) / 2);
points.AddRange(this.GetDrawingPoints(start, mid, depth + 1));
points.AddRange(this.GetDrawingPoints(mid, end, depth + 1));
}

return points;
}

private PointF CalculatePoint(float angle)
{
float x = (this.radiusX * MathF.Sin(MathF.PI * angle / 180) * MathF.Cos(MathF.PI * this.rotation / 180)) - (this.radiusY * MathF.Cos(MathF.PI * angle / 180) * MathF.Sin(MathF.PI * this.rotation / 180)) + this.x;
float y = (this.radiusX * MathF.Sin(MathF.PI * angle / 180) * MathF.Sin(MathF.PI * this.rotation / 180)) + (this.radiusY * MathF.Cos(MathF.PI * angle / 180) * MathF.Cos(MathF.PI * this.rotation / 180)) + this.y;
var currPoint = new PointF(x, y);
return PointF.Transform(currPoint, this.transformation);
}

/// <summary>
/// Returns the current <see cref="ILineSegment" /> a simple linear path.
/// </summary>
/// <returns>
/// Returns the current <see cref="ILineSegment" /> as simple linear path.
/// </returns>
public ReadOnlyMemory<PointF> Flatten() => this.linePoints;
}
}
80 changes: 80 additions & 0 deletions src/ImageSharp.Drawing/Shapes/PathBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,86 @@ public PathBuilder AddBezier(PointF startPoint, PointF controlPoint1, PointF con
return this;
}

/// <summary>
/// Adds an elliptical arc to the current figure
/// </summary>
/// <param name="rect"> A <see cref="RectangleF"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
/// <param name="rotation">The rotation of (<paramref name="rect"/>, measured in degrees clockwise.</param>
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
/// <returns>The <see cref="PathBuilder"/></returns>
public PathBuilder AddEllipticalArc(RectangleF rect, float rotation, float startAngle, float sweepAngle) => this.AddEllipticalArc((rect.Right + rect.Left) / 2, (rect.Bottom + rect.Top) / 2, rect.Width / 2, rect.Height / 2, rotation, startAngle, sweepAngle);

/// <summary>
/// Adds an elliptical arc to the current figure
/// </summary>
/// <param name="rect"> A <see cref="Rectangle"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
/// <param name="rotation">The rotation of (<paramref name="rect"/>, measured in degrees clockwise.</param>
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
/// <returns>The <see cref="PathBuilder"/></returns>
public PathBuilder AddEllipticalArc(Rectangle rect, int rotation, int startAngle, int sweepAngle) => this.AddEllipticalArc((float)(rect.Right + rect.Left) / 2, (float)(rect.Bottom + rect.Top) / 2, (float)rect.Width / 2, (float)rect.Height / 2, rotation, startAngle, sweepAngle);

/// <summary>
/// Adds an elliptical arc to the current figure
/// </summary>
/// <param name="center"> The center <see cref="PointF"/> of the ellips from which the arc is taken.</param>
/// <param name="radiusX">X radius of the ellipsis.</param>
/// <param name="radiusY">Y radius of the ellipsis.</param>
/// <param name="rotation">The rotation of (<paramref name="radiusX"/> to the X-axis and (<paramref name="radiusY"/> to the Y-axis, measured in degrees clockwise.</param>
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
/// <returns>The <see cref="PathBuilder"/></returns>
public PathBuilder AddEllipticalArc(PointF center, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle) => this.AddEllipticalArc(center.X, center.Y, radiusX, radiusY, rotation, startAngle, sweepAngle);

/// <summary>
/// Adds an elliptical arc to the current figure
/// </summary>
/// <param name="center"> The center <see cref="Point"/> of the ellips from which the arc is taken.</param>
/// <param name="radiusX">X radius of the ellipsis.</param>
/// <param name="radiusY">Y radius of the ellipsis.</param>
/// <param name="rotation">The rotation of (<paramref name="radiusX"/> to the X-axis and (<paramref name="radiusY"/> to the Y-axis, measured in degrees clockwise.</param>
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
/// <returns>The <see cref="PathBuilder"/></returns>
public PathBuilder AddEllipticalArc(Point center, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle) => this.AddEllipticalArc(center.X, center.Y, radiusX, radiusY, rotation, startAngle, sweepAngle);

/// <summary>
/// Adds an elliptical arc to the current figure
/// </summary>
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
/// <param name="radiusX">X radius of the ellipsis.</param>
/// <param name="radiusY">Y radius of the ellipsis.</param>
/// <param name="rotation">The rotation of (<paramref name="radiusX"/> to the X-axis and (<paramref name="radiusY"/> to the Y-axis, measured in degrees clockwise.</param>
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
/// <returns>The <see cref="PathBuilder"/></returns>
public PathBuilder AddEllipticalArc(int x, int y, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle)
{
this.currentFigure.AddSegment(new EllipticalArcLineSegment(x, y, radiusX, radiusY, rotation, startAngle, sweepAngle, this.currentTransform));

return this;
}

/// <summary>
/// Adds an elliptical arc to the current figure
/// </summary>
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
/// <param name="radiusX">X radius of the ellipsis.</param>
/// <param name="radiusY">Y radius of the ellipsis.</param>
/// <param name="rotation">The rotation of (<paramref name="radiusX"/> to the X-axis and (<paramref name="radiusY"/> to the Y-axis, measured in degrees clockwise.</param>
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
/// <returns>The <see cref="PathBuilder"/></returns>
public PathBuilder AddEllipticalArc(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)
{
this.currentFigure.AddSegment(new EllipticalArcLineSegment(x, y, radiusX, radiusY, rotation, startAngle, sweepAngle, this.currentTransform));

return this;
}

/// <summary>
/// Starts a new figure but leaves the previous one open.
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class DrawPathTests
};

[Theory]
[WithSolidFilledImages(nameof(DrawPathData), 300, 450, "Blue", PixelTypes.Rgba32)]
[WithSolidFilledImages(nameof(DrawPathData), 300, 600, "Blue", PixelTypes.Rgba32)]
public void DrawPath<TPixel>(TestImageProvider<TPixel> provider, string colorName, byte alpha, float thickness)
where TPixel : unmanaged, IPixel<TPixel>
{
Expand All @@ -36,8 +36,10 @@ public void DrawPath<TPixel>(TestImageProvider<TPixel> provider, string colorNam
new Vector2(500, 500),
new Vector2(60, 10),
new Vector2(10, 400));
var ellipticArcSegment1 = new EllipticalArcLineSegment(80, 425, (float)Math.Sqrt(5525), 40, (float)(Math.Atan2(25, 70) * 180 / Math.PI), -90, -180, Matrix3x2.Identity);
var ellipticArcSegment2 = new EllipticalArcLineSegment(150, 520, 140, 70, 0, 180, 360, Matrix3x2.Identity);

var path = new Path(linearSegment, bezierSegment);
var path = new Path(linearSegment, bezierSegment, ellipticArcSegment1, ellipticArcSegment2);

Rgba32 rgba = TestUtils.GetColorByName(colorName);
rgba.A = alpha;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System.Collections.Generic;
using System.Numerics;
using Xunit;

namespace SixLabors.ImageSharp.Drawing.Tests
{
public class EllipticalArcLineSegmentTest
{
[Fact]
public void ContainsStartandEnd()
{
var segment = new EllipticalArcLineSegment(10, 10, 10, 20, 0, 0, 90, Matrix3x2.Identity);
IReadOnlyList<PointF> points = segment.Flatten().ToArray();
Assert.Equal(10, points[0].X, 5);
Assert.Equal(30, points[0].Y, 5);
Assert.Equal(20, segment.EndPoint.X, 5);
Assert.Equal(10, segment.EndPoint.Y, 5);
}

[Fact]
public void checkZeroRadii()
{
IReadOnlyCollection<PointF> xRadiusZero = new EllipticalArcLineSegment(20, 10, 0, 20, 0, 0, 360, Matrix3x2.Identity).Flatten().ToArray();
IReadOnlyCollection<PointF> yRadiusZero = new EllipticalArcLineSegment(20, 10, 30, 0, 0, 0, 360, Matrix3x2.Identity).Flatten().ToArray();
IReadOnlyCollection<PointF> bothRadiiZero = new EllipticalArcLineSegment(20, 10, 0, 0, 0, 0, 360, Matrix3x2.Identity).Flatten().ToArray();
foreach (PointF point in xRadiusZero)
{
Assert.Equal(20, point.X);
}

foreach (PointF point in yRadiusZero)
{
Assert.Equal(10, point.Y);
}

foreach (PointF point in bothRadiiZero)
{
Assert.Equal(20, point.X);
Assert.Equal(10, point.Y);
}
}
}
}
10 changes: 10 additions & 0 deletions tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ public void AddBezier()
Assert.IsType<Path>(builder.Build());
}

[Fact]
public void AddEllipticArc()
{
var builder = new PathBuilder();

builder.AddEllipticalArc(new PointF(10, 10), 10, 10, 0, 0, 360);

Assert.IsType<Path>(builder.Build());
}

[Fact]
public void DrawLinesOpenFigure()
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 22c2a27

Please sign in to comment.