@@ -12,9 +12,35 @@ namespace SixLabors.ImageSharp.Drawing;
1212/// </summary>
1313public static class OutlinePathExtensions
1414{
15+ private const float MiterOffsetDelta = 20 ;
1516 private const JointStyle DefaultJointStyle = JointStyle . Square ;
1617 private const EndCapStyle DefaultEndCapStyle = EndCapStyle . Butt ;
1718
19+ /// <summary>
20+ /// Calculates the scaling matrixes tha tmust be applied to the inout and output paths of for successful clipping.
21+ /// </summary>
22+ /// <param name="width">the requested width</param>
23+ /// <param name="scaleUpMartrix">The matrix to apply to the input path</param>
24+ /// <param name="scaleDownMartrix">The matrix to apply to the output path</param>
25+ /// <returns>The final width to use internally to outlining</returns>
26+ private static float CalculateScalingMatrix ( float width , out Matrix3x2 scaleUpMartrix , out Matrix3x2 scaleDownMartrix )
27+ {
28+ // when the thickness is below a 0.5 threshold we need to scale
29+ // the source path (up) and result path (down) by a factor to ensure
30+ // the offest is greater than 0.5 to ensure offsetting isn't skipped.
31+ scaleUpMartrix = Matrix3x2 . Identity ;
32+ scaleDownMartrix = Matrix3x2 . Identity ;
33+ if ( width < 0.5 )
34+ {
35+ float scale = 1 / width ;
36+ scaleUpMartrix = Matrix3x2 . CreateScale ( scale ) ;
37+ scaleDownMartrix = Matrix3x2 . CreateScale ( width ) ;
38+ width = 1 ;
39+ }
40+
41+ return width ;
42+ }
43+
1844 /// <summary>
1945 /// Generates an outline of the path.
2046 /// </summary>
@@ -41,15 +67,14 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi
4167 return Path . Empty ;
4268 }
4369
44- List < Polygon > stroked = [ ] ;
70+ width = CalculateScalingMatrix ( width , out Matrix3x2 scaleUpMartrix , out Matrix3x2 scaleDownMartrix ) ;
4571
46- PolygonStroker stroker = new ( ) { Width = width , LineJoin = GetLineJoin ( jointStyle ) , LineCap = GetLineCap ( endCapStyle ) } ;
47- foreach ( ISimplePath simplePath in path . Flatten ( ) )
48- {
49- stroked . Add ( new Polygon ( stroker . ProcessPath ( simplePath . Points . Span , simplePath . IsClosed || endCapStyle is EndCapStyle . Polygon or EndCapStyle . Joined ) . ToArray ( ) ) ) ;
50- }
72+ ClipperOffset offset = new ( MiterOffsetDelta ) ;
5173
52- return new ComplexPolygon ( stroked ) ;
74+ // transform is noop for Matrix3x2.Identity
75+ offset . AddPath ( path . Transform ( scaleUpMartrix ) , jointStyle , endCapStyle ) ;
76+
77+ return offset . Execute ( width ) . Transform ( scaleDownMartrix ) ;
5378 }
5479
5580 /// <summary>
@@ -69,11 +94,11 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
6994 /// <param name="path">The path to outline</param>
7095 /// <param name="width">The outline width.</param>
7196 /// <param name="pattern">The pattern made of multiples of the width.</param>
72- /// <param name="invert ">Whether the first item in the pattern is off.</param>
97+ /// <param name="startOff ">Whether the first item in the pattern is on or off.</param>
7398 /// <returns>A new <see cref="IPath"/> representing the outline.</returns>
7499 /// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
75- public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool invert )
76- => GenerateOutline ( path , width , pattern , invert , DefaultJointStyle , DefaultEndCapStyle ) ;
100+ public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool startOff )
101+ => GenerateOutline ( path , width , pattern , startOff , DefaultJointStyle , DefaultEndCapStyle ) ;
77102
78103 /// <summary>
79104 /// Generates an outline of the path with alternating on and off segments based on the pattern.
@@ -94,12 +119,12 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
94119 /// <param name="path">The path to outline</param>
95120 /// <param name="width">The outline width.</param>
96121 /// <param name="pattern">The pattern made of multiples of the width.</param>
97- /// <param name="invert ">Whether the first item in the pattern is off.</param>
122+ /// <param name="startOff ">Whether the first item in the pattern is on or off.</param>
98123 /// <param name="jointStyle">The style to apply to the joints.</param>
99124 /// <param name="endCapStyle">The style to apply to the end caps.</param>
100125 /// <returns>A new <see cref="IPath"/> representing the outline.</returns>
101126 /// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
102- public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool invert , JointStyle jointStyle , EndCapStyle endCapStyle )
127+ public static IPath GenerateOutline ( this IPath path , float width , ReadOnlySpan < float > pattern , bool startOff , JointStyle jointStyle , EndCapStyle endCapStyle )
103128 {
104129 if ( width <= 0 )
105130 {
@@ -111,20 +136,22 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
111136 return path . GenerateOutline ( width , jointStyle , endCapStyle ) ;
112137 }
113138
114- PolygonStroker stroker = new ( ) { Width = width , LineJoin = GetLineJoin ( jointStyle ) , LineCap = GetLineCap ( endCapStyle ) } ;
115- PathsF stroked = [ ] ;
116- List < PointF > buffer = [ ] ;
139+ width = CalculateScalingMatrix ( width , out Matrix3x2 scaleUpMartrix , out Matrix3x2 scaleDownMartrix ) ;
140+
141+ IEnumerable < ISimplePath > paths = path . Transform ( scaleUpMartrix ) . Flatten ( ) ;
117142
118- foreach ( ISimplePath simplePath in path . Flatten ( ) )
143+ ClipperOffset offset = new ( MiterOffsetDelta ) ;
144+ List < PointF > buffer = new ( ) ;
145+ foreach ( ISimplePath p in paths )
119146 {
120- bool online = ! invert ;
147+ bool online = ! startOff ;
121148 float targetLength = pattern [ 0 ] * width ;
122149 int patternPos = 0 ;
123- ReadOnlySpan < PointF > points = simplePath . Points . Span ;
150+ ReadOnlySpan < PointF > points = p . Points . Span ;
124151
125152 // Create a new list of points representing the new outline
126153 int pCount = points . Length ;
127- if ( ! simplePath . IsClosed )
154+ if ( ! p . IsClosed )
128155 {
129156 pCount -- ;
130157 }
@@ -136,20 +163,20 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
136163 {
137164 int next = ( i + 1 ) % points . Length ;
138165 Vector2 targetPoint = points [ next ] ;
139- float distanceToNext = Vector2 . Distance ( currentPoint , targetPoint ) ;
140- if ( distanceToNext > targetLength )
166+ float distToNext = Vector2 . Distance ( currentPoint , targetPoint ) ;
167+ if ( distToNext > targetLength )
141168 {
142- // Find a point between the 2
143- float t = targetLength / distanceToNext ;
169+ // find a point between the 2
170+ float t = targetLength / distToNext ;
144171
145172 Vector2 point = ( currentPoint * ( 1 - t ) ) + ( targetPoint * t ) ;
146173 buffer . Add ( currentPoint ) ;
147174 buffer . Add ( point ) ;
148175
149- // We now insert a line
176+ // we now inset a line joining
150177 if ( online )
151178 {
152- stroked . Add ( stroker . ProcessPath ( CollectionsMarshal . AsSpan ( buffer ) , false ) ) ;
179+ offset . AddPath ( CollectionsMarshal . AsSpan ( buffer ) , jointStyle , endCapStyle ) ;
153180 }
154181
155182 online = ! online ;
@@ -158,22 +185,22 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
158185
159186 currentPoint = point ;
160187
161- // Next length
188+ // next length
162189 patternPos = ( patternPos + 1 ) % pattern . Length ;
163190 targetLength = pattern [ patternPos ] * width ;
164191 }
165- else if ( distanceToNext <= targetLength )
192+ else if ( distToNext <= targetLength )
166193 {
167194 buffer . Add ( currentPoint ) ;
168195 currentPoint = targetPoint ;
169196 i ++ ;
170- targetLength -= distanceToNext ;
197+ targetLength -= distToNext ;
171198 }
172199 }
173200
174201 if ( buffer . Count > 0 )
175202 {
176- if ( simplePath . IsClosed )
203+ if ( p . IsClosed )
177204 {
178205 buffer . Add ( points [ 0 ] ) ;
179206 }
@@ -184,54 +211,13 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
184211
185212 if ( online )
186213 {
187- stroked . Add ( stroker . ProcessPath ( CollectionsMarshal . AsSpan ( buffer ) , false ) ) ;
214+ offset . AddPath ( CollectionsMarshal . AsSpan ( buffer ) , jointStyle , endCapStyle ) ;
188215 }
189216
190217 buffer . Clear ( ) ;
191218 }
192219 }
193220
194- // Clean up self intersections.
195- PolygonClipper clipper = new ( ) { PreserveCollinear = true } ;
196- clipper . AddSubject ( stroked ) ;
197- PathsF clipped = [ ] ;
198- clipper . Execute ( ClippingOperation . Union , FillRule . Positive , clipped ) ;
199-
200- if ( clipped . Count == 0 )
201- {
202- // Cannot clip. Return the stroked path.
203- Polygon [ ] polygons = new Polygon [ stroked . Count ] ;
204- for ( int i = 0 ; i < stroked . Count ; i ++ )
205- {
206- polygons [ i ] = new Polygon ( stroked [ i ] . ToArray ( ) ) ;
207- }
208-
209- return new ComplexPolygon ( polygons ) ;
210- }
211-
212- // Convert the clipped paths back to polygons.
213- Polygon [ ] result = new Polygon [ clipped . Count ] ;
214- for ( int i = 0 ; i < clipped . Count ; i ++ )
215- {
216- result [ i ] = new Polygon ( clipped [ i ] . ToArray ( ) ) ;
217- }
218-
219- return new ComplexPolygon ( result ) ;
221+ return offset . Execute ( width ) . Transform ( scaleDownMartrix ) ;
220222 }
221-
222- private static LineJoin GetLineJoin ( JointStyle value )
223- => value switch
224- {
225- JointStyle . Square => LineJoin . BevelJoin ,
226- JointStyle . Round => LineJoin . RoundJoin ,
227- _ => LineJoin . MiterJoin ,
228- } ;
229-
230- private static LineCap GetLineCap ( EndCapStyle value )
231- => value switch
232- {
233- EndCapStyle . Round => LineCap . Round ,
234- EndCapStyle . Square => LineCap . Square ,
235- _ => LineCap . Butt ,
236- } ;
237223}
0 commit comments