Extract shared algorithms from DefaultNestEngine and StripNestEngine into focused helper classes: PairFiller, AngleCandidateBuilder, ShrinkFiller, RemnantFiller, AccumulatingProgress. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.8 KiB
Engine Refactor: Extract Shared Algorithms from DefaultNestEngine and StripNestEngine
Problem
DefaultNestEngine (~550 lines) mixes phase orchestration with strategy-specific logic (pair candidate selection, angle building, pattern helpers). StripNestEngine (~450 lines) duplicates patterns that DefaultNestEngine also uses: shrink-to-fit loops, iterative remnant filling, and progress accumulation. Both engines would benefit from extracting shared algorithms into focused, reusable classes.
Approach
Extract five classes from the two engines. No new interfaces or strategy patterns — just focused helper classes that each engine composes.
Extracted Classes
1. PairFiller
Source: DefaultNestEngine lines 362-462 (FillWithPairs, SelectPairCandidates, BuildRemainderPatterns, MinPairCandidates, PairTimeLimit).
API:
public class PairFiller
{
public PairFiller(Size plateSize, double partSpacing) { }
public List<Part> Fill(NestItem item, Box workArea,
CancellationToken token = default,
IProgress<NestProgress> progress = null);
}
Details:
- Constructor takes plate size and spacing — decoupled from
Plateobject. SelectPairCandidatesandBuildRemainderPatternsbecome private methods.- Uses
BestFitCache.GetOrCompute()internally (same as today). - Calls
BuildRotatedPatternandFillPattern— these becomeinternal staticmethods on DefaultNestEngine so PairFiller can call them without ceremony. - Returns
List<Part>(empty list if no result), same contract as today.
Caller: DefaultNestEngine.FindBestFill replaces this.FillWithPairs(...) with new PairFiller(Plate.Size, Plate.PartSpacing).Fill(...).
2. AngleCandidateBuilder
Source: DefaultNestEngine lines 279-347 (BuildCandidateAngles, knownGoodAngles HashSet, ForceFullAngleSweep property).
API:
public class AngleCandidateBuilder
{
public bool ForceFullSweep { get; set; }
public List<double> Build(NestItem item, double bestRotation, Box workArea);
public void RecordProductive(List<AngleResult> angleResults);
}
Details:
- Owns
knownGoodAnglesstate — lives as long as the engine instance so pruning accumulates across fills. Build()encapsulates the full pipeline: base angles, sweep check, ML prediction, known-good pruning.RecordProductive()replaces the inline loop that feedsknownGoodAnglesafter the linear phase.ForceFullAngleSweepmoves from DefaultNestEngine to here asForceFullSweep.
Caller: DefaultNestEngine creates one AngleCandidateBuilder instance in its constructor (or as a field) and calls Build()/RecordProductive() from FindBestFill.
3. ShrinkFiller
Source: StripNestEngine TryOrientation shrink loop (lines 188-215) and ShrinkFill (lines 358-418).
API:
public static class ShrinkFiller
{
public static ShrinkResult Shrink(
Func<NestItem, Box, List<Part>> fillFunc,
NestItem item, Box box,
double spacing,
ShrinkAxis axis,
CancellationToken token = default,
int maxIterations = 20);
}
public enum ShrinkAxis { Width, Height }
public class ShrinkResult
{
public List<Part> Parts { get; set; }
public double Dimension { get; set; }
}
Details:
fillFuncdelegate decouples ShrinkFiller from any specific engine — the caller provides how to fill.ShrinkAxisdetermines which dimension to reduce.TryOrientationpasses the axis matching its strip direction.ShrinkFillcallsShrinktwice (width then height).- Loop logic: fill initial box, measure placed bounding box, reduce dimension by
spacing, retry until count drops below initial count. - Returns both the best parts and the final tight dimension (needed by
TryOrientationto compute the remnant box).
Callers:
StripNestEngine.TryOrientationreplaces its inline shrink loop.StripNestEngine.ShrinkFillreplaces its two-axis inline shrink loops.
4. RemnantFiller
Source: StripNestEngine remnant loop (lines 262-342) and the simpler version in NestEngineBase.Nest (lines 74-97).
API:
public class RemnantFiller
{
public RemnantFiller(Box workArea, double spacing) { }
public void AddObstacles(IEnumerable<Part> parts);
public List<Part> FillItems(
List<NestItem> items,
Func<NestItem, Box, List<Part>> fillFunc,
CancellationToken token = default,
IProgress<NestProgress> progress = null);
}
Details:
- Owns a
RemnantFinderinstance internally. AddObstaclesregisters already-placed parts (bounding boxes offset by spacing).FillItemsruns the iterative loop: find remnants, try each item in each remnant, fill, update obstacles, repeat until no progress.- Local quantity tracking (dictionary keyed by drawing name) stays internal — does not mutate the input
NestItemquantities. Returns the placed parts; the caller deducts quantities. - Uses minimum-remnant-size filtering (smallest remaining part dimension), same as StripNestEngine today.
fillFuncdelegate allows callers to provide any fill strategy (DefaultNestEngine.Fill, ShrinkFill, etc.).
Callers:
StripNestEngine.TryOrientationreplaces its inline remnant loop withRemnantFiller.FillItems(...).NestEngineBase.Nestreplaces its hand-rolled largest-remnant loop, gaining multi-remnant and minimum-size filtering for free.
5. AccumulatingProgress
Source: StripNestEngine nested class (lines 425-449).
API:
internal class AccumulatingProgress : IProgress<NestProgress>
{
public AccumulatingProgress(IProgress<NestProgress> inner, List<Part> previousParts) { }
public void Report(NestProgress value);
}
Details:
- Moved from private nested class to standalone
internalclass in OpenNest.Engine. - No behavioral change — wraps an
IProgress<NestProgress>and prepends previously placed parts to each report.
What Stays on Each Engine
DefaultNestEngine (~200 lines after extraction)
Fill(NestItem, Box, ...)— public entry point, unchanged.Fill(List<Part>, Box, ...)— group-parts overload, unchanged.PackArea— bin packing delegation, unchanged.FindBestFill— orchestration, now ~30 lines: callsAngleCandidateBuilder.Build(),PairFiller.Fill(), linear angle loop,FillRectangleBestFit, picks best.FillRectangleBestFit— 6-line private method, too small to extract.BuildRotatedPattern/FillPattern— becomeinternal static, used by both the linear loop and PairFiller.QuickFillCount— stays (used by binary search, not shared).
StripNestEngine (~200 lines after extraction)
Nest— orchestration, unchanged.TryOrientation— becomes thinner: callsDefaultNestEngine.Fillfor initial fill,ShrinkFiller.Shrink()for tightening,RemnantFiller.FillItems()for remnants.ShrinkFill— replaced by twoShrinkFiller.Shrink()calls.SelectStripItemIndex/EstimateStripDimension— stay private, strip-specific.AccumulatingProgress— removed, uses shared class.
NestEngineBase
Nest— switches from hand-rolled remnant loop toRemnantFiller.FillItems().- All other methods unchanged.
File Layout
All new classes go in OpenNest.Engine/:
OpenNest.Engine/
PairFiller.cs
AngleCandidateBuilder.cs
ShrinkFiller.cs
RemnantFiller.cs
AccumulatingProgress.cs
Non-Goals
- No new interfaces or strategy patterns.
- No changes to FillLinear, FillBestFit, PackBottomLeft, or any other existing algorithm.
- No changes to NestEngineRegistry or the plugin system.
- No changes to public API surface — all existing callers continue to work unchanged.
- PatternHelper extraction deferred —
BuildRotatedPattern/FillPatternbecomeinternal staticon DefaultNestEngine for now. Extract if a third consumer appears.