Skip to content

Commit d3ae3c0

Browse files
committed
wip - missing arc cleanup
1 parent a4c7d06 commit d3ae3c0

2 files changed

Lines changed: 324 additions & 0 deletions

File tree

src/ImageSharp.Drawing/Shapes/Path.cs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Linq;
78
using System.Numerics;
89

@@ -116,5 +117,240 @@ SegmentInfo IPathInternals.PointAlongPath(float distance)
116117

117118
/// <inheritdoc/>
118119
IReadOnlyList<InternalPath> IInternalPathOwner.GetRingsAsInternalPath() => new[] { this.InnerPath };
120+
121+
public static bool TryParseSvgPath(ReadOnlySpan<char> data, out IPath value)
122+
{
123+
value = null;
124+
125+
var builder = new PathBuilder();
126+
127+
//parse svg
128+
PointF first = PointF.Empty;
129+
PointF c = PointF.Empty;
130+
PointF lastc = PointF.Empty;
131+
// stackalloc ???
132+
var points = new PointF[3].AsSpan();
133+
134+
char op = '\0';
135+
char previousOp = '\0';
136+
bool relative = false;
137+
while (true)
138+
{
139+
data = data.TrimStart();
140+
if (data.Length == 0)
141+
{
142+
break;
143+
}
144+
145+
char ch = data[0];
146+
if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.')
147+
{
148+
// are we are the end of the string or we are at the end of the path
149+
if (data.Length == 0 || op == 'Z')
150+
{
151+
return false;
152+
}
153+
}
154+
else if (IsSeperator(ch))
155+
{
156+
data = TrimSeperator(data);
157+
}
158+
else
159+
{
160+
op = ch;
161+
relative = false;
162+
if (char.IsLower(op))
163+
{
164+
op = char.ToUpper(op);
165+
relative = true;
166+
}
167+
168+
data = TrimSeperator(data.Slice(1));
169+
}
170+
switch (op)
171+
{
172+
case 'M':
173+
data = FindPoints(data, points, 1, relative, c);
174+
builder.MoveTo(points[0]);
175+
previousOp = '\0';
176+
op = 'L';
177+
c = points[0];
178+
break;
179+
case 'L':
180+
data = FindPoints(data, points, 1, relative, c);
181+
builder.LineTo(points[0]);
182+
c = points[0];
183+
break;
184+
case 'H':
185+
{
186+
data = FindScaler(data, out float x);
187+
if (relative)
188+
{
189+
x += c.X;
190+
}
191+
192+
builder.LineTo(x, c.Y);
193+
c.X = x;
194+
}
195+
196+
break;
197+
case 'V':
198+
{
199+
data = FindScaler(data, out float y);
200+
if (relative)
201+
{
202+
y += c.Y;
203+
}
204+
205+
builder.LineTo(c.X, y);
206+
c.Y = y;
207+
}
208+
break;
209+
case 'C':
210+
data = FindPoints(data, points, 3, relative, c);
211+
builder.CubicBezierTo(points[0], points[1], points[2]);
212+
lastc = points[1];
213+
c = points[2];
214+
break;
215+
case 'S':
216+
data = FindPoints(data, points, 2, relative, c);
217+
points[0] = c;
218+
if (previousOp == 'C' || previousOp == 'S')
219+
{
220+
points[0].X -= lastc.X - c.X;
221+
points[0].Y -= lastc.Y - c.Y;
222+
}
223+
builder.CubicBezierTo(points[0], points[1], points[2]);
224+
lastc = points[1];
225+
c = points[2];
226+
break;
227+
case 'Q': // Quadratic Bezier Curve
228+
data = FindPoints(data, points, 2, relative, c);
229+
builder.QuadraticBezierTo(points[0], points[1]);
230+
lastc = points[0];
231+
c = points[1];
232+
break;
233+
case 'T':
234+
data = FindPoints(data, points.Slice(1), 1, relative, c);
235+
points[0] = c;
236+
if (previousOp is 'Q' or 'T')
237+
{
238+
points[0].X -= lastc.X - c.X;
239+
points[0].Y -= lastc.Y - c.Y;
240+
}
241+
242+
builder.QuadraticBezierTo(points[0], points[1]);
243+
lastc = points[0];
244+
c = points[1];
245+
break;
246+
case 'A':
247+
{
248+
data = FindScaler(data, out float radiiX);
249+
data = TrimSeperator(data);
250+
data = FindScaler(data, out float radiiY);
251+
data = TrimSeperator(data);
252+
data = FindScaler(data, out float angle);
253+
data = TrimSeperator(data);
254+
data = FindScaler(data, out float largeArc);
255+
data = TrimSeperator(data);
256+
data = FindScaler(data, out float sweep);
257+
258+
data = FindPoint(data, out var point, relative, c);
259+
if (data.Length > 0)
260+
{
261+
builder.ArcTo(radiiX, radiiY, angle, largeArc == 1, sweep == 1, point);
262+
c = point;
263+
}
264+
}
265+
break;
266+
case 'Z':
267+
builder.CloseFigure();
268+
c = first;
269+
break;
270+
case '~':
271+
{
272+
SkPoint args[2];
273+
data = find_points(data, args, 2, false, nullptr);
274+
path.moveTo(args[0].fX, args[0].fY);
275+
path.lineTo(args[1].fX, args[1].fY);
276+
}
277+
break;
278+
default:
279+
return false;
280+
}
281+
if (previousOp == 0)
282+
{
283+
first = c;
284+
}
285+
previousOp = op;
286+
}
287+
288+
return true;
289+
290+
static bool IsSeperator(char ch)
291+
=> char.IsWhiteSpace(ch) || ch == ',';
292+
293+
static ReadOnlySpan<char> TrimSeperator(ReadOnlySpan<char> data)
294+
{
295+
if (data.Length == 0)
296+
{
297+
return data;
298+
}
299+
300+
int idx = 0;
301+
for (; idx < data.Length; idx++)
302+
{
303+
if (!IsSeperator(data[idx]))
304+
{
305+
break;
306+
}
307+
}
308+
309+
return data.Slice(idx);
310+
}
311+
312+
313+
static ReadOnlySpan<char> FindPoint(ReadOnlySpan<char> str, out PointF value, bool isRelative, in PointF relative)
314+
{
315+
str = FindScaler(str, out float x);
316+
str = FindScaler(str, out float y);
317+
if (isRelative)
318+
{
319+
x += relative.X;
320+
y += relative.Y;
321+
}
322+
323+
value = new PointF(x, y);
324+
return str;
325+
}
326+
327+
static ReadOnlySpan<char> FindPoints(ReadOnlySpan<char> str, Span<PointF> value, int count, bool isRelative, in PointF relative)
328+
{
329+
for (int i = 0; i < value.Length && i < count; i++)
330+
{
331+
str = FindPoint(str, out value[i], isRelative, relative);
332+
}
333+
334+
return str;
335+
}
336+
337+
static ReadOnlySpan<char> FindScaler(ReadOnlySpan<char> str, out float scaler)
338+
{
339+
str = str.TrimStart();
340+
scaler = 0;
341+
342+
for (var i = 0; i < str.Length; i++)
343+
{
344+
if (IsSeperator(str[i]))
345+
{
346+
scaler = float.Parse(str.Slice(0, i));
347+
str = str.Slice(i);
348+
}
349+
}
350+
351+
// we concumed eveything
352+
return ReadOnlySpan<char>.Empty;
353+
}
354+
}
119355
}
120356
}

src/ImageSharp.Drawing/Shapes/PathBuilder.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ public void MoveTo(Vector2 point)
108108
public PathBuilder LineTo(PointF point)
109109
=> this.AddLine(this.currentPoint, point);
110110

111+
/// <summary>
112+
/// Draws the line connecting the current the current point to the new point.
113+
/// </summary>
114+
/// <param name="x">The x.</param>
115+
/// <param name="y">The y.</param>
116+
/// <returns>The <see cref="PathBuilder"/></returns>
117+
public PathBuilder LineTo(float x, float y)
118+
=> this.LineTo(new PointF(x, y));
119+
111120
/// <summary>
112121
/// Adds the line connecting the current point to the new point.
113122
/// </summary>
@@ -217,6 +226,85 @@ public PathBuilder AddBezier(PointF startPoint, PointF controlPoint, PointF endP
217226
public PathBuilder AddBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint)
218227
=> this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint));
219228

229+
public PathBuilder ArcTo(float rx/*radiusX*/, float ry/*radiusY*/, float phi/*rotation*/, bool fa /*largeArc*/, bool fs /*sweep*/, PointF point)
230+
{
231+
var x1 = currentPoint.X;
232+
var y1 = currentPoint.Y;
233+
var x2 = point.X;
234+
var y2 = point.Y;
235+
236+
static float pow(float n) => MathF.Pow(n, 2);
237+
static float vectorAngle(float ux, float uy, float vx, float vy)
238+
{
239+
var sign = ux * vy - uy * vx < 0 ? -1 : 1;
240+
var ua = MathF.Sqrt(ux * ux + uy * uy);
241+
var va = MathF.Sqrt(vx * vx + vy * vy);
242+
var dot = ux * vx + uy * vy;
243+
244+
return sign * MathF.Acos(dot / (ua * va));
245+
}
246+
247+
static float deg(float rad) => rad * 180 / MathF.PI;
248+
249+
var sinphi = MathF.Sin(phi);
250+
var cosphi = MathF.Cos(phi);
251+
252+
// Step 1: simplify through translation/rotation
253+
var x = cosphi * (x1 - x2) / 2 + sinphi * (y1 - y2) / 2;
254+
var y = -sinphi * (x1 - x2) / 2 + cosphi * (y1 - y2) / 2;
255+
256+
var px = pow(x);
257+
var py = pow(y);
258+
var prx = pow(rx);
259+
var pry = pow(ry);
260+
261+
// correct of out-of-range radii
262+
var L = px / prx + py / pry;
263+
264+
if (L > 1)
265+
{
266+
rx = MathF.Sqrt(L) * MathF.Abs(rx);
267+
ry = MathF.Sqrt(L) * MathF.Abs(ry);
268+
}
269+
else
270+
{
271+
rx = MathF.Abs(rx);
272+
ry = MathF.Abs(ry);
273+
}
274+
275+
// Step 2 + 3: compute center
276+
var sign = fa == fs ? -1 : 1;
277+
var M = MathF.Sqrt((prx * pry - prx * py - pry * px) / (prx * py + pry * px)) * sign;
278+
279+
var _cx = M * (rx * y) / ry;
280+
var _cy = M * (-ry * x) / rx;
281+
282+
var cx = cosphi * _cx - sinphi * _cy + (x1 + x2) / 2;
283+
var cy = sinphi * _cx + cosphi * _cy + (y1 + y2) / 2;
284+
285+
// Step 4: compute θ and dθ
286+
var theta = vectorAngle(1, 0, (x - _cx) / rx, (y - _cy) / ry);
287+
288+
var _dTheta = deg(vectorAngle(
289+
(x - _cx) / rx,
290+
(y - _cy) / ry,
291+
(-x - _cx) / rx,
292+
(-y - _cy) / ry
293+
)) % 360;
294+
295+
if (!fs && _dTheta > 0)
296+
{
297+
_dTheta -= 360;
298+
}
299+
300+
if (fs && _dTheta < 0)
301+
{
302+
_dTheta += 360;
303+
}
304+
305+
return this.AddEllipticalArc(new PointF(cx, cy), rx, ry, 0, phi, _dTheta);
306+
}
307+
220308
/// <summary>
221309
/// Adds an elliptical arc to the current figure
222310
/// </summary>

0 commit comments

Comments
 (0)