Skip to content

Commit d3e28a3

Browse files
Push working solution plus notes.
1 parent 93f3475 commit d3e28a3

2 files changed

Lines changed: 101 additions & 13 deletions

File tree

src/ImageSharp.Drawing/Shapes/PathBuilder.cs

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Numerics;
8+
using System.Runtime.CompilerServices;
89

910
namespace SixLabors.ImageSharp.Drawing
1011
{
@@ -278,7 +279,7 @@ public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool larg
278279
// Compute center
279280
matrix.M11 = 1 / radiusX;
280281
matrix.M22 = 1 / radiusY;
281-
matrix = Matrix3x2Extensions.CreateRotationDegrees(-rotation) * matrix;
282+
matrix *= Matrix3x2Extensions.CreateRotationDegrees(-rotation);
282283

283284
var unit1 = Vector2.Transform(start, matrix);
284285
var unit2 = Vector2.Transform(point, matrix);
@@ -294,15 +295,19 @@ public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool larg
294295
}
295296

296297
delta *= scaleFactor;
298+
var deltaXY = new Vector2(-delta.Y, delta.X);
297299
Vector2 scaledCenter = unit1 + unit2;
298300
scaledCenter *= .5F;
299-
scaledCenter += new Vector2(-delta.Y, delta.X);
301+
scaledCenter += deltaXY;
300302
unit1 -= scaledCenter;
301303
unit2 -= scaledCenter;
302304

303305
// Compute θ and Δθ
304306
float theta1 = MathF.Atan2(unit1.Y, unit1.X);
305307
float theta2 = MathF.Atan2(unit2.Y, unit2.X);
308+
309+
// startAngle copied from https://github.com/UkooLabs/SVGSharpie/blob/5f7be977d487d416c4cf62578d6342b799a5c507/src/UkooLabs.SVGSharpie.ImageSharp/Shapes/ArcLineSegment.cs#L160
310+
float startAngle = -GeometryUtilities.RadianToDegree(VectorAngle(Vector2.UnitX, (xy - deltaXY) / new Vector2(radiusX, radiusY)));
306311
float sweepAngle = GeometryUtilities.RadianToDegree(theta2 - theta1);
307312

308313
// Fix the range to −360° < Δθ < 360°
@@ -324,14 +329,95 @@ public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool larg
324329
}
325330

326331
var center = Vector2.Lerp(start, point, .5F);
332+
foreach (ILineSegment item in EllipticArcToBezierCurveInner(start, center, new(radiusX, radiusY), rotation, startAngle, sweepAngle))
333+
{
334+
this.AddSegment(item);
335+
}
336+
337+
return this;
327338

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);
339+
// TODO: Fix this.
340+
// return this.AddEllipticalArc(center, radiusX, radiusY, rotation, startAngle, sweepAngle);
333341
}
334342

343+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
344+
private static float Clamp(float value, float min, float max)
345+
{
346+
if (value > max)
347+
{
348+
return max;
349+
}
350+
351+
if (value < min)
352+
{
353+
return min;
354+
}
355+
356+
return value;
357+
}
358+
359+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
360+
private static float VectorAngle(Vector2 u, Vector2 v)
361+
{
362+
float dot = Vector2.Dot(u, v);
363+
float length = u.Length() * v.Length();
364+
float angle = (float)Math.Acos(Clamp(dot / length, -1, 1)); // floating point precision, slightly over values appear
365+
if (((u.X * v.Y) - (u.X * v.Y)) < 0)
366+
{
367+
angle = -angle;
368+
}
369+
370+
return angle;
371+
}
372+
373+
private static IEnumerable<ILineSegment> EllipticArcToBezierCurveInner(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float deltaAngle)
374+
{
375+
xAngle = GeometryUtilities.DegreeToRadian(xAngle);
376+
startAngle = GeometryUtilities.DegreeToRadian(startAngle);
377+
deltaAngle = GeometryUtilities.DegreeToRadian(deltaAngle);
378+
379+
float s = startAngle;
380+
float e = s + deltaAngle;
381+
bool neg = e < s;
382+
float sign = neg ? -1 : 1;
383+
float remain = Math.Abs(e - s);
384+
385+
Vector2 prev = EllipticArcPoint(center, radius, xAngle, s);
386+
387+
while (remain > 1e-05f)
388+
{
389+
float step = (float)Math.Min(remain, Math.PI / 4);
390+
float signStep = step * sign;
391+
392+
Vector2 p1 = prev;
393+
Vector2 p2 = EllipticArcPoint(center, radius, xAngle, s + signStep);
394+
395+
float alphaT = (float)Math.Tan(signStep / 2);
396+
float alpha = (float)(Math.Sin(signStep) * (Math.Sqrt(4 + (3 * alphaT * alphaT)) - 1) / 3);
397+
Vector2 q1 = p1 + (alpha * EllipticArcDerivative(radius, xAngle, s));
398+
Vector2 q2 = p2 - (alpha * EllipticArcDerivative(radius, xAngle, s + signStep));
399+
400+
yield return new CubicBezierLineSegment(from, q1, q2, p2);
401+
from = p2;
402+
403+
s += signStep;
404+
remain -= step;
405+
prev = p2;
406+
}
407+
}
408+
409+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
410+
private static Vector2 EllipticArcDerivative(Vector2 r, float xAngle, float t)
411+
=> new(
412+
(-r.X * MathF.Cos(xAngle) * MathF.Sin(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Cos(t)),
413+
(-r.X * MathF.Sin(xAngle) * MathF.Sin(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Cos(t)));
414+
415+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
416+
private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, float t)
417+
=> new(
418+
c.X + (r.X * MathF.Cos(xAngle) * MathF.Cos(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Sin(t)),
419+
c.Y + (r.X * MathF.Sin(xAngle) * MathF.Cos(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Sin(t)));
420+
335421
/// <summary>
336422
/// Adds an elliptical arc to the current figure.
337423
/// </summary>

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,23 @@ public void DrawPathClippedOnTop<TPixel>(TestImageProvider<TPixel> provider)
101101
public void DrawPathArcTo<TPixel>(TestImageProvider<TPixel> provider)
102102
where TPixel : unmanaged, IPixel<TPixel>
103103
{
104+
// TODO:
105+
// There's something wrong with EllipticalArcLineSegment.
104106
var pb = new PathBuilder();
105107

106-
// This fails
108+
// This works using code derived from SVGSharpie
107109
pb.MoveTo(new Vector2(50, 50));
108110
pb.ArcTo(20, 50, -72, false, true, new Vector2(200, 200));
109111
IPath path = pb.Build();
110112

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();
113+
// This fails using the same derived properties.
114+
pb = new PathBuilder();
115+
pb.AddEllipticalArc(new(125, 125), 61.218582F, 153.046448F, -72, -38.1334763F, 180);
116+
IPath path2 = pb.Build();
115117

116118
// Filling for the moment for visibility.
117119
provider.VerifyOperation(
118-
image => image.Mutate(x => x.Fill(Color.Black, path)),
120+
image => image.Mutate(x => x.Fill(Color.Black, path).Fill(Color.Red.WithAlpha(.5F), path2)),
119121
appendSourceFileOrDescription: false,
120122
appendPixelTypeToFileName: false);
121123
}

0 commit comments

Comments
 (0)