Files
OpenNest/OpenNest.Engine.Tests/Jobs/PlateFillerContractTests.cs
T
2026-11-12 19:38:20 -05:00

442 lines
15 KiB
C#

using System.Threading;
using OpenNest.Clamps;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.Jobs;
public class PlateFillerContractTests
{
[Theory]
[InlineData("Fill")]
[InlineData("Vertical Remnant")]
[InlineData("Horizontal Remnant")]
public void StandardPlateFiller_Fill_ReturnsPartsBoundToInputDrawing(string strategy)
{
var plate = new Plate(new Size(30, 50));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
var filler = CreateFiller(strategy, plate);
var parts = filler.Fill(
new NestItem { Drawing = drawing, Quantity = 4 },
plate.WorkArea(),
null,
CancellationToken.None
);
Assert.NotEmpty(parts);
Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
Assert.NotEmpty(filler.PhaseResults);
}
[Fact]
public void PlateFiller_UsesOverriddenAngleSelection()
{
var plate = new Plate(new Size(30, 50));
var filler = new AngleProbePlateFiller(plate);
var parts = filler.Fill(
new NestItem
{
Drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4)),
Quantity = 4,
},
plate.WorkArea(),
null,
CancellationToken.None
);
Assert.NotEmpty(parts);
Assert.True(filler.BuildAnglesCalled);
}
[Fact]
public void PlateFiller_Nest_UsesOverriddenFillAndPackArea()
{
var plate = new Plate(new Size(100, 100));
var fillDrawing = new Drawing("fill", TestDrawingFactory.Rectangle(10, 10));
var packDrawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
var filler = new FillAndPackProbePlateFiller(plate, fillDrawing, packDrawing);
var parts = filler.Nest(
new List<NestItem>
{
new() { Drawing = fillDrawing, Quantity = 10 },
new() { Drawing = packDrawing, Quantity = 1 },
},
null,
CancellationToken.None
);
Assert.Equal(1, filler.FillCalls);
Assert.Equal(1, filler.PackAreaCalls);
Assert.Equal(11, parts.Count);
}
[Fact]
public void StripLayout_Nest_ReturnsPlacedPartsAndDeductsInput()
{
var plate = new Plate(new Size(30, 50));
var items = new List<NestItem>
{
new()
{
Drawing = new Drawing("rect-a", TestDrawingFactory.Rectangle(6, 4)),
Quantity = 5,
},
new()
{
Drawing = new Drawing("rect-b", TestDrawingFactory.Rectangle(4, 3)),
Quantity = 4,
},
new()
{
Drawing = new Drawing("rect-c", TestDrawingFactory.Rectangle(2, 7)),
Quantity = 3,
},
};
var filler = new PlateFiller(plate, PlateFillPolicy.Fill, PlateLayout.Strips) { PlateNumber = 7 };
var parts = filler.Nest(items, null, CancellationToken.None);
Assert.NotEmpty(parts);
Assert.All(parts, part => Assert.Contains(items, item => ReferenceEquals(item.Drawing, part.BaseDrawing)));
Assert.All(items, item => Assert.InRange(item.Quantity, 0, 5));
}
[Fact]
public void StripLayout_Nest_UsesOverriddenPackArea()
{
var plate = new Plate(new Size(100, 100));
var drawing = new Drawing("pack", TestDrawingFactory.Rectangle(10, 10));
var filler = new PackProbeStripFiller(plate, drawing);
var parts = filler.Nest(
new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } },
null,
CancellationToken.None
);
Assert.Equal(1, filler.PackAreaCalls);
Assert.Single(parts);
}
[Fact]
public void NestProgressReporter_Report_ClonesPartsAndPreservesReportFields()
{
var source = new Part(
new Drawing("part", TestDrawingFactory.Rectangle(10, 20)),
new Vector(3, 5)
);
source.Rotate(0.5);
var progress = new CapturingProgress();
var workArea = new Box(1, 2, 30, 40);
NestProgressReporter.Report(
progress,
new ProgressReport
{
Phase = NestPhase.Pairs,
PlateNumber = 3,
Parts = new List<Part> { source },
WorkArea = workArea,
Description = "candidate preview",
IsOverallBest = true,
}
);
var reported = Assert.Single(progress.Reports);
Assert.Equal(NestPhase.Pairs, reported.Phase);
Assert.Equal(3, reported.PlateNumber);
Assert.Equal("candidate preview", reported.Description);
Assert.True(reported.IsOverallBest);
Assert.Equal(workArea.X, reported.ActiveWorkArea.X);
Assert.Equal(workArea.Y, reported.ActiveWorkArea.Y);
Assert.Equal(workArea.Width, reported.ActiveWorkArea.Width);
Assert.Equal(workArea.Length, reported.ActiveWorkArea.Length);
var preview = Assert.Single(reported.BestParts);
Assert.NotSame(source, preview);
Assert.Same(source.BaseDrawing, preview.BaseDrawing);
Assert.Equal(source.Location.X, preview.Location.X);
Assert.Equal(source.Location.Y, preview.Location.Y);
Assert.Equal(source.Rotation, preview.Rotation);
}
[Fact]
public void NestProgressReporter_Report_DoesNotForwardMissingProgressOrParts()
{
var progress = new CapturingProgress();
var part = new Part(new Drawing("part", TestDrawingFactory.Rectangle()));
NestProgressReporter.Report(
null,
new ProgressReport { Parts = new List<Part> { part } }
);
NestProgressReporter.Report(progress, new ProgressReport { Parts = null });
NestProgressReporter.Report(progress, new ProgressReport { Parts = new List<Part>() });
Assert.Empty(progress.Reports);
}
[Fact]
public void PlateFillOrchestrator_Nest_UsesThresholdIdentityAndCallerDelegates()
{
var plate = new Plate(new Size(100, 100));
var fillDrawing = new Drawing("duplicate", TestDrawingFactory.Rectangle(10, 10));
var packDrawing = new Drawing("duplicate", TestDrawingFactory.Rectangle(10, 10));
var items = new List<NestItem>
{
new() { Drawing = fillDrawing, Quantity = 10 },
new() { Drawing = packDrawing, Quantity = 1 },
};
var calls = new List<string>();
var progress = new CapturingProgress();
var token = CancellationToken.None;
var parts = PlateFillOrchestrator.Nest(
plate,
items,
new DefaultFillComparer(),
(item, workArea, receivedProgress, receivedToken) =>
{
calls.Add("fill");
Assert.Same(progress, receivedProgress);
Assert.Equal(token, receivedToken);
Assert.Same(fillDrawing, item.Drawing);
var placed = new List<Part>();
for (var i = 0; i < item.Quantity; i++)
{
var part = new Part(item.Drawing);
part.Offset(new Vector(i * 10, 0));
placed.Add(part);
}
return placed;
},
(workArea, packItems, receivedProgress, receivedToken) =>
{
calls.Add("pack");
Assert.Same(progress, receivedProgress);
Assert.Equal(token, receivedToken);
var packItem = Assert.Single(packItems);
Assert.Same(packDrawing, packItem.Drawing);
return new List<Part> { new(packItem.Drawing, new Vector(0, 20)) };
},
progress,
token
);
Assert.Equal(new[] { "fill", "pack" }, calls);
Assert.Equal(11, parts.Count);
Assert.Equal(10, parts.Count(part => ReferenceEquals(part.BaseDrawing, fillDrawing)));
Assert.Single(parts.Where(part => ReferenceEquals(part.BaseDrawing, packDrawing)));
Assert.Equal(0, items[0].Quantity);
Assert.Equal(0, items[1].Quantity);
}
[Fact]
public void PlateFillOrchestrator_Nest_DoesNotInvokeDelegatesAfterCancellation()
{
var plate = new Plate(new Size(100, 100));
var item = new NestItem
{
Drawing = new Drawing("part", TestDrawingFactory.Rectangle(10, 10)),
Quantity = 10,
};
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
var parts = PlateFillOrchestrator.Nest(
plate,
new List<NestItem> { item },
new DefaultFillComparer(),
(_, _, _, _) => throw new Xunit.Sdk.XunitException("Fill must not run after cancellation"),
(_, _, _, _) => throw new Xunit.Sdk.XunitException("Pack must not run after cancellation"),
null,
cancellation.Token
);
Assert.Empty(parts);
Assert.Equal(10, item.Quantity);
}
[Fact]
public void PlateFillOrchestrator_Nest_KeepsTheFullSheetLayoutLessClampHitsWhenItPlacesMore()
{
var (plate, drawing) = ClampedPlate();
var fullSheetFills = 0;
var stripFilled = false;
var parts = PlateFillOrchestrator.Nest(
plate,
new List<NestItem> { new() { Drawing = drawing, Quantity = 10 } },
new DefaultFillComparer(),
(item, area, _, _) =>
{
if (IsFullSheet(area))
{
fullSheetFills++;
return Row(item.Drawing, 0.25, 5, 18, 30, 45, 60);
}
if (stripFilled)
return new List<Part>();
stripFilled = true;
return Row(item.Drawing, area.Bottom, 5, 30, 45);
},
(_, _, _, _) => new List<Part>(),
null,
CancellationToken.None
);
Assert.Equal(1, fullSheetFills);
Assert.Equal(4, parts.Count);
Assert.DoesNotContain(parts, part => part.BoundingBox.Left < 21.5 && part.BoundingBox.Right > 20);
}
[Fact]
public void PlateFillOrchestrator_Nest_SkipsTheFullSheetLayoutOnceDemandIsMet()
{
var (plate, drawing) = ClampedPlate();
var parts = PlateFillOrchestrator.Nest(
plate,
new List<NestItem> { new() { Drawing = drawing, Quantity = 10 } },
new DefaultFillComparer(),
(item, area, _, _) =>
{
Assert.False(IsFullSheet(area));
return Row(item.Drawing, area.Bottom, 0.25, 11, 22, 33, 44, 55, 66, 77, 88)
.Concat(Row(item.Drawing, area.Bottom + 11, 0.25)).ToList();
},
(_, _, _, _) => new List<Part>(),
null,
CancellationToken.None
);
Assert.Equal(10, parts.Count);
}
private static (Plate Plate, Drawing Drawing) ClampedPlate()
{
var plate = new Plate(new Size(100, 100));
plate.EdgeSpacing = new Spacing(0.25, 0.25);
plate.ClampZones.Add(new ClampZone("clamp", new Box(20, 0, 1.5, 0.625)));
return (plate, new Drawing("part", TestDrawingFactory.Rectangle(10, 10)));
}
private static bool IsFullSheet(Box area) => area.Bottom <= 0.25 && area.Length > 99;
private static List<Part> Row(Drawing drawing, double y, params double[] xs) =>
xs.Select(x => new Part(drawing, new Vector(x, y))).ToList();
private sealed class FillAndPackProbePlateFiller : PlateFiller
{
private readonly Drawing fillDrawing;
private readonly Drawing packDrawing;
internal FillAndPackProbePlateFiller(
Plate plate,
Drawing fillDrawing,
Drawing packDrawing
)
: base(plate)
{
this.fillDrawing = fillDrawing;
this.packDrawing = packDrawing;
}
internal int FillCalls { get; private set; }
internal int PackAreaCalls { get; private set; }
public override List<Part> Fill(
NestItem item,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
FillCalls++;
Assert.Same(fillDrawing, item.Drawing);
var parts = new List<Part>();
for (var i = 0; i < item.Quantity; i++)
parts.Add(new Part(fillDrawing, new Vector(i * 10, 0)));
return parts;
}
public override List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
PackAreaCalls++;
Assert.Same(packDrawing, Assert.Single(items).Drawing);
return new List<Part> { new(packDrawing, new Vector(0, 20)) };
}
}
private sealed class AngleProbePlateFiller : PlateFiller
{
internal AngleProbePlateFiller(Plate plate)
: base(plate) { }
internal bool BuildAnglesCalled { get; private set; }
public override List<double> BuildAngles(
NestItem item,
ClassificationResult classification,
Box workArea
)
{
BuildAnglesCalled = true;
return base.BuildAngles(item, classification, workArea);
}
}
private sealed class PackProbeStripFiller : PlateFiller
{
private readonly Drawing drawing;
internal PackProbeStripFiller(Plate plate, Drawing drawing)
: base(plate, PlateFillPolicy.Fill, PlateLayout.Strips)
{
this.drawing = drawing;
}
internal int PackAreaCalls { get; private set; }
public override List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
)
{
PackAreaCalls++;
Assert.Same(drawing, Assert.Single(items).Drawing);
return new List<Part> { new(drawing, new Vector(0, 0)) };
}
}
private static PlateFiller CreateFiller(string strategy, Plate plate) => strategy switch
{
"Fill" => new PlateFiller(plate),
"Vertical Remnant" => new PlateFiller(plate, PlateFillPolicy.VerticalRemnant),
"Horizontal Remnant" => new PlateFiller(plate, PlateFillPolicy.HorizontalRemnant),
_ => throw new ArgumentOutOfRangeException(nameof(strategy)),
};
private sealed class CapturingProgress : IProgress<NestProgress>
{
public List<NestProgress> Reports { get; } = new();
public void Report(NestProgress value)
{
Reports.Add(value);
}
}
}