feat: implement StripeFiller.Fill with pair iteration, stripe tiling, and remnant fill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Engine.Strategies;
|
using OpenNest.Engine.Strategies;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
@@ -25,8 +26,131 @@ public class StripeFiller
|
|||||||
|
|
||||||
public List<Part> Fill()
|
public List<Part> Fill()
|
||||||
{
|
{
|
||||||
// Placeholder — implemented in Task 3
|
var bestFits = GetPairCandidates();
|
||||||
return new List<Part>();
|
if (bestFits.Count == 0)
|
||||||
|
return new List<Part>();
|
||||||
|
|
||||||
|
var workArea = _context.WorkArea;
|
||||||
|
var spacing = _context.Plate.PartSpacing;
|
||||||
|
var drawing = _context.Item.Drawing;
|
||||||
|
var perpAxis = _primaryAxis == NestDirection.Horizontal
|
||||||
|
? NestDirection.Vertical
|
||||||
|
: NestDirection.Horizontal;
|
||||||
|
var sheetSpan = GetDimension(workArea, _primaryAxis);
|
||||||
|
var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column";
|
||||||
|
|
||||||
|
List<Part> bestParts = null;
|
||||||
|
var bestScore = default(FillScore);
|
||||||
|
|
||||||
|
for (var i = 0; i < bestFits.Count; i++)
|
||||||
|
{
|
||||||
|
_context.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var candidate = bestFits[i];
|
||||||
|
var pairParts = candidate.BuildParts(drawing);
|
||||||
|
|
||||||
|
var (angle, waste, count) = ConvergeStripeAngle(
|
||||||
|
pairParts, sheetSpan, spacing, _primaryAxis, _context.Token);
|
||||||
|
|
||||||
|
if (count <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||||
|
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
||||||
|
var stripeBox = MakeStripeBox(workArea, perpDim, _primaryAxis);
|
||||||
|
var stripeEngine = new FillLinear(stripeBox, spacing);
|
||||||
|
var stripeParts = stripeEngine.Fill(rotatedPattern, _primaryAxis);
|
||||||
|
|
||||||
|
if (stripeParts == null || stripeParts.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var stripePattern = new Pattern();
|
||||||
|
stripePattern.Parts.AddRange(stripeParts);
|
||||||
|
stripePattern.UpdateBounds();
|
||||||
|
|
||||||
|
var gridEngine = new FillLinear(workArea, spacing);
|
||||||
|
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
|
||||||
|
|
||||||
|
if (gridParts == null || gridParts.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var allParts = new List<Part>(gridParts);
|
||||||
|
var remnantParts = FillRemnant(gridParts, drawing, angle, workArea, spacing);
|
||||||
|
if (remnantParts != null)
|
||||||
|
allParts.AddRange(remnantParts);
|
||||||
|
|
||||||
|
var score = FillScore.Compute(allParts, workArea);
|
||||||
|
if (bestParts == null || score > bestScore)
|
||||||
|
{
|
||||||
|
bestParts = allParts;
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom,
|
||||||
|
_context.PlateNumber, bestParts, workArea,
|
||||||
|
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestScore.Count} parts");
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestParts ?? new List<Part>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BestFitResult> GetPairCandidates()
|
||||||
|
{
|
||||||
|
List<BestFitResult> bestFits;
|
||||||
|
|
||||||
|
if (_context.SharedState.TryGetValue("BestFits", out var cached))
|
||||||
|
bestFits = (List<BestFitResult>)cached;
|
||||||
|
else
|
||||||
|
bestFits = BestFitCache.GetOrCompute(
|
||||||
|
_context.Item.Drawing,
|
||||||
|
_context.Plate.Size.Length,
|
||||||
|
_context.Plate.Size.Width,
|
||||||
|
_context.Plate.PartSpacing);
|
||||||
|
|
||||||
|
return bestFits
|
||||||
|
.Where(r => r.Keep)
|
||||||
|
.Take(MaxPairCandidates)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Box MakeStripeBox(Box workArea, double perpDim, NestDirection primaryAxis)
|
||||||
|
{
|
||||||
|
return primaryAxis == NestDirection.Horizontal
|
||||||
|
? new Box(workArea.X, workArea.Y, workArea.Width, perpDim)
|
||||||
|
: new Box(workArea.X, workArea.Y, perpDim, workArea.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Part> FillRemnant(
|
||||||
|
List<Part> gridParts, Drawing drawing, double angle,
|
||||||
|
Box workArea, double spacing)
|
||||||
|
{
|
||||||
|
var gridBox = gridParts.GetBoundingBox();
|
||||||
|
var minDim = System.Math.Min(
|
||||||
|
drawing.Program.BoundingBox().Width,
|
||||||
|
drawing.Program.BoundingBox().Length);
|
||||||
|
|
||||||
|
Box remnantBox;
|
||||||
|
|
||||||
|
if (_primaryAxis == NestDirection.Horizontal)
|
||||||
|
{
|
||||||
|
var remnantY = gridBox.Top + spacing;
|
||||||
|
var remnantLength = workArea.Top - remnantY;
|
||||||
|
if (remnantLength < minDim)
|
||||||
|
return null;
|
||||||
|
remnantBox = new Box(workArea.X, remnantY, workArea.Width, remnantLength);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var remnantX = gridBox.Right + spacing;
|
||||||
|
var remnantWidth = workArea.Right - remnantX;
|
||||||
|
if (remnantWidth < minDim)
|
||||||
|
return null;
|
||||||
|
remnantBox = new Box(remnantX, workArea.Y, remnantWidth, workArea.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var engine = new FillLinear(remnantBox, spacing);
|
||||||
|
var parts = engine.Fill(drawing, angle, _primaryAxis);
|
||||||
|
return parts != null && parts.Count > 0 ? parts : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double FindAngleForTargetSpan(
|
public static double FindAngleForTargetSpan(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.Engine.BestFit;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Engine.Strategies;
|
using OpenNest.Engine.Strategies;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
@@ -27,6 +29,43 @@ public class StripeFillerTests
|
|||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a simple side-by-side pair BestFitResult for a rectangular drawing.
|
||||||
|
/// Places two copies next to each other along the X axis with the given spacing.
|
||||||
|
/// </summary>
|
||||||
|
private static List<BestFitResult> MakeSideBySideBestFits(
|
||||||
|
Drawing drawing, double spacing)
|
||||||
|
{
|
||||||
|
var bb = drawing.Program.BoundingBox();
|
||||||
|
var w = bb.Width;
|
||||||
|
var h = bb.Length;
|
||||||
|
|
||||||
|
var candidate = new PairCandidate
|
||||||
|
{
|
||||||
|
Drawing = drawing,
|
||||||
|
Part1Rotation = 0,
|
||||||
|
Part2Rotation = 0,
|
||||||
|
Part2Offset = new Vector(w + spacing, 0),
|
||||||
|
Spacing = spacing,
|
||||||
|
};
|
||||||
|
|
||||||
|
var pairWidth = 2 * w + spacing;
|
||||||
|
var result = new BestFitResult
|
||||||
|
{
|
||||||
|
Candidate = candidate,
|
||||||
|
BoundingWidth = pairWidth,
|
||||||
|
BoundingHeight = h,
|
||||||
|
RotatedArea = pairWidth * h,
|
||||||
|
TrueArea = 2 * w * h,
|
||||||
|
OptimalRotation = 0,
|
||||||
|
Keep = true,
|
||||||
|
Reason = "Valid",
|
||||||
|
HullAngles = new List<double>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new List<BestFitResult> { result };
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FindAngleForTargetSpan_ZeroAngle_WhenAlreadyMatches()
|
public void FindAngleForTargetSpan_ZeroAngle_WhenAlreadyMatches()
|
||||||
{
|
{
|
||||||
@@ -92,4 +131,86 @@ public class StripeFillerTests
|
|||||||
|
|
||||||
Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}");
|
Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_ProducesPartsForSimpleDrawing()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120) { PartSpacing = 0.5 };
|
||||||
|
var drawing = MakeRectDrawing(20, 10);
|
||||||
|
var item = new NestItem { Drawing = drawing };
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
|
||||||
|
var bestFits = MakeSideBySideBestFits(drawing, 0.5);
|
||||||
|
|
||||||
|
var context = new OpenNest.Engine.Strategies.FillContext
|
||||||
|
{
|
||||||
|
Item = item,
|
||||||
|
WorkArea = workArea,
|
||||||
|
Plate = plate,
|
||||||
|
PlateNumber = 0,
|
||||||
|
Token = System.Threading.CancellationToken.None,
|
||||||
|
Progress = null,
|
||||||
|
};
|
||||||
|
context.SharedState["BestFits"] = bestFits;
|
||||||
|
|
||||||
|
var filler = new StripeFiller(context, NestDirection.Horizontal);
|
||||||
|
var parts = filler.Fill();
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
Assert.True(parts.Count > 0, "Expected parts from stripe fill");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_VerticalProducesParts()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120) { PartSpacing = 0.5 };
|
||||||
|
var drawing = MakeRectDrawing(20, 10);
|
||||||
|
var item = new NestItem { Drawing = drawing };
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
|
||||||
|
var bestFits = MakeSideBySideBestFits(drawing, 0.5);
|
||||||
|
|
||||||
|
var context = new OpenNest.Engine.Strategies.FillContext
|
||||||
|
{
|
||||||
|
Item = item,
|
||||||
|
WorkArea = workArea,
|
||||||
|
Plate = plate,
|
||||||
|
PlateNumber = 0,
|
||||||
|
Token = System.Threading.CancellationToken.None,
|
||||||
|
Progress = null,
|
||||||
|
};
|
||||||
|
context.SharedState["BestFits"] = bestFits;
|
||||||
|
|
||||||
|
var filler = new StripeFiller(context, NestDirection.Vertical);
|
||||||
|
var parts = filler.Fill();
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
Assert.True(parts.Count > 0, "Expected parts from column fill");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fill_ReturnsEmpty_WhenNoBestFits()
|
||||||
|
{
|
||||||
|
var plate = new Plate(60, 120) { PartSpacing = 0.5 };
|
||||||
|
var drawing = MakeRectDrawing(20, 10);
|
||||||
|
var item = new NestItem { Drawing = drawing };
|
||||||
|
var workArea = new Box(0, 0, 120, 60);
|
||||||
|
|
||||||
|
var context = new OpenNest.Engine.Strategies.FillContext
|
||||||
|
{
|
||||||
|
Item = item,
|
||||||
|
WorkArea = workArea,
|
||||||
|
Plate = plate,
|
||||||
|
PlateNumber = 0,
|
||||||
|
Token = System.Threading.CancellationToken.None,
|
||||||
|
Progress = null,
|
||||||
|
};
|
||||||
|
context.SharedState["BestFits"] = new List<OpenNest.Engine.BestFit.BestFitResult>();
|
||||||
|
|
||||||
|
var filler = new StripeFiller(context, NestDirection.Horizontal);
|
||||||
|
var parts = filler.Fill();
|
||||||
|
|
||||||
|
Assert.NotNull(parts);
|
||||||
|
Assert.Empty(parts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user