22// Licensed under the Six Labors Split License.
33
44using System . Numerics ;
5+ using System . Runtime . InteropServices ;
56using SixLabors . ImageSharp . Drawing . Shapes . PolygonClipper ;
67
78namespace SixLabors . ImageSharp . Drawing ;
@@ -11,7 +12,6 @@ namespace SixLabors.ImageSharp.Drawing;
1112/// </summary>
1213public static class OutlinePathExtensions
1314{
14- private const float MiterOffsetDelta = 20 ;
1515 private const JointStyle DefaultJointStyle = JointStyle . Square ;
1616 private const EndCapStyle DefaultEndCapStyle = EndCapStyle . Butt ;
1717
@@ -45,20 +45,16 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi
4545 return Path . Empty ;
4646 }
4747
48- List < Polygon > polygons = [ ] ;
48+ List < Polygon > stroked = [ ] ;
49+
50+ // TODO: Wire up options
51+ PolygonStroker stroker = new ( ) { Width = width , LineJoin = LineJoin . BevelJoin , LineCap = LineCap . Butt } ;
4952 foreach ( ISimplePath simplePath in path . Flatten ( ) )
5053 {
51- PolygonStroker stroker = new ( ) { Width = width } ;
52- Polygon polygon = stroker . ProcessPath ( simplePath . Points . Span , simplePath . IsClosed ) ;
53- polygons . Add ( polygon ) ;
54+ stroked . Add ( new Polygon ( stroker . ProcessPath ( simplePath . Points . Span , simplePath . IsClosed ) . ToArray ( ) ) ) ;
5455 }
5556
56- return new ComplexPolygon ( polygons ) ;
57-
58- // ClipperOffset offset = new(MiterOffsetDelta);
59- // offset.AddPath(path, jointStyle, endCapStyle);
60-
61- // return offset.Execute(width);
57+ return new ComplexPolygon ( stroked ) ;
6258 }
6359
6460 /// <summary>
@@ -78,11 +74,11 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
7874 /// <param name="path">The path to outline</param>
7975 /// <param name="width">The outline width.</param>
8076 /// <param name="pattern">The pattern made of multiples of the width.</param>
81- /// <param name="startOff ">Whether the first item in the pattern is on or off.</param>
77+ /// <param name="invert ">Whether the first item in the pattern is off.</param>
8278 /// <returns>A new <see cref="IPath"/> representing the outline.</returns>
8379 /// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
84- public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool startOff )
85- => GenerateOutline ( path , width , pattern , startOff , DefaultJointStyle , DefaultEndCapStyle ) ;
80+ public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool invert )
81+ => GenerateOutline ( path , width , pattern , invert , DefaultJointStyle , DefaultEndCapStyle ) ;
8682
8783 /// <summary>
8884 /// Generates an outline of the path with alternating on and off segments based on the pattern.
@@ -103,12 +99,12 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
10399 /// <param name="path">The path to outline</param>
104100 /// <param name="width">The outline width.</param>
105101 /// <param name="pattern">The pattern made of multiples of the width.</param>
106- /// <param name="startOff ">Whether the first item in the pattern is on or off.</param>
102+ /// <param name="invert ">Whether the first item in the pattern is off.</param>
107103 /// <param name="jointStyle">The style to apply to the joints.</param>
108104 /// <param name="endCapStyle">The style to apply to the end caps.</param>
109105 /// <returns>A new <see cref="IPath"/> representing the outline.</returns>
110106 /// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
111- public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool startOff , JointStyle jointStyle , EndCapStyle endCapStyle )
107+ public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool invert , JointStyle jointStyle , EndCapStyle endCapStyle )
112108 {
113109 if ( width <= 0 )
114110 {
@@ -122,18 +118,21 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
122118
123119 IEnumerable < ISimplePath > paths = path . Flatten ( ) ;
124120
125- ClipperOffset offset = new ( MiterOffsetDelta ) ;
126- List < PointF > buffer = new ( ) ;
127- foreach ( ISimplePath p in paths )
121+ // TODO: Wire up options
122+ PolygonStroker stroker = new ( ) { Width = width , LineJoin = LineJoin . BevelJoin , LineCap = LineCap . Butt } ;
123+
124+ PathsF stroked = [ ] ;
125+ List < PointF > buffer = [ ] ;
126+ foreach ( ISimplePath simplePath in paths )
128127 {
129- bool online = ! startOff ;
128+ bool online = ! invert ;
130129 float targetLength = pattern [ 0 ] * width ;
131130 int patternPos = 0 ;
132- ReadOnlySpan < PointF > points = p . Points . Span ;
131+ ReadOnlySpan < PointF > points = simplePath . Points . Span ;
133132
134133 // Create a new list of points representing the new outline
135134 int pCount = points . Length ;
136- if ( ! p . IsClosed )
135+ if ( ! simplePath . IsClosed )
137136 {
138137 pCount -- ;
139138 }
@@ -145,20 +144,20 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
145144 {
146145 int next = ( i + 1 ) % points . Length ;
147146 Vector2 targetPoint = points [ next ] ;
148- float distToNext = Vector2 . Distance ( currentPoint , targetPoint ) ;
149- if ( distToNext > targetLength )
147+ float distanceToNext = Vector2 . Distance ( currentPoint , targetPoint ) ;
148+ if ( distanceToNext > targetLength )
150149 {
151- // find a point between the 2
152- float t = targetLength / distToNext ;
150+ // Find a point between the 2
151+ float t = targetLength / distanceToNext ;
153152
154153 Vector2 point = ( currentPoint * ( 1 - t ) ) + ( targetPoint * t ) ;
155154 buffer . Add ( currentPoint ) ;
156155 buffer . Add ( point ) ;
157156
158- // we now inset a line joining
157+ // We now insert a line
159158 if ( online )
160159 {
161- offset . AddPath ( new ReadOnlySpan < PointF > ( buffer . ToArray ( ) ) , jointStyle , endCapStyle ) ;
160+ stroked . Add ( stroker . ProcessPath ( CollectionsMarshal . AsSpan ( buffer ) , false ) ) ;
162161 }
163162
164163 online = ! online ;
@@ -167,39 +166,64 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
167166
168167 currentPoint = point ;
169168
170- // next length
169+ // Next length
171170 patternPos = ( patternPos + 1 ) % pattern . Length ;
172171 targetLength = pattern [ patternPos ] * width ;
173172 }
174- else if ( distToNext <= targetLength )
173+ else if ( distanceToNext <= targetLength )
175174 {
176175 buffer . Add ( currentPoint ) ;
177176 currentPoint = targetPoint ;
178177 i ++ ;
179- targetLength -= distToNext ;
178+ targetLength -= distanceToNext ;
180179 }
181180 }
182181
183182 if ( buffer . Count > 0 )
184183 {
185- if ( p . IsClosed )
184+ if ( simplePath . IsClosed )
186185 {
187186 buffer . Add ( points [ 0 ] ) ;
188187 }
189188 else
190189 {
191- buffer . Add ( points [ points . Length - 1 ] ) ;
190+ buffer . Add ( points [ ^ 1 ] ) ;
192191 }
193192
194193 if ( online )
195194 {
196- offset . AddPath ( new ReadOnlySpan < PointF > ( buffer . ToArray ( ) ) , jointStyle , endCapStyle ) ;
195+ stroked . Add ( stroker . ProcessPath ( CollectionsMarshal . AsSpan ( buffer ) , false ) ) ;
197196 }
198197
199198 buffer . Clear ( ) ;
200199 }
201200 }
202201
203- return offset . Execute ( width ) ;
202+ // Clean up self intersections.
203+ PolygonClipper clipper = new ( ) { PreserveCollinear = true } ;
204+ clipper . AddSubject ( stroked ) ;
205+ PathsF clipped = [ ] ;
206+ clipper . Execute ( ClippingOperation . Union , FillRule . Positive , clipped ) ;
207+
208+ if ( clipped . Count == 0 )
209+ {
210+ // Cannot clip. Return the stroked path.
211+ Polygon [ ] polygons = new Polygon [ stroked . Count ] ;
212+ for ( int i = 0 ; i < stroked . Count ; i ++ )
213+ {
214+ polygons [ i ] = new Polygon ( stroked [ i ] . ToArray ( ) ) ;
215+ }
216+
217+ return new ComplexPolygon ( polygons ) ;
218+ }
219+
220+ // Convert the clipped paths back to polygons.
221+ Polygon [ ] result = new Polygon [ clipped . Count ] ;
222+ for ( int i = 0 ; i < clipped . Count ; i ++ )
223+ {
224+ result [ i ] = new Polygon ( clipped [ i ] . ToArray ( ) ) ;
225+ }
226+
227+ return new ComplexPolygon ( result ) ;
204228 }
205229}
0 commit comments