refactor(engine): introduce PairFillResult and remove FillRemainingStrip
PairFiller now returns PairFillResult (Parts + BestFits) instead of using a mutable BestFits property. Extracted EvaluateCandidates, TryReduceWorkArea, and BuildTilingAngles for clarity. Simplified the candidate loop by leveraging FillScore comparison semantics. Removed FillRemainingStrip and all its helpers (FindPlacedEdge, BuildRemainingStrip, BuildRotationSet, FindBestFill, TryFewerRows, RemainderPatterns) from FillLinear — these were a major bottleneck in strip nesting, running expensive fills on undersized remnant strips. ShrinkFiller + RemnantFiller already handle space optimization, making the remainder strip fill redundant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,11 +19,6 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
public double HalfSpacing => PartSpacing / 2;
|
||||
|
||||
/// <summary>
|
||||
/// Optional multi-part patterns (e.g. interlocking pairs) to try in remainder strips.
|
||||
/// </summary>
|
||||
public List<Pattern> RemainderPatterns { get; set; }
|
||||
|
||||
private static Vector MakeOffset(NestDirection direction, double distance)
|
||||
{
|
||||
return direction == NestDirection.Horizontal
|
||||
@@ -346,215 +341,9 @@ namespace OpenNest.Engine.Fill
|
||||
var gridResult = new List<Part>(rowPattern.Parts);
|
||||
gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries));
|
||||
|
||||
// Step 3: Fill remaining strip
|
||||
var remaining = FillRemainingStrip(gridResult, pattern, perpAxis, direction);
|
||||
if (remaining.Count > 0)
|
||||
gridResult.AddRange(remaining);
|
||||
|
||||
// Step 4: Try fewer rows optimization
|
||||
var fewerResult = TryFewerRows(gridResult, rowPattern, pattern, perpAxis, direction);
|
||||
if (fewerResult != null && fewerResult.Count > gridResult.Count)
|
||||
return fewerResult;
|
||||
|
||||
return gridResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries removing the last row/column from the grid and re-filling the
|
||||
/// larger remainder strip. Returns null if this doesn't improve the total.
|
||||
/// </summary>
|
||||
private List<Part> TryFewerRows(
|
||||
List<Part> fullResult, Pattern rowPattern, Pattern seedPattern,
|
||||
NestDirection tiledAxis, NestDirection primaryAxis)
|
||||
{
|
||||
var rowPartCount = rowPattern.Parts.Count;
|
||||
|
||||
if (fullResult.Count < rowPartCount * 2)
|
||||
return null;
|
||||
|
||||
var fewerParts = new List<Part>(fullResult.Count - rowPartCount);
|
||||
|
||||
for (var i = 0; i < fullResult.Count - rowPartCount; i++)
|
||||
fewerParts.Add(fullResult[i]);
|
||||
|
||||
var remaining = FillRemainingStrip(fewerParts, seedPattern, tiledAxis, primaryAxis);
|
||||
|
||||
if (remaining.Count <= rowPartCount)
|
||||
return null;
|
||||
|
||||
fewerParts.AddRange(remaining);
|
||||
return fewerParts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After tiling full rows/columns, fills the remaining strip with individual
|
||||
/// parts. The strip is the leftover space along the tiled axis between the
|
||||
/// last full row/column and the work area boundary. Each unique drawing and
|
||||
/// rotation from the seed pattern is tried in both directions.
|
||||
/// </summary>
|
||||
private List<Part> FillRemainingStrip(
|
||||
List<Part> placedParts, Pattern seedPattern,
|
||||
NestDirection tiledAxis, NestDirection primaryAxis)
|
||||
{
|
||||
var placedEdge = FindPlacedEdge(placedParts, tiledAxis);
|
||||
var remainingStrip = BuildRemainingStrip(placedEdge, tiledAxis);
|
||||
|
||||
if (remainingStrip == null)
|
||||
return new List<Part>();
|
||||
|
||||
var rotations = BuildRotationSet(seedPattern);
|
||||
var best = FindBestFill(rotations, remainingStrip);
|
||||
|
||||
if (RemainderPatterns != null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Strip: {remainingStrip.Width:F1}x{remainingStrip.Length:F1}, individual best={best?.Count ?? 0}, trying {RemainderPatterns.Count} patterns");
|
||||
|
||||
foreach (var pattern in RemainderPatterns)
|
||||
{
|
||||
var filler = new FillLinear(remainingStrip, PartSpacing);
|
||||
var h = filler.Fill(pattern, NestDirection.Horizontal);
|
||||
var v = filler.Fill(pattern, NestDirection.Vertical);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Pattern ({pattern.Parts.Count} parts, bbox={pattern.BoundingBox.Width:F1}x{pattern.BoundingBox.Length:F1}): H={h?.Count ?? 0}, V={v?.Count ?? 0}");
|
||||
|
||||
if (h != null && h.Count > (best?.Count ?? 0))
|
||||
best = h;
|
||||
if (v != null && v.Count > (best?.Count ?? 0))
|
||||
best = v;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Final best={best?.Count ?? 0}");
|
||||
}
|
||||
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
private static double FindPlacedEdge(List<Part> placedParts, NestDirection tiledAxis)
|
||||
{
|
||||
var placedEdge = double.MinValue;
|
||||
|
||||
foreach (var part in placedParts)
|
||||
{
|
||||
var edge = tiledAxis == NestDirection.Vertical
|
||||
? part.BoundingBox.Top
|
||||
: part.BoundingBox.Right;
|
||||
|
||||
if (edge > placedEdge)
|
||||
placedEdge = edge;
|
||||
}
|
||||
|
||||
return placedEdge;
|
||||
}
|
||||
|
||||
private Box BuildRemainingStrip(double placedEdge, NestDirection tiledAxis)
|
||||
{
|
||||
if (tiledAxis == NestDirection.Vertical)
|
||||
{
|
||||
var bottom = placedEdge + PartSpacing;
|
||||
var height = WorkArea.Top - bottom;
|
||||
|
||||
if (height <= Tolerance.Epsilon)
|
||||
return null;
|
||||
|
||||
return new Box(WorkArea.X, bottom, WorkArea.Width, height);
|
||||
}
|
||||
else
|
||||
{
|
||||
var left = placedEdge + PartSpacing;
|
||||
var width = WorkArea.Right - left;
|
||||
|
||||
if (width <= Tolerance.Epsilon)
|
||||
return null;
|
||||
|
||||
return new Box(left, WorkArea.Y, width, WorkArea.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a set of (drawing, rotation) candidates: cardinal orientations
|
||||
/// (0° and 90°) for each unique drawing, plus any seed pattern rotations
|
||||
/// not already covered.
|
||||
/// </summary>
|
||||
private static List<(Drawing drawing, double rotation)> BuildRotationSet(Pattern seedPattern)
|
||||
{
|
||||
var rotations = new List<(Drawing drawing, double rotation)>();
|
||||
var drawings = new List<Drawing>();
|
||||
|
||||
foreach (var seedPart in seedPattern.Parts)
|
||||
{
|
||||
var found = false;
|
||||
|
||||
foreach (var d in drawings)
|
||||
{
|
||||
if (d == seedPart.BaseDrawing)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
drawings.Add(seedPart.BaseDrawing);
|
||||
}
|
||||
|
||||
foreach (var drawing in drawings)
|
||||
{
|
||||
rotations.Add((drawing, 0));
|
||||
rotations.Add((drawing, Angle.HalfPI));
|
||||
}
|
||||
|
||||
foreach (var seedPart in seedPattern.Parts)
|
||||
{
|
||||
var skip = false;
|
||||
|
||||
foreach (var (d, r) in rotations)
|
||||
{
|
||||
if (d == seedPart.BaseDrawing && r.IsEqualTo(seedPart.Rotation))
|
||||
{
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip)
|
||||
rotations.Add((seedPart.BaseDrawing, seedPart.Rotation));
|
||||
}
|
||||
|
||||
return rotations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries all rotation candidates in both directions in parallel, returns the
|
||||
/// fill with the most parts.
|
||||
/// </summary>
|
||||
private List<Part> FindBestFill(List<(Drawing drawing, double rotation)> rotations, Box strip)
|
||||
{
|
||||
var bag = new System.Collections.Concurrent.ConcurrentBag<List<Part>>();
|
||||
|
||||
Parallel.ForEach(rotations, entry =>
|
||||
{
|
||||
var filler = new FillLinear(strip, PartSpacing);
|
||||
var h = filler.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal);
|
||||
var v = filler.Fill(entry.drawing, entry.rotation, NestDirection.Vertical);
|
||||
|
||||
if (h != null && h.Count > 0)
|
||||
bag.Add(h);
|
||||
|
||||
if (v != null && v.Count > 0)
|
||||
bag.Add(v);
|
||||
});
|
||||
|
||||
List<Part> best = null;
|
||||
|
||||
foreach (var candidate in bag)
|
||||
{
|
||||
if (best == null || candidate.Count > best.Count)
|
||||
best = candidate;
|
||||
}
|
||||
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a single row of identical parts along one axis using geometry-aware spacing.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user