Skip to content

Commit 46f0eb1

Browse files
Merge pull request #17 from SixLabors/js/optimize-possible-intersection
Reduce allocations and internal copies
2 parents 5e53144 + 7889d92 commit 46f0eb1

21 files changed

+32967
-246
lines changed

src/PolygonClipper/Box2.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace PolygonClipper;
1616
/// </summary>
1717
/// <param name="vector">The xy-coordinate.</param>
1818
[MethodImpl(MethodImplOptions.AggressiveInlining)]
19-
public Box2(Vertex vector)
19+
public Box2(in Vertex vector)
2020
: this(vector, vector)
2121
{
2222
}
@@ -26,7 +26,7 @@ public Box2(Vertex vector)
2626
/// </summary>
2727
/// <param name="min">The minimum xy-coordinate.</param>
2828
/// <param name="max">The maximum xy-coordinate.</param>
29-
public Box2(Vertex min, Vertex max)
29+
public Box2(in Vertex min, in Vertex max)
3030
{
3131
this.Min = min;
3232
this.Max = max;
@@ -43,11 +43,11 @@ public Box2(Vertex min, Vertex max)
4343
public Vertex Max { get; }
4444

4545
[MethodImpl(MethodImplOptions.AggressiveInlining)]
46-
public static bool operator ==(Box2 left, Box2 right)
46+
public static bool operator ==(in Box2 left, in Box2 right)
4747
=> left.Equals(right);
4848

4949
[MethodImpl(MethodImplOptions.AggressiveInlining)]
50-
public static bool operator !=(Box2 left, Box2 right)
50+
public static bool operator !=(in Box2 left, in Box2 right)
5151
=> !(left == right);
5252

5353
/// <summary>
@@ -56,7 +56,7 @@ public Box2(Vertex min, Vertex max)
5656
/// <param name="other">The other box.</param>
5757
/// <returns>The summed <see cref="Box2"/>.</returns>
5858
[MethodImpl(MethodImplOptions.AggressiveInlining)]
59-
public Box2 Add(Box2 other)
59+
public Box2 Add(in Box2 other)
6060
=> new(Vertex.Min(this.Min, other.Min), Vertex.Max(this.Max, other.Max));
6161

6262
/// <inheritdoc/>

src/PolygonClipper/Contour.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ public sealed class Contour
4343
public int HoleCount => this.holes.Count;
4444

4545
/// <summary>
46-
/// Gets a value indicating whether the contour
47-
/// is external (not a hole).
46+
/// Gets a value indicating whether the contour is external (not a hole).
4847
/// </summary>
4948
public bool IsExternal => this.HoleOf == null;
5049

@@ -127,12 +126,12 @@ public bool IsCounterClockwise()
127126
{
128127
c = points[i];
129128
c1 = points[i + 1];
130-
area += (c.X * c1.Y) - (c1.X * c.Y);
129+
area += Vertex.Cross(c, c1);
131130
}
132131

133-
c = points[this.points.Count - 1];
132+
c = points[^1];
134133
c1 = points[0];
135-
area += (c.X * c1.Y) - (c1.X * c.Y);
134+
area += Vertex.Cross(c, c1);
136135
return this.cc = area >= 0;
137136
}
138137

@@ -176,11 +175,11 @@ public void SetCounterClockwise()
176175
}
177176

178177
/// <summary>
179-
/// Offsets the contour by the specified x and y values.
178+
/// Translates the contour by the specified x and y values.
180179
/// </summary>
181180
/// <param name="x">The x-coordinate offset.</param>
182181
/// <param name="y">The y-coordinate offset.</param>
183-
public void Offset(double x, double y)
182+
public void Translate(double x, double y)
184183
{
185184
List<Vertex> points = this.points;
186185
for (int i = 0; i < points.Count; i++)
@@ -194,7 +193,7 @@ public void Offset(double x, double y)
194193
/// </summary>
195194
/// <param name="vertex">The vertex to add.</param>
196195
[MethodImpl(MethodImplOptions.AggressiveInlining)]
197-
public void AddVertex(Vertex vertex) => this.points.Add(vertex);
196+
public void AddVertex(in Vertex vertex) => this.points.Add(vertex);
198197

199198
/// <summary>
200199
/// Removes the vertex at the specified index from the contour.

src/PolygonClipper/Polygon.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,15 @@ public Box2 GetBoundingBox()
9393
}
9494

9595
/// <summary>
96-
/// Offsets the polygon by the specified x and y values.
96+
/// Translates the polygon by the specified x and y values.
9797
/// </summary>
9898
/// <param name="x">The x-coordinate offset.</param>
9999
/// <param name="y">The y-coordinate offset.</param>
100-
public void Offset(double x, double y)
100+
public void Translate(double x, double y)
101101
{
102102
for (int i = 0; i < this.contours.Count; i++)
103103
{
104-
this.contours[i].Offset(x, y);
104+
this.contours[i].Translate(x, y);
105105
}
106106
}
107107

src/PolygonClipper/PolygonClipper.cs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,32 @@ public Polygon Run()
123123
Vertex min = new(double.PositiveInfinity);
124124
Vertex max = new(double.NegativeInfinity);
125125

126-
int eventCount = (subject.GetVertexCount() + clipping.GetVertexCount()) * 2;
127-
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(new SweepEventComparer(), eventCount);
126+
// Estimate the total number of sweep events.
127+
// Each segment contributes two events (left/right endpoints),
128+
// and subdivision during intersection may increase the count,
129+
// so we conservatively double the total vertex count.
130+
int subjectVertexCount = subject.GetVertexCount();
131+
int clippingVertexCount = clipping.GetVertexCount();
132+
int eventCount = (subjectVertexCount + clippingVertexCount) * 2;
133+
134+
SweepEventComparer comparer = new();
135+
List<SweepEvent> unorderedEventQueue = new(eventCount);
128136
int contourId = 0;
137+
129138
for (int i = 0; i < subject.ContourCount; i++)
130139
{
131140
Contour contour = subject[i];
132141
contourId++;
133142
for (int j = 0; j < contour.VertexCount - 1; j++)
134143
{
135-
ProcessSegment(contourId, contour.Segment(j), PolygonType.Subject, eventQueue, ref min, ref max);
144+
ProcessSegment(
145+
contourId,
146+
contour.Segment(j),
147+
PolygonType.Subject,
148+
unorderedEventQueue,
149+
comparer,
150+
ref min,
151+
ref max);
136152
}
137153
}
138154

@@ -147,7 +163,14 @@ public Polygon Run()
147163

148164
for (int j = 0; j < contour.VertexCount - 1; j++)
149165
{
150-
ProcessSegment(contourId, contour.Segment(j), PolygonType.Clipping, eventQueue, ref min, ref max);
166+
ProcessSegment(
167+
contourId,
168+
contour.Segment(j),
169+
PolygonType.Clipping,
170+
unorderedEventQueue,
171+
comparer,
172+
ref min,
173+
ref max);
151174
}
152175
}
153176

@@ -158,9 +181,14 @@ public Polygon Run()
158181
}
159182

160183
// Sweep line algorithm: process events in the priority queue
161-
List<SweepEvent> sortedEvents = [];
162-
StatusLine statusLine = new();
163-
SweepEventComparer comparer = eventQueue.Comparer;
184+
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(comparer, unorderedEventQueue);
185+
List<SweepEvent> sortedEvents = new(eventCount);
186+
187+
// Heuristic capacity for the sweep line status structure.
188+
// At any given point during the sweep, only a subset of segments
189+
// are active, so we preallocate half the subject's vertex count
190+
// to reduce resizing without overcommitting memory.
191+
StatusLine statusLine = new(subjectVertexCount >> 1);
164192
double subjectMaxX = subjectBB.Max.X;
165193
double minMaxX = Vertex.Min(subjectBB.Max, clippingBB.Max).X;
166194

@@ -335,14 +363,16 @@ private static bool TryTrivialOperationForNonOverlappingBoundingBoxes(
335363
/// <param name="contourId">The identifier of the contour to which the segment belongs.</param>
336364
/// <param name="s">The segment to process.</param>
337365
/// <param name="pt">The polygon type to which the segment belongs.</param>
338-
/// <param name="eventQueue">The event queue to add the generated events to.</param>
366+
/// <param name="eventQueue">The unordered event queue to add the generated events to.</param>
367+
/// <param name="comparer">The comparer used to determine the order of sweep events in the queue.</param>
339368
/// <param name="min">The minimum vertex of the bounding box.</param>
340369
/// <param name="max">The maximum vertex of the bounding box.</param>
341370
private static void ProcessSegment(
342371
int contourId,
343372
Segment s,
344373
PolygonType pt,
345-
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue,
374+
List<SweepEvent> eventQueue,
375+
SweepEventComparer comparer,
346376
ref Vertex min,
347377
ref Vertex max)
348378
{
@@ -359,7 +389,7 @@ private static void ProcessSegment(
359389
e1.ContourId = e2.ContourId = contourId;
360390

361391
// Determine which endpoint is the left endpoint
362-
if (eventQueue.Comparer.Compare(e1, e2) < 0)
392+
if (comparer.Compare(e1, e2) < 0)
363393
{
364394
e2.Left = false;
365395
}
@@ -372,8 +402,8 @@ private static void ProcessSegment(
372402
max = Vertex.Max(max, s.Max);
373403

374404
// Add the events to the event queue
375-
eventQueue.Enqueue(e1);
376-
eventQueue.Enqueue(e2);
405+
eventQueue.Add(e1);
406+
eventQueue.Add(e2);
377407
}
378408

379409
/// <summary>
@@ -868,7 +898,7 @@ private static Polygon ConnectEdges(List<SweepEvent> sortedEvents, SweepEventCom
868898

869899
private static ReadOnlySpan<int> PrecomputeIterationOrder(List<SweepEvent> data)
870900
{
871-
Span<int> map = new(new int[data.Count]);
901+
Span<int> map = new int[data.Count];
872902

873903
int i = 0;
874904
while (i < data.Count)

src/PolygonClipper/PolygonUtilities.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal static class PolygonUtilities
1717
/// <param name="p2">The third point.</param>
1818
/// <returns>The <see cref="double"/> area.</returns>
1919
[MethodImpl(MethodImplOptions.AggressiveInlining)]
20-
public static double SignedArea(Vertex p0, Vertex p1, Vertex p2)
20+
public static double SignedArea(in Vertex p0, in Vertex p1, in Vertex p2)
2121
=> ((p0.X - p2.X) * (p1.Y - p2.Y)) - ((p1.X - p2.X) * (p0.Y - p2.Y));
2222

2323
/// <summary>
@@ -33,7 +33,7 @@ public static double SignedArea(Vertex p0, Vertex p1, Vertex p2)
3333
/// - Returns 1 if the segments intersect at a single point.
3434
/// - Returns 2 if the segments overlap.
3535
/// </returns>
36-
public static int FindIntersection(Segment seg0, Segment seg1, out Vertex pi0, out Vertex pi1)
36+
public static int FindIntersection(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1)
3737
{
3838
pi0 = default;
3939
pi1 = default;
@@ -76,7 +76,7 @@ public static int FindIntersection(Segment seg0, Segment seg1, out Vertex pi0, o
7676
/// - Returns 1 if the segments intersect at a single point.
7777
/// - Returns 2 if the segments overlap.
7878
/// </returns>
79-
private static int FindIntersectionImpl(Segment seg0, Segment seg1, out Vertex pi0, out Vertex pi1)
79+
private static int FindIntersectionImpl(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1)
8080
{
8181
pi0 = default;
8282
pi1 = default;
@@ -175,7 +175,12 @@ private static int FindIntersectionImpl(Segment seg0, Segment seg1, out Vertex p
175175
/// <returns>
176176
/// <see langword="true"/> if the segments intersect; otherwise, <see langword="false"/>.
177177
/// </returns>
178-
private static bool TryGetIntersectionBoundingBox(Vertex a1, Vertex a2, Vertex b1, Vertex b2, [NotNullWhen(true)] out Box2? result)
178+
private static bool TryGetIntersectionBoundingBox(
179+
in Vertex a1,
180+
in Vertex a2,
181+
in Vertex b1,
182+
in Vertex b2,
183+
[NotNullWhen(true)] out Box2? result)
179184
{
180185
Vertex minA = Vertex.Min(a1, a2);
181186
Vertex maxA = Vertex.Max(a1, a2);
@@ -202,7 +207,7 @@ private static bool TryGetIntersectionBoundingBox(Vertex a1, Vertex a2, Vertex b
202207
/// <param name="bbox">The bounding box.</param>
203208
/// <returns>The constrained point.</returns>
204209
[MethodImpl(MethodImplOptions.AggressiveInlining)]
205-
private static Vertex ConstrainToBoundingBox(Vertex p, Box2 bbox)
210+
private static Vertex ConstrainToBoundingBox(in Vertex p, in Box2 bbox)
206211
=> Vertex.Min(Vertex.Max(p, bbox.Min), bbox.Max);
207212

208213
/// <summary>
@@ -213,5 +218,5 @@ private static Vertex ConstrainToBoundingBox(Vertex p, Box2 bbox)
213218
/// <param name="d">The direction vector of the segment.</param>
214219
/// <returns>The interpolated vertex at the given fractional distance.</returns>
215220
[MethodImpl(MethodImplOptions.AggressiveInlining)]
216-
public static Vertex MidPoint(Vertex p, double s, Vertex d) => p + (s * d);
221+
public static Vertex MidPoint(in Vertex p, double s, in Vertex d) => p + (s * d);
217222
}

src/PolygonClipper/Segment.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace PolygonClipper;
1717
/// <param name="source">The segment source.</param>
1818
/// <param name="target">The segment target.</param>
1919
[MethodImpl(MethodImplOptions.AggressiveInlining)]
20-
public Segment(Vertex source, Vertex target)
20+
public Segment(in Vertex source, in Vertex target)
2121
{
2222
this.Source = source;
2323
this.Target = target;
@@ -46,11 +46,11 @@ public Segment(Vertex source, Vertex target)
4646
public Vertex Max { get; }
4747

4848
[MethodImpl(MethodImplOptions.AggressiveInlining)]
49-
public static bool operator ==(Segment left, Segment right)
49+
public static bool operator ==(in Segment left, in Segment right)
5050
=> left.Equals(right);
5151

5252
[MethodImpl(MethodImplOptions.AggressiveInlining)]
53-
public static bool operator !=(Segment left, Segment right)
53+
public static bool operator !=(in Segment left, in Segment right)
5454
=> !(left == right);
5555

5656
/// <summary>

src/PolygonClipper/SegmentComparer.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ namespace PolygonClipper;
1313
/// </summary>
1414
internal sealed class SegmentComparer : IComparer<SweepEvent>, IComparer
1515
{
16-
private readonly SweepEventComparer eventComparer = new();
17-
1816
/// <inheritdoc/>
1917
public int Compare(SweepEvent? x, SweepEvent? y)
2018
{
@@ -35,7 +33,7 @@ public int Compare(SweepEvent? x, SweepEvent? y)
3533
}
3634

3735
SweepEvent perhapsInversedX, perhapsInversedY;
38-
bool inversed = false;
36+
bool inversed;
3937

4038
if (x.IsBefore(y))
4139
{

0 commit comments

Comments
 (0)