Skip to content

Commit 52312a1

Browse files
Clean PolygonClipperUtilities
1 parent 1174d90 commit 52312a1

2 files changed

Lines changed: 64 additions & 126 deletions

File tree

src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ private void DisposeIntersectNodes()
153153
[MethodImpl(MethodImplOptions.AggressiveInlining)]
154154
private void AddNewIntersectNode(Active ae1, Active ae2, float topY)
155155
{
156-
if (!ClipperUtils.GetIntersectPt(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip))
156+
if (!PolygonClipperUtilities.GetLineIntersectPoint(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip))
157157
{
158158
ip = new Vector2(ae1.CurX, topY);
159159
}
@@ -168,20 +168,20 @@ private void AddNewIntersectNode(Active ae1, Active ae2, float topY)
168168
{
169169
if (absDx1 > absDx2)
170170
{
171-
ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top);
171+
ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top);
172172
}
173173
else
174174
{
175-
ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top);
175+
ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top);
176176
}
177177
}
178178
else if (absDx1 > 100)
179179
{
180-
ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top);
180+
ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top);
181181
}
182182
else if (absDx2 > 100)
183183
{
184-
ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top);
184+
ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top);
185185
}
186186
else
187187
{
@@ -989,8 +989,8 @@ private void CleanCollinear(OutRec outrec)
989989
do
990990
{
991991
// NB if preserveCollinear == true, then only remove 180 deg. spikes
992-
if ((ClipperUtils.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0)
993-
&& ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (ClipperUtils.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0)))
992+
if ((PolygonClipperUtilities.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0)
993+
&& ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (PolygonClipperUtilities.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0)))
994994
{
995995
if (op2 == outrec.Pts)
996996
{
@@ -1024,7 +1024,7 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp)
10241024
OutPt nextNextOp = splitOp.Next.Next;
10251025
outrec.Pts = prevOp;
10261026

1027-
ClipperUtils.GetIntersectPoint(
1027+
PolygonClipperUtilities.GetIntersectPoint(
10281028
prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip);
10291029

10301030
float area1 = Area(prevOp);
@@ -1085,7 +1085,7 @@ private void FixSelfIntersects(OutRec outrec)
10851085
// triangles can't self-intersect
10861086
while (op2.Prev != op2.Next.Next)
10871087
{
1088-
if (ClipperUtils.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point))
1088+
if (PolygonClipperUtilities.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point))
10891089
{
10901090
this.DoSplitOp(outrec, op2);
10911091
if (outrec.Pts == null)
@@ -2453,7 +2453,7 @@ private static bool IsValidAelOrder(Active resident, Active newcomer)
24532453
}
24542454

24552455
// get the turning direction a1.top, a2.bot, a2.top
2456-
float d = ClipperUtils.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top);
2456+
float d = PolygonClipperUtilities.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top);
24572457
if (d != 0)
24582458
{
24592459
return d < 0;
@@ -2465,12 +2465,12 @@ private static bool IsValidAelOrder(Active resident, Active newcomer)
24652465
// the direction they're about to turn
24662466
if (!IsMaxima(resident) && (resident.Top.Y > newcomer.Top.Y))
24672467
{
2468-
return ClipperUtils.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0;
2468+
return PolygonClipperUtilities.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0;
24692469
}
24702470

24712471
if (!IsMaxima(newcomer) && (newcomer.Top.Y > resident.Top.Y))
24722472
{
2473-
return ClipperUtils.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0;
2473+
return PolygonClipperUtilities.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0;
24742474
}
24752475

24762476
float y = newcomer.Bot.Y;
@@ -2487,13 +2487,13 @@ private static bool IsValidAelOrder(Active resident, Active newcomer)
24872487
return newcomerIsLeft;
24882488
}
24892489

2490-
if (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0)
2490+
if (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0)
24912491
{
24922492
return true;
24932493
}
24942494

24952495
// compare turning direction of the alternate bound
2496-
return (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft;
2496+
return (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft;
24972497
}
24982498

24992499
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2933,7 +2933,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false)
29332933

29342934
if (checkCurrX)
29352935
{
2936-
if (ClipperUtils.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25)
2936+
if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25)
29372937
{
29382938
return;
29392939
}
@@ -2943,7 +2943,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false)
29432943
return;
29442944
}
29452945

2946-
if (ClipperUtils.CrossProduct(e.Top, pt, prev.Top) != 0)
2946+
if (PolygonClipperUtilities.CrossProduct(e.Top, pt, prev.Top) != 0)
29472947
{
29482948
return;
29492949
}
@@ -2988,7 +2988,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false)
29882988

29892989
if (checkCurrX)
29902990
{
2991-
if (ClipperUtils.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25)
2991+
if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25)
29922992
{
29932993
return;
29942994
}
@@ -2998,7 +2998,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false)
29982998
return;
29992999
}
30003000

3001-
if (ClipperUtils.CrossProduct(e.Top, pt, next.Top) != 0)
3001+
if (PolygonClipperUtilities.CrossProduct(e.Top, pt, next.Top) != 0)
30023002
{
30033003
return;
30043004
}
@@ -3383,7 +3383,7 @@ public PolyPathF AddChild(PathF p)
33833383
[MethodImpl(MethodImplOptions.AggressiveInlining)]
33843384
public float Area()
33853385
{
3386-
float result = this.Polygon == null ? 0 : ClipperUtils.Area(this.Polygon);
3386+
float result = this.Polygon == null ? 0 : PolygonClipperUtilities.SignedArea(this.Polygon);
33873387
for (int i = 0; i < this.items.Count; i++)
33883388
{
33893389
PolyPathF child = this.items[i];

src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs renamed to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs

Lines changed: 45 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@
66

77
namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
88

9-
internal static class ClipperUtils
9+
internal static class PolygonClipperUtilities
1010
{
11-
public const float DefaultArcTolerance = .25F;
12-
public const float FloatingPointTolerance = 1e-05F;
13-
public const float DefaultMinimumEdgeLength = .1F;
14-
15-
// TODO: rename to Pow2?
16-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
17-
public static float Sqr(float value) => value * value;
18-
11+
/// <summary>
12+
/// Computes the signed area of a path using the shoelace formula.
13+
/// </summary>
14+
/// <remarks>
15+
/// Positive values indicate clockwise orientation in screen space.
16+
/// </remarks>
1917
[MethodImpl(MethodImplOptions.AggressiveInlining)]
20-
public static float Area(PathF path)
18+
public static float SignedArea(PathF path)
2119
{
2220
// https://en.wikipedia.org/wiki/Shoelace_formula
2321
float a = 0F;
@@ -26,7 +24,8 @@ public static float Area(PathF path)
2624
return a;
2725
}
2826

29-
Vector2 prevPt = path[path.Count - 1];
27+
// Sum over edges (prev -> current).
28+
Vector2 prevPt = path[^1];
3029
for (int i = 0; i < path.Count; i++)
3130
{
3231
Vector2 pt = path[i];
@@ -37,87 +36,34 @@ public static float Area(PathF path)
3736
return a * .5F;
3837
}
3938

40-
public static PathF StripDuplicates(PathF path, bool isClosedPath)
41-
{
42-
int cnt = path.Count;
43-
PathF result = new(cnt);
44-
if (cnt == 0)
45-
{
46-
return result;
47-
}
48-
49-
PointF lastPt = path[0];
50-
result.Add(lastPt);
51-
for (int i = 1; i < cnt; i++)
52-
{
53-
if (lastPt != path[i])
54-
{
55-
lastPt = path[i];
56-
result.Add(lastPt);
57-
}
58-
}
59-
60-
if (isClosedPath && lastPt == result[0])
61-
{
62-
result.RemoveAt(result.Count - 1);
63-
}
64-
65-
return result;
66-
}
67-
68-
public static PathF Ellipse(Vector2 center, float radiusX, float radiusY = 0, int steps = 0)
69-
{
70-
if (radiusX <= 0)
71-
{
72-
return [];
73-
}
74-
75-
if (radiusY <= 0)
76-
{
77-
radiusY = radiusX;
78-
}
79-
80-
if (steps <= 2)
81-
{
82-
steps = (int)MathF.Ceiling(MathF.PI * MathF.Sqrt((radiusX + radiusY) * .5F));
83-
}
84-
85-
float si = MathF.Sin(2 * MathF.PI / steps);
86-
float co = MathF.Cos(2 * MathF.PI / steps);
87-
float dx = co, dy = si;
88-
PathF result = new(steps) { new Vector2(center.X + radiusX, center.Y) };
89-
Vector2 radiusXY = new(radiusX, radiusY);
90-
for (int i = 1; i < steps; ++i)
91-
{
92-
result.Add(center + (radiusXY * new Vector2(dx, dy)));
93-
float x = (dx * co) - (dy * si);
94-
dy = (dy * co) + (dx * si);
95-
dx = x;
96-
}
97-
98-
return result;
99-
}
100-
10139
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10240
public static float DotProduct(Vector2 vec1, Vector2 vec2)
10341
=> Vector2.Dot(vec1, vec2);
10442

43+
/// <summary>
44+
/// Returns the dot product of the segments (pt1->pt2) and (pt2->pt3).
45+
/// </summary>
46+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
47+
public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3)
48+
=> Vector2.Dot(pt2 - pt1, pt3 - pt2);
49+
50+
/// <summary>
51+
/// Returns the 2D cross product magnitude of <paramref name="vec1" /> and <paramref name="vec2" />.
52+
/// </summary>
10553
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10654
public static float CrossProduct(Vector2 vec1, Vector2 vec2)
10755
=> (vec1.Y * vec2.X) - (vec2.Y * vec1.X);
10856

57+
/// <summary>
58+
/// Returns the cross product of the segments (pt1->pt2) and (pt2->pt3).
59+
/// </summary>
10960
[MethodImpl(MethodImplOptions.AggressiveInlining)]
11061
public static float CrossProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3)
11162
=> ((pt2.X - pt1.X) * (pt3.Y - pt2.Y)) - ((pt2.Y - pt1.Y) * (pt3.X - pt2.X));
11263

113-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
114-
public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3)
115-
=> Vector2.Dot(pt2 - pt1, pt3 - pt2);
116-
117-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
118-
public static bool IsAlmostZero(float value)
119-
=> MathF.Abs(value) <= FloatingPointTolerance;
120-
64+
/// <summary>
65+
/// Returns the squared perpendicular distance from a point to a line segment.
66+
/// </summary>
12167
[MethodImpl(MethodImplOptions.AggressiveInlining)]
12268
public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 line2)
12369
{
@@ -128,9 +74,18 @@ public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2
12874
return 0;
12975
}
13076

131-
return Sqr(CrossProduct(cd, ab)) / DotProduct(cd, cd);
77+
float cross = CrossProduct(cd, ab);
78+
return (cross * cross) / DotProduct(cd, cd);
13279
}
13380

81+
/// <summary>
82+
/// Returns true when two segments intersect.
83+
/// </summary>
84+
/// <param name="seg1a">First endpoint of segment 1.</param>
85+
/// <param name="seg1b">Second endpoint of segment 1.</param>
86+
/// <param name="seg2a">First endpoint of segment 2.</param>
87+
/// <param name="seg2b">Second endpoint of segment 2.</param>
88+
/// <param name="inclusive">If true, allows shared endpoints; if false, requires a proper intersection.</param>
13489
public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Vector2 seg2b, bool inclusive = false)
13590
{
13691
if (inclusive)
@@ -158,26 +113,7 @@ public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Ve
158113
}
159114

160115
[MethodImpl(MethodImplOptions.AggressiveInlining)]
161-
internal static bool GetIntersectPt(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip)
162-
{
163-
Vector2 dxy1 = ln1b - ln1a;
164-
Vector2 dxy2 = ln2b - ln2a;
165-
float cp = CrossProduct(dxy1, dxy2);
166-
if (cp == 0F)
167-
{
168-
ip = default;
169-
return false;
170-
}
171-
172-
float qx = CrossProduct(ln1a, dxy1);
173-
float qy = CrossProduct(ln2a, dxy2);
174-
175-
ip = ((dxy1 * qy) - (dxy2 * qx)) / cp;
176-
return ip != new Vector2(float.MaxValue);
177-
}
178-
179-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
180-
public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip)
116+
public static bool GetLineIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip)
181117
{
182118
Vector2 dxy1 = ln1b - ln1a;
183119
Vector2 dxy2 = ln2b - ln2a;
@@ -189,6 +125,8 @@ public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, V
189125
}
190126

191127
float t = (((ln1a.X - ln2a.X) * dxy2.Y) - ((ln1a.Y - ln2a.Y) * dxy2.X)) / det;
128+
129+
// Clamp intersection to the segment endpoints.
192130
if (t <= 0F)
193131
{
194132
ip = ln1a;
@@ -205,6 +143,12 @@ public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, V
205143
return true;
206144
}
207145

146+
/// <summary>
147+
/// Returns the closest point on a segment to an external point.
148+
/// </summary>
149+
/// <param name="offPt">The point to project onto the segment.</param>
150+
/// <param name="seg1">First endpoint of the segment.</param>
151+
/// <param name="seg2">Second endpoint of the segment.</param>
208152
public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 seg2)
209153
{
210154
if (seg1 == seg2)
@@ -227,10 +171,4 @@ public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2
227171

228172
return seg1 + (dxy * q);
229173
}
230-
231-
public static PathF ReversePath(PathF path)
232-
{
233-
path.Reverse();
234-
return path;
235-
}
236174
}

0 commit comments

Comments
 (0)