From b2d92aefbe5cc3a76b63b449cdd36ceac413d29a Mon Sep 17 00:00:00 2001 From: xaviergonz Date: Thu, 15 Oct 2026 00:30:14 +0200 Subject: [PATCH] Skip the intersection merge sort when the active edges are still sorted BuildIntersectList's bottom-up merge sort always makes about log2(n) passes over the active edge list, even when no edges cross in the scanbeam, which is the usual case. AdjustCurrXAndCopyToSEL now also checks whether the edges are still in order, and if so there's nothing to find, so the merge sort is skipped. In that case every edge's curr_x is already TopX(top_y), so DoTopOfScanbeam doesn't recompute it either. Output is unchanged. Done in all three ports. --- .../include/clipper2/clipper.engine.h | 3 ++- CPP/Clipper2Lib/src/clipper.engine.cpp | 22 +++++++++++++--- CSharp/Clipper2Lib/Clipper.Engine.cs | 23 +++++++++++++--- Delphi/Clipper2Lib/Clipper.Engine.pas | 26 +++++++++++++++---- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h index 0e9c74798..df0641317 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h @@ -195,6 +195,7 @@ namespace Clipper2Lib { FillRule fillrule_ = FillRule::EvenOdd; FillRule fillpos = FillRule::Positive; int64_t bot_y_ = 0; + bool curr_x_at_top_ = false; // every active edge's curr_x is already TopX(top_y) bool minima_list_sorted_ = false; bool using_polytree_ = false; Active* actives_ = nullptr; @@ -226,7 +227,7 @@ namespace Clipper2Lib { inline void UpdateEdgeIntoAEL(Active *e); void IntersectEdges(Active &e1, Active &e2, const Point64& pt); inline void DeleteFromAEL(Active &e); - inline void AdjustCurrXAndCopyToSEL(const int64_t top_y); + inline bool AdjustCurrXAndCopyToSEL(const int64_t top_y); void DoIntersections(const int64_t top_y); void AddNewIntersectNode(Active &e1, Active &e2, const int64_t top_y); bool BuildIntersectList(const int64_t top_y); diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 6442c96ad..97ad51dc1 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -2099,10 +2099,13 @@ namespace Clipper2Lib { } - inline void ClipperBase::AdjustCurrXAndCopyToSEL(const int64_t top_y) + // returns true when the edges are still sorted by curr_x (ie no intersections) + inline bool ClipperBase::AdjustCurrXAndCopyToSEL(const int64_t top_y) { Active* e = actives_; sel_ = e; + bool sorted = true; + int64_t prev_x = std::numeric_limits::min(); while (e) { e->prev_in_sel = e->prev_in_ael; @@ -2110,9 +2113,13 @@ namespace Clipper2Lib { e->jump = e->next_in_sel; // it is safe to ignore 'joined' edges here because // if necessary they will be split in IntersectEdges() - e->curr_x = TopX(*e, top_y); + int64_t x = TopX(*e, top_y); + e->curr_x = x; + if (x < prev_x) sorted = false; + prev_x = x; e = e->next_in_ael; } + return sorted; } bool ClipperBase::ExecuteInternal(ClipType ct, FillRule fillrule, bool use_polytrees) @@ -2335,6 +2342,7 @@ namespace Clipper2Lib { void ClipperBase::DoIntersections(const int64_t top_y) { + curr_x_at_top_ = false; if (BuildIntersectList(top_y)) { ProcessIntersectList(); @@ -2382,7 +2390,13 @@ namespace Clipper2Lib { //Calculate edge positions at the top of the current scanbeam, and from this //we will determine the intersections required to reach these new positions. - AdjustCurrXAndCopyToSEL(top_y); + //If the edges are still in order there are no intersections, so the merge + //sort (which always does log2(n) passes over the edges) can be skipped. + if (AdjustCurrXAndCopyToSEL(top_y)) + { + curr_x_at_top_ = true; + return false; + } //Find all edge intersections in the current scanbeam using a stable merge //sort that ensures only adjacent edges are intersecting. Intersect info is //stored in FIntersectList ready to be processed in ProcessIntersectList. @@ -2718,7 +2732,7 @@ namespace Clipper2Lib { PushHorz(*e); // horizontals are processed later } } - else // i.e. not the top of the edge + else if (!curr_x_at_top_) // i.e. not the top of the edge e->curr_x = TopX(*e, y); e = e->next_in_ael; diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index 481e723af..39e3b1d48 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -367,6 +367,7 @@ public class ClipperBase private readonly OutPtPoolList _outPtPool; private int _currentLocMin; private long _currentBotY; + private bool _currXAtTop; // every active edge's curX is already TopX(topY) private bool _isSortedMinimaList; private bool _hasOpenPaths; internal bool _using_polytree; @@ -1855,11 +1856,14 @@ private Active NewActive() return ae; } + // returns true when the edges are still sorted by curX (ie no intersections) [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AdjustCurrXAndCopyToSEL(long topY) + private bool AdjustCurrXAndCopyToSEL(long topY) { Active? ae = _actives; _sel = ae; + bool sorted = true; + long prevX = long.MinValue; while (ae != null) { ae.prevInSEL = ae.prevInAEL; @@ -1867,10 +1871,14 @@ private void AdjustCurrXAndCopyToSEL(long topY) ae.jump = ae.nextInSEL; // it is safe to ignore 'joined' edges here because // if necessary they will be split in IntersectEdges() - ae.curX = TopX(ae, topY); + long x = TopX(ae, topY); + ae.curX = x; + if (x < prevX) sorted = false; + prevX = x; // NB don't update ae.curr.Y yet (see AddNewIntersectNode) ae = ae.nextInAEL; } + return sorted; } protected void ExecuteInternal(ClipType ct, FillRule fillRule) @@ -1903,6 +1911,7 @@ protected void ExecuteInternal(ClipType ct, FillRule fillRule) [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DoIntersections(long topY) { + _currXAtTop = false; if (!BuildIntersectList(topY)) return; ProcessIntersectList(); DisposeIntersectNodes(); @@ -1984,7 +1993,13 @@ private bool BuildIntersectList(long topY) // Calculate edge positions at the top of the current scanbeam, and from this // we will determine the intersections required to reach these new positions. - AdjustCurrXAndCopyToSEL(topY); + // If the edges are still in order there are no intersections, so the merge + // sort (which always does log2(n) passes over the edges) can be skipped. + if (AdjustCurrXAndCopyToSEL(topY)) + { + _currXAtTop = true; + return false; + } // Find all edge intersections in the current scanbeam using a stable merge // sort that ensures only adjacent edges are intersecting. Intersect info is @@ -2331,7 +2346,7 @@ private void DoTopOfScanbeam(long y) if (IsHorizontal(ae)) PushHorz(ae); // horizontals are processed later } - else // i.e. not the top of the edge + else if (!_currXAtTop) // i.e. not the top of the edge ae.curX = TopX(ae, y); ae = ae.nextInAEL; diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index fba641eae..b5147375a 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -198,6 +198,7 @@ TScanLine = record TClipperBase = class {$IFDEF STRICT}strict{$ENDIF} private FBotY : Int64; + FCurrXAtTop : Boolean; // every active edge's currX is already TopX(topY) FScanLine : PScanLine; FCurrentLocMinIdx : Integer; FClipType : TClipType; @@ -242,7 +243,7 @@ TClipperBase = class procedure IntersectEdges(e1, e2: PActive; pt: TPoint64); procedure DeleteEdges(var e: PActive); procedure DeleteFromAEL(e: PActive); - procedure AdjustCurrXAndCopyToSEL(topY: Int64); + function AdjustCurrXAndCopyToSEL(topY: Int64): Boolean; procedure ConvertHorzSegsToJoins; procedure ProcessHorzJoins; procedure DoIntersections(const topY: Int64); @@ -2823,10 +2824,14 @@ procedure TClipperBase.DeleteFromAEL(e: PActive); end; //------------------------------------------------------------------------------ -procedure TClipperBase.AdjustCurrXAndCopyToSEL(topY: Int64); +function TClipperBase.AdjustCurrXAndCopyToSEL(topY: Int64): Boolean; var e: PActive; + x, prevX: Int64; begin + // returns true when the edges are still sorted by currX (ie no intersections) + Result := true; + prevX := Low(Int64); FSel := FActives; e := FActives; while Assigned(e) do @@ -2836,7 +2841,10 @@ procedure TClipperBase.AdjustCurrXAndCopyToSEL(topY: Int64); e.jump := e.nextInSEL; // it is safe to ignore 'joined' edges here because // if necessary they will be split in IntersectEdges() - e.currX := TopX(e, topY); + x := TopX(e, topY); + e.currX := x; + if x < prevX then Result := false; + prevX := x; e := e.nextInAEL; end; end; @@ -3133,6 +3141,7 @@ procedure TClipperBase.ProcessHorzJoins; procedure TClipperBase.DoIntersections(const topY: Int64); begin + FCurrXAtTop := false; if BuildIntersectList(topY) then try ProcessIntersectList; @@ -3221,7 +3230,13 @@ function TClipperBase.BuildIntersectList(const topY: Int64): Boolean; // Calculate edge positions at the top of the current scanbeam, and from this // we will determine the intersections required to reach these new positions. - AdjustCurrXAndCopyToSEL(topY); + // If the edges are still in order there are no intersections, so the merge + // sort (which always does log2(n) passes over the edges) can be skipped. + if AdjustCurrXAndCopyToSEL(topY) then + begin + FCurrXAtTop := true; + Exit; + end; // Find all edge intersections in the current scanbeam using a stable merge // sort that ensures only adjacent edges are intersecting. Intersect info is @@ -3604,7 +3619,8 @@ procedure TClipperBase.DoTopOfScanbeam(Y: Int64); if IsHorizontal(e) then PushHorz(e); end; - end else + end + else if not FCurrXAtTop then // i.e. not the top of the edge e.currX := TopX(e, Y); e := e.nextInAEL; end;