Skip to content

Commit 22c2a27

Browse files
authored
Merge pull request #144 from derBobo/master
Implenting addArc feature
2 parents d31cc33 + 0087497 commit 22c2a27

10 files changed

Lines changed: 311 additions & 2 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Numerics;
7+
8+
namespace SixLabors.ImageSharp.Drawing
9+
{
10+
/// <summary>
11+
/// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc
12+
/// </summary>
13+
/// <seealso cref="ILineSegment" />
14+
public sealed class EllipticalArcLineSegment : ILineSegment
15+
{
16+
private const float MinimumSqrDistance = 1.75f;
17+
private readonly PointF[] linePoints;
18+
private readonly float x;
19+
private readonly float y;
20+
private readonly float radiusX;
21+
private readonly float radiusY;
22+
private readonly float rotation;
23+
private readonly float startAngle;
24+
private readonly float sweepAngle;
25+
private readonly Matrix3x2 transformation;
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="EllipticalArcLineSegment"/> class.
29+
/// </summary>
30+
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
31+
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
32+
/// <param name="radiusX">X radius of the ellipsis.</param>
33+
/// <param name="radiusY">Y radius of the ellipsis.</param>
34+
/// <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>
35+
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
36+
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
37+
/// <param name="transformation">The Tranformation matrix, that should be used on the arc.</param>
38+
public EllipticalArcLineSegment(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle, Matrix3x2 transformation)
39+
{
40+
Guard.MustBeGreaterThanOrEqualTo(radiusX, 0, nameof(radiusX));
41+
Guard.MustBeGreaterThanOrEqualTo(radiusY, 0, nameof(radiusY));
42+
this.x = x;
43+
this.y = y;
44+
this.radiusX = radiusX;
45+
this.radiusY = radiusY;
46+
this.rotation = rotation % 360;
47+
this.startAngle = startAngle % 360;
48+
this.transformation = transformation;
49+
this.sweepAngle = sweepAngle;
50+
if (sweepAngle > 360)
51+
{
52+
this.sweepAngle = 360;
53+
}
54+
55+
if (sweepAngle < -360)
56+
{
57+
this.sweepAngle = -360;
58+
}
59+
60+
this.linePoints = this.GetDrawingPoints();
61+
this.EndPoint = this.linePoints[this.linePoints.Length - 1];
62+
}
63+
64+
/// <summary>
65+
/// Gets the end point.
66+
/// </summary>
67+
/// <value>
68+
/// The end point.
69+
/// </value>
70+
public PointF EndPoint { get; }
71+
72+
/// <summary>
73+
/// Transforms the current LineSegment using specified matrix.
74+
/// </summary>
75+
/// <param name="matrix">The transformation matrix.</param>
76+
/// <returns>A line segment with the matrix applied to it.</returns>
77+
public EllipticalArcLineSegment Transform(Matrix3x2 matrix)
78+
{
79+
if (matrix.IsIdentity)
80+
{
81+
return this;
82+
}
83+
84+
return new EllipticalArcLineSegment(this.x, this.y, this.radiusX, this.radiusY, this.rotation, this.startAngle, this.sweepAngle, Matrix3x2.Multiply(this.transformation, matrix));
85+
}
86+
87+
/// <summary>
88+
/// Transforms the current LineSegment using specified matrix.
89+
/// </summary>
90+
/// <param name="matrix">The matrix.</param>
91+
/// <returns>A line segment with the matrix applied to it.</returns>
92+
ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix);
93+
94+
private PointF[] GetDrawingPoints()
95+
{
96+
var points = new List<PointF>()
97+
{
98+
this.CalculatePoint(this.startAngle)
99+
};
100+
if (this.sweepAngle < 0)
101+
{
102+
for (float i = this.startAngle; i > this.startAngle + this.sweepAngle; i--)
103+
{
104+
float end = i - 1;
105+
if (end <= this.startAngle + this.sweepAngle)
106+
{
107+
end = this.startAngle + this.sweepAngle;
108+
}
109+
110+
points.AddRange(this.GetDrawingPoints(i, end, 0));
111+
}
112+
}
113+
else
114+
{
115+
for (float i = this.startAngle; i < this.startAngle + this.sweepAngle; i++)
116+
{
117+
float end = i + 1;
118+
if (end >= this.startAngle + this.sweepAngle)
119+
{
120+
end = this.startAngle + this.sweepAngle;
121+
}
122+
123+
points.AddRange(this.GetDrawingPoints(i, end, 0));
124+
}
125+
}
126+
127+
return points.ToArray();
128+
}
129+
130+
private List<PointF> GetDrawingPoints(float start, float end, int depth)
131+
{
132+
if (depth > 1000)
133+
{
134+
return new List<PointF>();
135+
}
136+
137+
var points = new List<PointF>();
138+
139+
PointF startP = this.CalculatePoint(start);
140+
PointF endP = this.CalculatePoint(end);
141+
if ((new Vector2(endP.X, endP.Y) - new Vector2(startP.X, startP.Y)).LengthSquared() < MinimumSqrDistance)
142+
{
143+
points.Add(endP);
144+
}
145+
else
146+
{
147+
float mid = start + ((end - start) / 2);
148+
points.AddRange(this.GetDrawingPoints(start, mid, depth + 1));
149+
points.AddRange(this.GetDrawingPoints(mid, end, depth + 1));
150+
}
151+
152+
return points;
153+
}
154+
155+
private PointF CalculatePoint(float angle)
156+
{
157+
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;
158+
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;
159+
var currPoint = new PointF(x, y);
160+
return PointF.Transform(currPoint, this.transformation);
161+
}
162+
163+
/// <summary>
164+
/// Returns the current <see cref="ILineSegment" /> a simple linear path.
165+
/// </summary>
166+
/// <returns>
167+
/// Returns the current <see cref="ILineSegment" /> as simple linear path.
168+
/// </returns>
169+
public ReadOnlyMemory<PointF> Flatten() => this.linePoints;
170+
}
171+
}

src/ImageSharp.Drawing/Shapes/PathBuilder.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,86 @@ public PathBuilder AddBezier(PointF startPoint, PointF controlPoint1, PointF con
199199
return this;
200200
}
201201

202+
/// <summary>
203+
/// Adds an elliptical arc to the current figure
204+
/// </summary>
205+
/// <param name="rect"> A <see cref="RectangleF"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
206+
/// <param name="rotation">The rotation of (<paramref name="rect"/>, measured in degrees clockwise.</param>
207+
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
208+
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
209+
/// <returns>The <see cref="PathBuilder"/></returns>
210+
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);
211+
212+
/// <summary>
213+
/// Adds an elliptical arc to the current figure
214+
/// </summary>
215+
/// <param name="rect"> A <see cref="Rectangle"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
216+
/// <param name="rotation">The rotation of (<paramref name="rect"/>, measured in degrees clockwise.</param>
217+
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
218+
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
219+
/// <returns>The <see cref="PathBuilder"/></returns>
220+
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);
221+
222+
/// <summary>
223+
/// Adds an elliptical arc to the current figure
224+
/// </summary>
225+
/// <param name="center"> The center <see cref="PointF"/> of the ellips from which the arc is taken.</param>
226+
/// <param name="radiusX">X radius of the ellipsis.</param>
227+
/// <param name="radiusY">Y radius of the ellipsis.</param>
228+
/// <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>
229+
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
230+
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
231+
/// <returns>The <see cref="PathBuilder"/></returns>
232+
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);
233+
234+
/// <summary>
235+
/// Adds an elliptical arc to the current figure
236+
/// </summary>
237+
/// <param name="center"> The center <see cref="Point"/> of the ellips from which the arc is taken.</param>
238+
/// <param name="radiusX">X radius of the ellipsis.</param>
239+
/// <param name="radiusY">Y radius of the ellipsis.</param>
240+
/// <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>
241+
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
242+
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
243+
/// <returns>The <see cref="PathBuilder"/></returns>
244+
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);
245+
246+
/// <summary>
247+
/// Adds an elliptical arc to the current figure
248+
/// </summary>
249+
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
250+
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
251+
/// <param name="radiusX">X radius of the ellipsis.</param>
252+
/// <param name="radiusY">Y radius of the ellipsis.</param>
253+
/// <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>
254+
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
255+
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
256+
/// <returns>The <see cref="PathBuilder"/></returns>
257+
public PathBuilder AddEllipticalArc(int x, int y, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle)
258+
{
259+
this.currentFigure.AddSegment(new EllipticalArcLineSegment(x, y, radiusX, radiusY, rotation, startAngle, sweepAngle, this.currentTransform));
260+
261+
return this;
262+
}
263+
264+
/// <summary>
265+
/// Adds an elliptical arc to the current figure
266+
/// </summary>
267+
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
268+
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
269+
/// <param name="radiusX">X radius of the ellipsis.</param>
270+
/// <param name="radiusY">Y radius of the ellipsis.</param>
271+
/// <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>
272+
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
273+
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
274+
/// <returns>The <see cref="PathBuilder"/></returns>
275+
public PathBuilder AddEllipticalArc(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)
276+
{
277+
this.currentFigure.AddSegment(new EllipticalArcLineSegment(x, y, radiusX, radiusY, rotation, startAngle, sweepAngle, this.currentTransform));
278+
279+
return this;
280+
}
281+
202282
/// <summary>
203283
/// Starts a new figure but leaves the previous one open.
204284
/// </summary>

tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class DrawPathTests
2323
};
2424

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

40-
var path = new Path(linearSegment, bezierSegment);
42+
var path = new Path(linearSegment, bezierSegment, ellipticArcSegment1, ellipticArcSegment2);
4143

4244
Rgba32 rgba = TestUtils.GetColorByName(colorName);
4345
rgba.A = alpha;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System.Collections.Generic;
5+
using System.Numerics;
6+
using Xunit;
7+
8+
namespace SixLabors.ImageSharp.Drawing.Tests
9+
{
10+
public class EllipticalArcLineSegmentTest
11+
{
12+
[Fact]
13+
public void ContainsStartandEnd()
14+
{
15+
var segment = new EllipticalArcLineSegment(10, 10, 10, 20, 0, 0, 90, Matrix3x2.Identity);
16+
IReadOnlyList<PointF> points = segment.Flatten().ToArray();
17+
Assert.Equal(10, points[0].X, 5);
18+
Assert.Equal(30, points[0].Y, 5);
19+
Assert.Equal(20, segment.EndPoint.X, 5);
20+
Assert.Equal(10, segment.EndPoint.Y, 5);
21+
}
22+
23+
[Fact]
24+
public void checkZeroRadii()
25+
{
26+
IReadOnlyCollection<PointF> xRadiusZero = new EllipticalArcLineSegment(20, 10, 0, 20, 0, 0, 360, Matrix3x2.Identity).Flatten().ToArray();
27+
IReadOnlyCollection<PointF> yRadiusZero = new EllipticalArcLineSegment(20, 10, 30, 0, 0, 0, 360, Matrix3x2.Identity).Flatten().ToArray();
28+
IReadOnlyCollection<PointF> bothRadiiZero = new EllipticalArcLineSegment(20, 10, 0, 0, 0, 0, 360, Matrix3x2.Identity).Flatten().ToArray();
29+
foreach (PointF point in xRadiusZero)
30+
{
31+
Assert.Equal(20, point.X);
32+
}
33+
34+
foreach (PointF point in yRadiusZero)
35+
{
36+
Assert.Equal(10, point.Y);
37+
}
38+
39+
foreach (PointF point in bothRadiiZero)
40+
{
41+
Assert.Equal(20, point.X);
42+
Assert.Equal(10, point.Y);
43+
}
44+
}
45+
}
46+
}

tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ public void AddBezier()
3232
Assert.IsType<Path>(builder.Build());
3333
}
3434

35+
[Fact]
36+
public void AddEllipticArc()
37+
{
38+
var builder = new PathBuilder();
39+
40+
builder.AddEllipticalArc(new PointF(10, 10), 10, 10, 0, 0, 360);
41+
42+
Assert.IsType<Path>(builder.Build());
43+
}
44+
3545
[Fact]
3646
public void DrawLinesOpenFigure()
3747
{
-4.87 KB
Loading
-8.29 KB
Loading
-4.58 KB
Loading
-4.44 KB
Loading
-4.73 KB
Loading

0 commit comments

Comments
 (0)