11// Copyright (c) Six Labors.
22// Licensed under the Six Labors Split License.
33
4+ using System . Numerics ;
5+ using SixLabors . ImageSharp . Drawing . Utilities ;
46using SixLabors . PolygonClipper ;
57
68namespace SixLabors . ImageSharp . Drawing . Shapes . PolygonGeometry ;
@@ -57,6 +59,7 @@ public IPath[] GenerateStrokedShapes(List<PointF[]> spans, float width)
5759 // 1) Stroke each dashed span as open.
5860 this . polygonStroker . Width = width ;
5961
62+ List < PointF [ ] > ringPoints = new ( spans . Count ) ;
6063 List < IPath > rings = new ( spans . Count ) ;
6164 foreach ( PointF [ ] span in spans )
6265 {
@@ -71,6 +74,7 @@ public IPath[] GenerateStrokedShapes(List<PointF[]> spans, float width)
7174 continue ;
7275 }
7376
77+ ringPoints . Add ( stroked ) ;
7478 rings . Add ( new Polygon ( new LinearLineSegment ( stroked ) ) ) ;
7579 }
7680
@@ -80,10 +84,9 @@ public IPath[] GenerateStrokedShapes(List<PointF[]> spans, float width)
8084 return [ ] ;
8185 }
8286
83- if ( count == 1 )
87+ if ( ! HasIntersections ( ringPoints ) )
8488 {
85- // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty.
86- return [ rings [ 0 ] ] ;
89+ return count == 1 ? [ rings [ 0 ] ] : [ .. rings ] ;
8790 }
8891
8992 // 2) Partition so the first and last are on different polygons
@@ -140,6 +143,7 @@ public IPath[] GenerateStrokedShapes(List<PointF[]> spans, float width)
140143 public IPath [ ] GenerateStrokedShapes ( IPath path , float width )
141144 {
142145 // 1) Stroke the input path into closed rings
146+ List < PointF [ ] > ringPoints = [ ] ;
143147 List < IPath > rings = [ ] ;
144148 this . polygonStroker . Width = width ;
145149
@@ -151,6 +155,7 @@ public IPath[] GenerateStrokedShapes(IPath path, float width)
151155 continue ; // skip degenerate outputs
152156 }
153157
158+ ringPoints . Add ( stroked ) ;
154159 rings . Add ( new Polygon ( new LinearLineSegment ( stroked ) ) ) ;
155160 }
156161
@@ -160,10 +165,9 @@ public IPath[] GenerateStrokedShapes(IPath path, float width)
160165 return [ ] ;
161166 }
162167
163- if ( count == 1 )
168+ if ( ! HasIntersections ( ringPoints ) )
164169 {
165- // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty.
166- return [ rings [ 0 ] ] ;
170+ return count == 1 ? [ rings [ 0 ] ] : [ .. rings ] ;
167171 }
168172
169173 // 2) Partition so the first and last are on different polygons
@@ -204,4 +208,92 @@ public IPath[] GenerateStrokedShapes(IPath path, float width)
204208 // 4) Return the cleaned, merged outline
205209 return clipper . GenerateClippedShapes ( BooleanOperation . Union ) ;
206210 }
211+
212+ /// <summary>
213+ /// Determines whether any of the provided rings contain self-intersections or intersect with other rings.
214+ /// </summary>
215+ /// <remarks>
216+ /// This method performs a conservative scan to detect intersections among the provided rings. It
217+ /// checks for both self-intersections within each ring and intersections between different rings. Rings are treated
218+ /// as polylines; if a ring is closed (its first and last points are equal), the closing segment is included in the
219+ /// intersection checks. This method is intended for fast intersection detection and may be used to determine
220+ /// whether further geometric processing, such as clipping, is necessary.
221+ /// </remarks>
222+ /// <param name="rings">
223+ /// A list of rings, where each ring is represented as an array of points defining its vertices. Each ring is
224+ /// expected to be a sequence of points forming a polyline or polygon.
225+ /// </param>
226+ /// <returns><see langword="true"/> if any ring self-intersects or any two rings intersect; otherwise, <see langword="false"/>.</returns>
227+ private static bool HasIntersections ( List < PointF [ ] > rings )
228+ {
229+ // Detect whether any stroked ring self-intersects or intersects another ring.
230+ // This is a fast, conservative scan used to decide whether we can skip clipping.
231+ Vector2 intersection = default ;
232+
233+ for ( int r = 0 ; r < rings . Count ; r ++ )
234+ {
235+ PointF [ ] ring = rings [ r ] ;
236+ int segmentCount = ring . Length - 1 ;
237+ if ( segmentCount < 2 )
238+ {
239+ continue ;
240+ }
241+
242+ // 1) Self-intersection scan for the current ring.
243+ // Adjacent segments share a vertex and are skipped to avoid trivial hits.
244+ bool isClosed = ring [ 0 ] == ring [ ^ 1 ] ;
245+ for ( int i = 0 ; i < segmentCount ; i ++ )
246+ {
247+ Vector2 a0 = new ( ring [ i ] . X , ring [ i ] . Y ) ;
248+ Vector2 a1 = new ( ring [ i + 1 ] . X , ring [ i + 1 ] . Y ) ;
249+
250+ for ( int j = i + 1 ; j < segmentCount ; j ++ )
251+ {
252+ // Skip neighbors and the closing edge pair in a closed ring.
253+ if ( j == i + 1 || ( isClosed && i == 0 && j == segmentCount - 1 ) )
254+ {
255+ continue ;
256+ }
257+
258+ Vector2 b0 = new ( ring [ j ] . X , ring [ j ] . Y ) ;
259+ Vector2 b1 = new ( ring [ j + 1 ] . X , ring [ j + 1 ] . Y ) ;
260+ if ( Intersect . LineSegmentToLineSegmentIgnoreCollinear ( a0 , a1 , b0 , b1 , ref intersection ) )
261+ {
262+ return true ;
263+ }
264+ }
265+ }
266+
267+ // 2) Cross-ring intersection scan against later rings only.
268+ // This avoids double work while checking all ring pairs.
269+ for ( int s = r + 1 ; s < rings . Count ; s ++ )
270+ {
271+ PointF [ ] other = rings [ s ] ;
272+ int otherSegmentCount = other . Length - 1 ;
273+ if ( otherSegmentCount < 1 )
274+ {
275+ continue ;
276+ }
277+
278+ for ( int i = 0 ; i < segmentCount ; i ++ )
279+ {
280+ Vector2 a0 = new ( ring [ i ] . X , ring [ i ] . Y ) ;
281+ Vector2 a1 = new ( ring [ i + 1 ] . X , ring [ i + 1 ] . Y ) ;
282+
283+ for ( int j = 0 ; j < otherSegmentCount ; j ++ )
284+ {
285+ Vector2 b0 = new ( other [ j ] . X , other [ j ] . Y ) ;
286+ Vector2 b1 = new ( other [ j + 1 ] . X , other [ j + 1 ] . Y ) ;
287+ if ( Intersect . LineSegmentToLineSegmentIgnoreCollinear ( a0 , a1 , b0 , b1 , ref intersection ) )
288+ {
289+ return true ;
290+ }
291+ }
292+ }
293+ }
294+ }
295+
296+ // No intersections detected.
297+ return false ;
298+ }
207299}
0 commit comments