Skip to content

Commit 93f3475

Browse files
Begin Arc2 implementation
1 parent a4c7d06 commit 93f3475

2 files changed

Lines changed: 151 additions & 12 deletions

File tree

src/ImageSharp.Drawing/Shapes/PathBuilder.cs

Lines changed: 127 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -218,76 +218,191 @@ public PathBuilder AddBezier(PointF startPoint, PointF controlPoint1, PointF con
218218
=> this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint));
219219

220220
/// <summary>
221-
/// Adds an elliptical arc to the current figure
221+
/// <para>
222+
/// Adds an arc to the current figure. The arc curves from the last point to <paramref name="point"/>,
223+
/// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger.
224+
/// </para>
225+
/// <para>
226+
/// Th arc sweep is always less than 360 degrees. The method appends a line
227+
/// to the last point if either radii are zero, or if last point is equal to <paramref name="point"/>.
228+
/// In addition the method scales the radii to fit last point and <paramref name="point"/> if both
229+
/// are greater than zero but too small to describe an arc.
230+
/// </para>
231+
/// </summary>
232+
/// <param name="radiusX">X radius of the ellipsis.</param>
233+
/// <param name="radiusY">Y radius of the ellipsis.</param>
234+
/// <param name="rotation">The rotation along the X-axis; measured in degrees clockwise.</param>
235+
/// <param name="largeArc">Whether to use a larger arc.</param>
236+
/// <param name="sweep">Whether to move the arc clockwise or counter-clockwise.</param>
237+
/// <param name="point">The end point.</param>
238+
/// <returns>The <see cref="PathBuilder"/>.</returns>
239+
public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point)
240+
{
241+
// If rx = 0 or ry = 0 then this arc is treated as a straight line segment
242+
// joining the endpoints.
243+
// http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
244+
if (radiusX == 0 || radiusY == 0)
245+
{
246+
return this.LineTo(point);
247+
}
248+
249+
// If the current point and target point for the arc are identical, it should be treated as a
250+
// zero length path. This ensures continuity in animations.
251+
Vector2 start = this.currentPoint;
252+
if (start == point)
253+
{
254+
return this.LineTo(point);
255+
}
256+
257+
radiusX = MathF.Abs(radiusX);
258+
radiusY = MathF.Abs(radiusY);
259+
260+
// Check if the radii are big enough to draw the arc, scale radii if not.
261+
// http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
262+
Vector2 midPointDistance = (start - point) * .5F;
263+
Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(-rotation);
264+
var xy = Vector2.Transform(midPointDistance, matrix);
265+
266+
float squareRx = radiusX * radiusX;
267+
float squareRy = radiusY * radiusY;
268+
float squareX = xy.X * xy.X;
269+
float squareY = xy.Y * xy.Y;
270+
271+
float radiiScale = (squareX / squareRx) + (squareY / squareRy);
272+
if (radiiScale > 1)
273+
{
274+
radiusX = MathF.Sqrt(radiiScale) * radiusX;
275+
radiusY = MathF.Sqrt(radiiScale) * radiusY;
276+
}
277+
278+
// Compute center
279+
matrix.M11 = 1 / radiusX;
280+
matrix.M22 = 1 / radiusY;
281+
matrix = Matrix3x2Extensions.CreateRotationDegrees(-rotation) * matrix;
282+
283+
var unit1 = Vector2.Transform(start, matrix);
284+
var unit2 = Vector2.Transform(point, matrix);
285+
Vector2 delta = unit2 - unit1;
286+
287+
float dot = Vector2.Dot(delta, delta);
288+
float scaleFactorSquared = MathF.Max((1 / dot) - .25F, 0F);
289+
float scaleFactor = MathF.Sqrt(scaleFactorSquared);
290+
291+
if (largeArc == sweep)
292+
{
293+
scaleFactor = -scaleFactor;
294+
}
295+
296+
delta *= scaleFactor;
297+
Vector2 scaledCenter = unit1 + unit2;
298+
scaledCenter *= .5F;
299+
scaledCenter += new Vector2(-delta.Y, delta.X);
300+
unit1 -= scaledCenter;
301+
unit2 -= scaledCenter;
302+
303+
// Compute θ and Δθ
304+
float theta1 = MathF.Atan2(unit1.Y, unit1.X);
305+
float theta2 = MathF.Atan2(unit2.Y, unit2.X);
306+
float sweepAngle = GeometryUtilities.RadianToDegree(theta2 - theta1);
307+
308+
// Fix the range to −360° < Δθ < 360°
309+
if (!sweep && sweepAngle > 0)
310+
{
311+
sweepAngle -= 360;
312+
}
313+
314+
if (sweep && sweepAngle < 0)
315+
{
316+
sweepAngle += 360;
317+
}
318+
319+
// Skia notes an issue with very small sweep angles.
320+
// Epsilon is based upon their fix.
321+
if (MathF.Abs(sweepAngle) < 0.001F)
322+
{
323+
return this.LineTo(point);
324+
}
325+
326+
var center = Vector2.Lerp(start, point, .5F);
327+
328+
// TODO: This is wrong and is the source of our problems when we use uneven radii.
329+
// The rotation parameter does not appear to serve as the correct option either.
330+
// Maybe passing both in the correct form is required?
331+
float startAngle = GeometryUtilities.RadianToDegree(MathF.Atan2(start.Y - point.Y, start.X - point.X));
332+
return this.AddEllipticalArc(center, radiusX, radiusY, 0, startAngle, sweepAngle);
333+
}
334+
335+
/// <summary>
336+
/// Adds an elliptical arc to the current figure.
222337
/// </summary>
223338
/// <param name="rect"> A <see cref="RectangleF"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
224339
/// <param name="rotation">The rotation of (<paramref name="rect"/>, measured in degrees clockwise.</param>
225-
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
340+
/// <param name="startAngle">The start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
226341
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
227342
/// <returns>The <see cref="PathBuilder"/></returns>
228343
public PathBuilder AddEllipticalArc(RectangleF rect, float rotation, float startAngle, float sweepAngle)
229344
=> this.AddEllipticalArc((rect.Right + rect.Left) / 2, (rect.Bottom + rect.Top) / 2, rect.Width / 2, rect.Height / 2, rotation, startAngle, sweepAngle);
230345

231346
/// <summary>
232-
/// Adds an elliptical arc to the current figure
347+
/// Adds an elliptical arc to the current figure.
233348
/// </summary>
234349
/// <param name="rect"> A <see cref="Rectangle"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
235350
/// <param name="rotation">The rotation of (<paramref name="rect"/>, measured in degrees clockwise.</param>
236-
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
351+
/// <param name="startAngle">The start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
237352
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
238353
/// <returns>The <see cref="PathBuilder"/></returns>
239354
public PathBuilder AddEllipticalArc(Rectangle rect, int rotation, int startAngle, int sweepAngle)
240355
=> 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);
241356

242357
/// <summary>
243-
/// Adds an elliptical arc to the current figure
358+
/// Adds an elliptical arc to the current figure.
244359
/// </summary>
245360
/// <param name="center"> The center <see cref="PointF"/> of the ellips from which the arc is taken.</param>
246361
/// <param name="radiusX">X radius of the ellipsis.</param>
247362
/// <param name="radiusY">Y radius of the ellipsis.</param>
248363
/// <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>
249-
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
364+
/// <param name="startAngle">The start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
250365
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
251366
/// <returns>The <see cref="PathBuilder"/></returns>
252367
public PathBuilder AddEllipticalArc(PointF center, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)
253368
=> this.AddEllipticalArc(center.X, center.Y, radiusX, radiusY, rotation, startAngle, sweepAngle);
254369

255370
/// <summary>
256-
/// Adds an elliptical arc to the current figure
371+
/// Adds an elliptical arc to the current figure.
257372
/// </summary>
258373
/// <param name="center"> The center <see cref="Point"/> of the ellips from which the arc is taken.</param>
259374
/// <param name="radiusX">X radius of the ellipsis.</param>
260375
/// <param name="radiusY">Y radius of the ellipsis.</param>
261376
/// <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>
262-
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
377+
/// <param name="startAngle">The start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
263378
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
264379
/// <returns>The <see cref="PathBuilder"/></returns>
265380
public PathBuilder AddEllipticalArc(Point center, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle)
266381
=> this.AddEllipticalArc(center.X, center.Y, radiusX, radiusY, rotation, startAngle, sweepAngle);
267382

268383
/// <summary>
269-
/// Adds an elliptical arc to the current figure
384+
/// Adds an elliptical arc to the current figure.
270385
/// </summary>
271386
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
272387
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
273388
/// <param name="radiusX">X radius of the ellipsis.</param>
274389
/// <param name="radiusY">Y radius of the ellipsis.</param>
275390
/// <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>
276-
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
391+
/// <param name="startAngle">The start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
277392
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
278393
/// <returns>The <see cref="PathBuilder"/></returns>
279394
public PathBuilder AddEllipticalArc(int x, int y, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle)
280395
=> this.AddSegment(new EllipticalArcLineSegment(x, y, radiusX, radiusY, rotation, startAngle, sweepAngle, Matrix3x2.Identity));
281396

282397
/// <summary>
283-
/// Adds an elliptical arc to the current figure
398+
/// Adds an elliptical arc to the current figure.
284399
/// </summary>
285400
/// <param name="x"> The x-coordinate of the center point of the ellips from which the arc is taken.</param>
286401
/// <param name="y"> The y-coordinate of the center point of the ellips from which the arc is taken.</param>
287402
/// <param name="radiusX">X radius of the ellipsis.</param>
288403
/// <param name="radiusY">Y radius of the ellipsis.</param>
289404
/// <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>
290-
/// <param name="startAngle">The Start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
405+
/// <param name="startAngle">The start angle of the ellipsis, measured in degrees anticlockwise from the Y-axis.</param>
291406
/// <param name="sweepAngle"> The angle between (<paramref name="startAngle"/> and the end of the arc. </param>
292407
/// <returns>The <see cref="PathBuilder"/></returns>
293408
public PathBuilder AddEllipticalArc(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,29 @@ public void DrawPathClippedOnTop<TPixel>(TestImageProvider<TPixel> provider)
9595
appendSourceFileOrDescription: false,
9696
appendPixelTypeToFileName: false);
9797
}
98+
99+
[Theory]
100+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32)]
101+
public void DrawPathArcTo<TPixel>(TestImageProvider<TPixel> provider)
102+
where TPixel : unmanaged, IPixel<TPixel>
103+
{
104+
var pb = new PathBuilder();
105+
106+
// This fails
107+
pb.MoveTo(new Vector2(50, 50));
108+
pb.ArcTo(20, 50, -72, false, true, new Vector2(200, 200));
109+
IPath path = pb.Build();
110+
111+
// This works.
112+
//pb.MoveTo(new Vector2(50, 50));
113+
//pb.ArcTo(20, 20, -72, true, true, new Vector2(200, 200));
114+
//IPath path = pb.Build();
115+
116+
// Filling for the moment for visibility.
117+
provider.VerifyOperation(
118+
image => image.Mutate(x => x.Fill(Color.Black, path)),
119+
appendSourceFileOrDescription: false,
120+
appendPixelTypeToFileName: false);
121+
}
98122
}
99123
}

0 commit comments

Comments
 (0)