Skip to content

Commit f0fe9d9

Browse files
Refactor WebGPU encoder and linearize path model
1 parent 3e512d5 commit f0fe9d9

28 files changed

Lines changed: 4075 additions & 2265 deletions

src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs

Lines changed: 562 additions & 51 deletions
Large diffs are not rendered by default.

src/ImageSharp.Drawing/ArcLineSegment.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool
4444
{
4545
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
4646
}
47+
48+
this.Bounds = CalculateBounds(this.linePoints);
4749
}
4850

4951
/// <summary>
@@ -78,16 +80,39 @@ public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAn
7880
{
7981
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
8082
}
83+
84+
this.Bounds = CalculateBounds(this.linePoints);
85+
}
86+
87+
private ArcLineSegment(PointF[] linePoints)
88+
{
89+
this.linePoints = linePoints;
90+
this.Bounds = CalculateBounds(linePoints);
8191
}
8292

83-
private ArcLineSegment(PointF[] linePoints) => this.linePoints = linePoints;
93+
/// <inheritdoc/>
94+
public PointF StartPoint => this.linePoints[0];
8495

8596
/// <inheritdoc/>
8697
public PointF EndPoint => this.linePoints[^1];
8798

99+
/// <inheritdoc />
100+
public RectangleF Bounds { get; }
101+
102+
/// <inheritdoc />
103+
public int LinearVertexCount => this.linePoints.Length;
104+
88105
/// <inheritdoc/>
89106
public ReadOnlyMemory<PointF> Flatten() => this.linePoints;
90107

108+
/// <inheritdoc />
109+
public void CopyTo(Span<PointF> destination, bool skipFirstPoint)
110+
{
111+
int startIndex = skipFirstPoint ? 1 : 0;
112+
113+
this.linePoints.AsSpan(startIndex).CopyTo(destination);
114+
}
115+
91116
/// <summary>
92117
/// Transforms the current <see cref="ArcLineSegment"/> using specified matrix.
93118
/// </summary>
@@ -112,6 +137,28 @@ public ILineSegment Transform(Matrix4x4 matrix)
112137
/// <inheritdoc/>
113138
ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix);
114139

140+
/// <summary>
141+
/// Computes the bounds for the retained linearized arc points.
142+
/// </summary>
143+
private static RectangleF CalculateBounds(ReadOnlySpan<PointF> points)
144+
{
145+
float minX = float.MaxValue;
146+
float minY = float.MaxValue;
147+
float maxX = float.MinValue;
148+
float maxY = float.MinValue;
149+
150+
for (int i = 0; i < points.Length; i++)
151+
{
152+
PointF point = points[i];
153+
minX = MathF.Min(minX, point.X);
154+
minY = MathF.Min(minY, point.Y);
155+
maxX = MathF.Max(maxX, point.X);
156+
maxY = MathF.Max(maxY, point.Y);
157+
}
158+
159+
return RectangleF.FromLTRB(minX, minY, maxX, maxY);
160+
}
161+
115162
private static PointF[] EllipticArcFromEndParams(
116163
PointF from,
117164
PointF to,

src/ImageSharp.Drawing/ComplexPolygon.cs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public sealed class ComplexPolygon : IPath, IPathInternals, IInternalPathOwner
1717
private List<InternalPath>? internalPaths;
1818
private float length;
1919
private RectangleF? bounds;
20+
private IPath? closedPath;
21+
private LinearGeometry? linearGeometry;
2022

2123
/// <summary>
2224
/// Initializes a new instance of the <see cref="ComplexPolygon"/> class.
@@ -97,6 +99,98 @@ public IEnumerable<ISimplePath> Flatten()
9799
return paths;
98100
}
99101

102+
/// <inheritdoc />
103+
public LinearGeometry ToLinearGeometry()
104+
{
105+
if (this.linearGeometry is not null)
106+
{
107+
return this.linearGeometry;
108+
}
109+
110+
int pointCount = 0;
111+
int contourCount = 0;
112+
int segmentCount = 0;
113+
114+
bool hasBounds = false;
115+
float minX = float.MaxValue;
116+
float minY = float.MaxValue;
117+
float maxX = float.MinValue;
118+
float maxY = float.MinValue;
119+
120+
foreach (IPath path in this.paths)
121+
{
122+
LinearGeometry geometry = path.ToLinearGeometry();
123+
124+
if (geometry.Info.PointCount == 0)
125+
{
126+
continue;
127+
}
128+
129+
RectangleF childBounds = geometry.Info.Bounds;
130+
minX = MathF.Min(minX, childBounds.Left);
131+
minY = MathF.Min(minY, childBounds.Top);
132+
maxX = MathF.Max(maxX, childBounds.Right);
133+
maxY = MathF.Max(maxY, childBounds.Bottom);
134+
hasBounds = true;
135+
136+
pointCount += geometry.Info.PointCount;
137+
contourCount += geometry.Info.ContourCount;
138+
segmentCount += geometry.Info.SegmentCount;
139+
}
140+
141+
PointF[] points = new PointF[pointCount];
142+
LinearContour[] contours = new LinearContour[contourCount];
143+
int pointStart = 0;
144+
int contourStart = 0;
145+
int segmentStart = 0;
146+
147+
foreach (IPath path in this.paths)
148+
{
149+
LinearGeometry geometry = path.ToLinearGeometry();
150+
if (geometry.Info.PointCount == 0)
151+
{
152+
continue;
153+
}
154+
155+
for (int i = 0; i < geometry.Points.Count; i++)
156+
{
157+
points[pointStart + i] = geometry.Points[i];
158+
}
159+
160+
for (int i = 0; i < geometry.Contours.Count; i++)
161+
{
162+
LinearContour contour = geometry.Contours[i];
163+
contours[contourStart + i] = new LinearContour
164+
{
165+
PointStart = pointStart + contour.PointStart,
166+
PointCount = contour.PointCount,
167+
SegmentStart = segmentStart + contour.SegmentStart,
168+
SegmentCount = contour.SegmentCount,
169+
IsClosed = contour.IsClosed
170+
};
171+
}
172+
173+
pointStart += geometry.Info.PointCount;
174+
contourStart += geometry.Info.ContourCount;
175+
segmentStart += geometry.Info.SegmentCount;
176+
}
177+
178+
RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty;
179+
180+
this.linearGeometry = new LinearGeometry(
181+
new LinearGeometryInfo
182+
{
183+
Bounds = bounds,
184+
ContourCount = contours.Length,
185+
PointCount = points.Length,
186+
SegmentCount = segmentCount
187+
},
188+
contours,
189+
points);
190+
191+
return this.linearGeometry;
192+
}
193+
100194
/// <inheritdoc/>
101195
public IPath AsClosedPath()
102196
{
@@ -105,13 +199,19 @@ public IPath AsClosedPath()
105199
return this;
106200
}
107201

202+
if (this.closedPath is not null)
203+
{
204+
return this.closedPath;
205+
}
206+
108207
IPath[] paths = new IPath[this.paths.Length];
109208
for (int i = 0; i < this.paths.Length; i++)
110209
{
111210
paths[i] = this.paths[i].AsClosedPath();
112211
}
113212

114-
return new ComplexPolygon(paths);
213+
this.closedPath = new ComplexPolygon(paths);
214+
return this.closedPath;
115215
}
116216

117217
/// <inheritdoc/>

src/ImageSharp.Drawing/CubicBezierLineSegment.cs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,12 @@ namespace SixLabors.ImageSharp.Drawing;
1212
/// <seealso cref="ILineSegment" />
1313
public sealed class CubicBezierLineSegment : ILineSegment
1414
{
15-
// code for this taken from <see href="http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/"/>
15+
// Code for this taken from <see href="http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/"/>
1616
private const float MinimumSqrDistance = 1.75f;
1717
private const float DivisionThreshold = -.9995f;
1818

19-
/// <summary>
20-
/// The line points.
21-
/// </summary>
19+
private RectangleF? bounds;
2220
private PointF[]? linePoints;
23-
2421
private readonly PointF[] controlPoints;
2522

2623
/// <summary>
@@ -64,11 +61,29 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control
6461
/// </summary>
6562
public IReadOnlyList<PointF> ControlPoints => this.controlPoints;
6663

64+
/// <inheritdoc/>
65+
public PointF StartPoint => this.controlPoints[0];
66+
6767
/// <inheritdoc/>
6868
public PointF EndPoint => this.controlPoints[^1];
6969

70+
/// <inheritdoc />
71+
public RectangleF Bounds => this.bounds ??= CalculateBounds(this.GetLinePoints());
72+
73+
/// <inheritdoc />
74+
public int LinearVertexCount => this.GetLinePoints().Length;
75+
7076
/// <inheritdoc/>
71-
public ReadOnlyMemory<PointF> Flatten() => this.linePoints ??= GetDrawingPoints(this.controlPoints);
77+
public ReadOnlyMemory<PointF> Flatten() => this.GetLinePoints();
78+
79+
/// <inheritdoc />
80+
public void CopyTo(Span<PointF> destination, bool skipFirstPoint)
81+
{
82+
PointF[] linePoints = this.GetLinePoints();
83+
int startIndex = skipFirstPoint ? 1 : 0;
84+
85+
linePoints.AsSpan(startIndex).CopyTo(destination);
86+
}
7287

7388
/// <summary>
7489
/// Gets the control points of this curve.
@@ -102,10 +117,15 @@ public CubicBezierLineSegment Transform(Matrix4x4 matrix)
102117
/// <inheritdoc/>
103118
ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix);
104119

120+
private PointF[] GetLinePoints()
121+
=> this.linePoints ??= GetDrawingPoints(this.controlPoints);
122+
105123
private static PointF[] GetDrawingPoints(PointF[] controlPoints)
106124
{
107-
List<PointF> drawingPoints = [];
125+
// Each cubic contributes its end point plus a small number of midpoint inserts in the common case,
126+
// so 4 points per curve is a cheap baseline that avoids most growth without a sizing prepass.
108127
int curveCount = (controlPoints.Length - 1) / 3;
128+
List<PointF> drawingPoints = new(curveCount * 4);
109129

110130
for (int curveIndex = 0; curveIndex < curveCount; curveIndex++)
111131
{
@@ -200,4 +220,26 @@ private static Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vec
200220

201221
return p;
202222
}
223+
224+
/// <summary>
225+
/// Computes the bounds for the cached linearized bezier points.
226+
/// </summary>
227+
private static RectangleF CalculateBounds(ReadOnlySpan<PointF> points)
228+
{
229+
float minX = float.MaxValue;
230+
float minY = float.MaxValue;
231+
float maxX = float.MinValue;
232+
float maxY = float.MinValue;
233+
234+
for (int i = 0; i < points.Length; i++)
235+
{
236+
PointF point = points[i];
237+
minX = MathF.Min(minX, point.X);
238+
minY = MathF.Min(minY, point.Y);
239+
maxX = MathF.Max(maxX, point.X);
240+
maxY = MathF.Max(maxY, point.Y);
241+
}
242+
243+
return RectangleF.FromLTRB(minX, minY, maxX, maxY);
244+
}
203245
}

src/ImageSharp.Drawing/EmptyPath.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ namespace SixLabors.ImageSharp.Drawing;
1010
/// </summary>
1111
public sealed class EmptyPath : IPath
1212
{
13+
private static readonly LinearGeometry EmptyGeometry = new(
14+
new LinearGeometryInfo
15+
{
16+
Bounds = RectangleF.Empty,
17+
ContourCount = 0,
18+
PointCount = 0,
19+
SegmentCount = 0
20+
},
21+
[],
22+
[]);
23+
1324
private EmptyPath(PathTypes pathType) => this.PathType = pathType;
1425

1526
/// <summary>
@@ -34,6 +45,9 @@ public sealed class EmptyPath : IPath
3445
/// <inheritdoc />
3546
public IEnumerable<ISimplePath> Flatten() => [];
3647

48+
/// <inheritdoc />
49+
public LinearGeometry ToLinearGeometry() => EmptyGeometry;
50+
3751
/// <inheritdoc />
3852
public IPath Transform(Matrix4x4 matrix) => this;
3953
}

src/ImageSharp.Drawing/ILineSegment.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Drawing;
1010
/// </summary>
1111
public interface ILineSegment
1212
{
13+
/// <summary>
14+
/// Gets the start point.
15+
/// </summary>
16+
public PointF StartPoint { get; }
17+
1318
/// <summary>
1419
/// Gets the end point.
1520
/// </summary>
@@ -18,12 +23,29 @@ public interface ILineSegment
1823
/// </value>
1924
public PointF EndPoint { get; }
2025

26+
/// <summary>
27+
/// Gets the bounds of the linearized segment output.
28+
/// </summary>
29+
public RectangleF Bounds { get; }
30+
31+
/// <summary>
32+
/// Gets the number of linear vertices emitted by this segment.
33+
/// </summary>
34+
public int LinearVertexCount { get; }
35+
2136
/// <summary>
2237
/// Converts the <see cref="ILineSegment" /> into a simple linear path..
2338
/// </summary>
2439
/// <returns>Returns the current <see cref="ILineSegment" /> as simple linear path.</returns>
2540
public ReadOnlyMemory<PointF> Flatten();
2641

42+
/// <summary>
43+
/// Copies the linearized point data to the destination span.
44+
/// </summary>
45+
/// <param name="destination">The destination point span.</param>
46+
/// <param name="skipFirstPoint">Whether to skip the first emitted point.</param>
47+
public void CopyTo(Span<PointF> destination, bool skipFirstPoint);
48+
2749
/// <summary>
2850
/// Transforms the current LineSegment using specified matrix.
2951
/// </summary>

src/ImageSharp.Drawing/IPath.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ public interface IPath
2626
/// <returns>Returns the current <see cref="IPath" /> as simple linear path.</returns>
2727
public IEnumerable<ISimplePath> Flatten();
2828

29+
/// <summary>
30+
/// Converts this path into retained linear geometry for backend consumption.
31+
/// </summary>
32+
/// <remarks>
33+
/// <para>
34+
/// The returned <see cref="LinearGeometry"/> is the canonical lowered representation for backend scene building.
35+
/// It contains the point storage, contour metadata, and derived segment metadata needed to traverse the final
36+
/// linearized geometry without repeatedly rediscovering it through <see cref="Flatten()"/>.
37+
/// </para>
38+
/// <para>
39+
/// Closed contours are represented by contour metadata rather than by repeating the first point at the end of the
40+
/// stored point run.
41+
/// </para>
42+
/// <para>
43+
/// The returned geometry is expected to preserve the already-sanitized path shape. This method does not introduce
44+
/// additional geometric simplification policy beyond the mechanical stitching required by the segment emission
45+
/// contract.
46+
/// </para>
47+
/// </remarks>
48+
/// <returns>The retained linear geometry for this path.</returns>
49+
public LinearGeometry ToLinearGeometry();
50+
2951
/// <summary>
3052
/// Transforms the path using the specified matrix.
3153
/// </summary>

0 commit comments

Comments
 (0)