refactor: organize test project into subdirectories by feature area
Move 43 root-level test files into feature-specific subdirectories mirroring the main codebase structure: Geometry, Fill, BestFit, CutOffs, CuttingStrategy, Engine, IO. Update namespaces to match folder paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
53
OpenNest.Tests/Fill/AccumulatingProgressTests.cs
Normal file
53
OpenNest.Tests/Fill/AccumulatingProgressTests.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class AccumulatingProgressTests
|
||||
{
|
||||
private class CapturingProgress : IProgress<NestProgress>
|
||||
{
|
||||
public NestProgress Last { get; private set; }
|
||||
public void Report(NestProgress value) => Last = value;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Report_PrependsPreviousParts()
|
||||
{
|
||||
var inner = new CapturingProgress();
|
||||
var previous = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
var accumulating = new AccumulatingProgress(inner, previous);
|
||||
|
||||
var newParts = new List<Part> { TestHelpers.MakePartAt(20, 0, 10) };
|
||||
accumulating.Report(new NestProgress { BestParts = newParts });
|
||||
|
||||
Assert.NotNull(inner.Last);
|
||||
Assert.Equal(2, inner.Last.BestParts.Count);
|
||||
Assert.Equal(2, inner.Last.BestPartCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Report_NoPreviousParts_PassesThrough()
|
||||
{
|
||||
var inner = new CapturingProgress();
|
||||
var accumulating = new AccumulatingProgress(inner, new List<Part>());
|
||||
|
||||
var newParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
accumulating.Report(new NestProgress { BestParts = newParts });
|
||||
|
||||
Assert.NotNull(inner.Last);
|
||||
Assert.Single(inner.Last.BestParts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Report_NullBestParts_PassesThrough()
|
||||
{
|
||||
var inner = new CapturingProgress();
|
||||
var previous = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
var accumulating = new AccumulatingProgress(inner, previous);
|
||||
|
||||
accumulating.Report(new NestProgress { BestParts = null });
|
||||
|
||||
Assert.NotNull(inner.Last);
|
||||
Assert.Null(inner.Last.BestParts);
|
||||
}
|
||||
}
|
||||
155
OpenNest.Tests/Fill/AngleCandidateBuilderTests.cs
Normal file
155
OpenNest.Tests/Fill/AngleCandidateBuilderTests.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class AngleCandidateBuilderTests
|
||||
{
|
||||
private static Drawing MakeRectDrawing(double w, double h)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
private static ClassificationResult MakeClassification(double primaryAngle = 0, PartType type = PartType.Irregular)
|
||||
=> new ClassificationResult { PrimaryAngle = primaryAngle, Type = type };
|
||||
|
||||
[Fact]
|
||||
public void Build_ReturnsAtLeastTwoAngles()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
|
||||
var angles = builder.Build(item, MakeClassification(), workArea);
|
||||
|
||||
Assert.True(angles.Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_RectangleType_NarrowWorkArea_UsesBaseAnglesOnly()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var narrowArea = new Box(0, 0, 100, 8); // narrower than part's longest side
|
||||
|
||||
var angles = builder.Build(item, MakeClassification(0, PartType.Rectangle), narrowArea);
|
||||
|
||||
// Rectangle classification always returns exactly 2 angles regardless of work area
|
||||
Assert.Equal(2, angles.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceFullSweep_ProducesFullSweep()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(5, 5) };
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
|
||||
var angles = builder.Build(item, MakeClassification(), workArea);
|
||||
|
||||
// Full sweep at 5deg steps = ~36 angles (0 to 175), plus base angles
|
||||
Assert.True(angles.Count > 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordProductive_PrunesSubsequentBuilds()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 100, 8);
|
||||
|
||||
// First build — full sweep
|
||||
var firstAngles = builder.Build(item, MakeClassification(), workArea);
|
||||
|
||||
// Record some as productive
|
||||
var productive = new List<AngleResult>
|
||||
{
|
||||
new AngleResult { AngleDeg = 0, PartCount = 5 },
|
||||
new AngleResult { AngleDeg = 45, PartCount = 3 },
|
||||
};
|
||||
builder.RecordProductive(productive);
|
||||
|
||||
// Second build — should be pruned to known-good + base angles
|
||||
builder.ForceFullSweep = false;
|
||||
var secondAngles = builder.Build(item, MakeClassification(), workArea);
|
||||
|
||||
Assert.True(secondAngles.Count < firstAngles.Count,
|
||||
$"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_RectanglePart_ReturnsTwoAngles()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var classification = MakeClassification(0, PartType.Rectangle);
|
||||
|
||||
var angles = builder.Build(item, classification, workArea);
|
||||
|
||||
Assert.Equal(2, angles.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_CirclePart_ReturnsOneAngle()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(10, 10) };
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var classification = MakeClassification(0, PartType.Circle);
|
||||
|
||||
var angles = builder.Build(item, classification, workArea);
|
||||
|
||||
Assert.Single(angles);
|
||||
Assert.Equal(0, angles[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UserConstraints_OverrideRectangleClassification()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem
|
||||
{
|
||||
Drawing = MakeRectDrawing(100, 50),
|
||||
RotationStart = Angle.ToRadians(10),
|
||||
RotationEnd = Angle.ToRadians(90),
|
||||
StepAngle = Angle.ToRadians(10),
|
||||
};
|
||||
var classification = MakeClassification(0, PartType.Rectangle);
|
||||
var workArea = new Box(0, 0, 1000, 500);
|
||||
|
||||
var angles = builder.Build(item, classification, workArea);
|
||||
|
||||
Assert.True(angles.Count > 2,
|
||||
$"User constraints should override rect classification, got {angles.Count} angles");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UserConstraints_StartingAtZero_AreRespected()
|
||||
{
|
||||
var builder = new AngleCandidateBuilder();
|
||||
var item = new NestItem
|
||||
{
|
||||
Drawing = MakeRectDrawing(100, 50),
|
||||
RotationStart = 0,
|
||||
RotationEnd = System.Math.PI,
|
||||
StepAngle = Angle.ToRadians(45),
|
||||
};
|
||||
var classification = MakeClassification(0, PartType.Rectangle);
|
||||
var workArea = new Box(0, 0, 1000, 500);
|
||||
|
||||
var angles = builder.Build(item, classification, workArea);
|
||||
|
||||
// Start=0, End=PI is NOT "no constraints" — it's a real 0-180 range
|
||||
Assert.True(angles.Count > 2,
|
||||
$"0-to-PI constraint should produce multiple angles, got {angles.Count}");
|
||||
}
|
||||
}
|
||||
150
OpenNest.Tests/Fill/BestCombinationTests.cs
Normal file
150
OpenNest.Tests/Fill/BestCombinationTests.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class BestCombinationTests
|
||||
{
|
||||
[Fact]
|
||||
public void BothFit_FindsZeroRemnant()
|
||||
{
|
||||
// 100 = 0*30 + 5*20 (algorithm iterates from countLength1=0, finds zero remnant first)
|
||||
var result = BestCombination.FindFrom2(30, 20, 100, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 100.0 - (c1 * 30.0 + c2 * 20.0), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyLength1Fits_ReturnsMaxCount1()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(10, 200, 50, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(5, c1);
|
||||
Assert.Equal(0, c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyLength2Fits_ReturnsMaxCount2()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(200, 10, 50, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(5, c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NeitherFits_ReturnsFalse()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(100, 200, 50, out var c1, out var c2);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(0, c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Length1FillsExactly_ZeroRemnant()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(25, 10, 100, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 100.0 - (c1 * 25.0 + c2 * 10.0), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixMinimizesRemnant()
|
||||
{
|
||||
// 7 and 3 into 20: best is 2*7 + 2*3 = 20 (zero remnant)
|
||||
var result = BestCombination.FindFrom2(7, 3, 20, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(2, c1);
|
||||
Assert.Equal(2, c2);
|
||||
Assert.True(c1 * 7 + c2 * 3 <= 20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefersLessRemnant_OverMoreOfLength1()
|
||||
{
|
||||
// 6 and 5 into 17:
|
||||
// all length1: 2*6=12, remnant=5 -> actually 2*6+1*5=17 perfect
|
||||
var result = BestCombination.FindFrom2(6, 5, 17, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 17.0 - (c1 * 6.0 + c2 * 5.0), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualLengths_FillsWithLength1()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(10, 10, 50, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(5, c1 + c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmallLengths_LargeOverall()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(3, 7, 100, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
var used = c1 * 3.0 + c2 * 7.0;
|
||||
Assert.True(used <= 100);
|
||||
Assert.True(100 - used < 3); // remnant less than smallest piece
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Length2IsBetter_SoleCandidate()
|
||||
{
|
||||
// length1=9, length2=5, overall=10:
|
||||
// length1 alone: 1*9=9 remnant=1
|
||||
// length2 alone: 2*5=10 remnant=0
|
||||
var result = BestCombination.FindFrom2(9, 5, 10, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(2, c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FractionalLengths_WorkCorrectly()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(2.5, 3.5, 12, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
var used = c1 * 2.5 + c2 * 3.5;
|
||||
Assert.True(used <= 12.0 + 0.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverallExactlyOneOfEach()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(40, 60, 100, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(1, c1);
|
||||
Assert.Equal(1, c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverallSmallerThanEither_ReturnsFalse()
|
||||
{
|
||||
var result = BestCombination.FindFrom2(10, 20, 5, out var c1, out var c2);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, c1);
|
||||
Assert.Equal(0, c2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroRemnant_StopsEarly()
|
||||
{
|
||||
// 4 and 6 into 24: 0*4+4*6=24 or 3*4+2*6=24 or 6*4+0*6=24
|
||||
// Algorithm iterates from 0 length1 upward, finds zero remnant and breaks
|
||||
var result = BestCombination.FindFrom2(4, 6, 24, out var c1, out var c2);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(0.0, 24.0 - (c1 * 4.0 + c2 * 6.0), 5);
|
||||
}
|
||||
}
|
||||
165
OpenNest.Tests/Fill/CompactorTests.cs
Normal file
165
OpenNest.Tests/Fill/CompactorTests.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using OpenNest;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Tests.Fill
|
||||
{
|
||||
public class CompactorTests
|
||||
{
|
||||
private static Drawing MakeRectDrawing(double w, double h)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
private static Part MakeRectPart(double x, double y, double w, double h)
|
||||
{
|
||||
var drawing = MakeRectDrawing(w, h);
|
||||
var part = new Part(drawing) { Location = new Vector(x, y) };
|
||||
part.UpdateBounds();
|
||||
return part;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Push_Left_MovesPartTowardEdge()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var part = MakeRectPart(50, 0, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part>();
|
||||
|
||||
var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
|
||||
|
||||
Assert.True(distance > 0);
|
||||
Assert.True(part.BoundingBox.Left < 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Push_Left_StopsAtObstacle()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var obstacle = MakeRectPart(0, 0, 10, 10);
|
||||
var part = MakeRectPart(50, 0, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part> { obstacle };
|
||||
|
||||
Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
|
||||
|
||||
Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Push_Down_MovesPartTowardEdge()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var part = MakeRectPart(0, 50, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part>();
|
||||
|
||||
var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Down);
|
||||
|
||||
Assert.True(distance > 0);
|
||||
Assert.True(part.BoundingBox.Bottom < 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Push_ReturnsZero_WhenAlreadyAtEdge()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var part = MakeRectPart(0, 0, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part>();
|
||||
|
||||
var distance = Compactor.Push(moving, obstacles, workArea, 0, PushDirection.Left);
|
||||
|
||||
Assert.Equal(0, distance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Push_WithSpacing_MovesLessThanWithout()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
|
||||
// Push without spacing.
|
||||
var obstacle1 = MakeRectPart(0, 0, 10, 10);
|
||||
var part1 = MakeRectPart(50, 0, 10, 10);
|
||||
var distNoSpacing = Compactor.Push(new List<Part> { part1 }, new List<Part> { obstacle1 }, workArea, 0, PushDirection.Left);
|
||||
|
||||
// Push with spacing.
|
||||
var obstacle2 = MakeRectPart(0, 0, 10, 10);
|
||||
var part2 = MakeRectPart(50, 0, 10, 10);
|
||||
var distWithSpacing = Compactor.Push(new List<Part> { part2 }, new List<Part> { obstacle2 }, workArea, 2, PushDirection.Left);
|
||||
|
||||
// Spacing should cause the part to stop at a different position than without spacing.
|
||||
Assert.NotEqual(distNoSpacing, distWithSpacing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Push_AngleLeft_MovesPartTowardEdge()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var part = MakeRectPart(50, 0, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part>();
|
||||
|
||||
// direction = left
|
||||
var direction = new Vector(System.Math.Cos(System.Math.PI), System.Math.Sin(System.Math.PI));
|
||||
var distance = Compactor.Push(moving, obstacles, workArea, 0, direction);
|
||||
|
||||
Assert.True(distance > 0);
|
||||
Assert.True(part.BoundingBox.Left < 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Push_AngleDown_MovesPartTowardEdge()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var part = MakeRectPart(0, 50, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part>();
|
||||
|
||||
// direction = down
|
||||
var angle = 3 * System.Math.PI / 2;
|
||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
||||
var distance = Compactor.Push(moving, obstacles, workArea, 0, direction);
|
||||
|
||||
Assert.True(distance > 0);
|
||||
Assert.True(part.BoundingBox.Bottom < 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushBoundingBox_Left_MovesPartTowardEdge()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var part = MakeRectPart(50, 0, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part>();
|
||||
|
||||
var distance = Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left);
|
||||
|
||||
Assert.True(distance > 0);
|
||||
Assert.True(part.BoundingBox.Left < 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushBoundingBox_StopsAtObstacle()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var obstacle = MakeRectPart(0, 0, 10, 10);
|
||||
var part = MakeRectPart(50, 0, 10, 10);
|
||||
var moving = new List<Part> { part };
|
||||
var obstacles = new List<Part> { obstacle };
|
||||
|
||||
Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left);
|
||||
|
||||
Assert.True(part.BoundingBox.Left >= obstacle.BoundingBox.Right - 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
173
OpenNest.Tests/Fill/FillComparerTests.cs
Normal file
173
OpenNest.Tests/Fill/FillComparerTests.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class DefaultFillComparerTests
|
||||
{
|
||||
private readonly IFillComparer comparer = new DefaultFillComparer();
|
||||
private readonly Box workArea = new(0, 0, 100, 100);
|
||||
|
||||
[Fact]
|
||||
public void NullCandidate_ReturnsFalse()
|
||||
{
|
||||
var current = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
Assert.False(comparer.IsBetter(null, current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyCandidate_ReturnsFalse()
|
||||
{
|
||||
var current = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
Assert.False(comparer.IsBetter(new List<Part>(), current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullCurrent_ReturnsTrue()
|
||||
{
|
||||
var candidate = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
Assert.True(comparer.IsBetter(candidate, null, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HigherCount_Wins()
|
||||
{
|
||||
var candidate = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(20, 0, 10),
|
||||
TestHelpers.MakePartAt(40, 0, 10)
|
||||
};
|
||||
var current = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(20, 0, 10)
|
||||
};
|
||||
Assert.True(comparer.IsBetter(candidate, current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameCount_HigherDensityWins()
|
||||
{
|
||||
var candidate = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(12, 0, 10)
|
||||
};
|
||||
var current = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(50, 0, 10)
|
||||
};
|
||||
Assert.True(comparer.IsBetter(candidate, current, workArea));
|
||||
}
|
||||
}
|
||||
|
||||
public class VerticalRemnantComparerTests
|
||||
{
|
||||
private readonly IFillComparer comparer = new VerticalRemnantComparer();
|
||||
private readonly Box workArea = new(0, 0, 100, 100);
|
||||
|
||||
[Fact]
|
||||
public void HigherCount_WinsRegardlessOfExtent()
|
||||
{
|
||||
var candidate = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(40, 0, 10),
|
||||
TestHelpers.MakePartAt(80, 0, 10)
|
||||
};
|
||||
var current = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(12, 0, 10)
|
||||
};
|
||||
Assert.True(comparer.IsBetter(candidate, current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameCount_SmallerXExtent_Wins()
|
||||
{
|
||||
var candidate = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(12, 0, 10)
|
||||
};
|
||||
var current = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(50, 0, 10)
|
||||
};
|
||||
Assert.True(comparer.IsBetter(candidate, current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameCount_SameExtent_HigherDensityWins()
|
||||
{
|
||||
var candidate = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(40, 0, 10)
|
||||
};
|
||||
var current = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(40, 40, 10)
|
||||
};
|
||||
Assert.True(comparer.IsBetter(candidate, current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullCandidate_ReturnsFalse()
|
||||
{
|
||||
var current = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
Assert.False(comparer.IsBetter(null, current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullCurrent_ReturnsTrue()
|
||||
{
|
||||
var candidate = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
Assert.True(comparer.IsBetter(candidate, null, workArea));
|
||||
}
|
||||
}
|
||||
|
||||
public class HorizontalRemnantComparerTests
|
||||
{
|
||||
private readonly IFillComparer comparer = new HorizontalRemnantComparer();
|
||||
private readonly Box workArea = new(0, 0, 100, 100);
|
||||
|
||||
[Fact]
|
||||
public void SameCount_SmallerYExtent_Wins()
|
||||
{
|
||||
var candidate = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(0, 12, 10)
|
||||
};
|
||||
var current = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(0, 50, 10)
|
||||
};
|
||||
Assert.True(comparer.IsBetter(candidate, current, workArea));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HigherCount_WinsRegardlessOfExtent()
|
||||
{
|
||||
var candidate = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(0, 40, 10),
|
||||
TestHelpers.MakePartAt(0, 80, 10)
|
||||
};
|
||||
var current = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(0, 12, 10)
|
||||
};
|
||||
Assert.True(comparer.IsBetter(candidate, current, workArea));
|
||||
}
|
||||
}
|
||||
162
OpenNest.Tests/Fill/FillExtentsTests.cs
Normal file
162
OpenNest.Tests/Fill/FillExtentsTests.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class FillExtentsTests
|
||||
{
|
||||
private static Drawing MakeRightTriangle(double w, double h)
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("triangle", pgm);
|
||||
}
|
||||
|
||||
private static Drawing MakeRect(double w, double h)
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_Triangle_ReturnsPartsWithinWorkArea()
|
||||
{
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
var filler = new FillExtents(workArea, 0.5);
|
||||
var drawing = MakeRightTriangle(10, 8);
|
||||
|
||||
var parts = filler.Fill(drawing);
|
||||
|
||||
Assert.NotNull(parts);
|
||||
Assert.True(parts.Count > 0, "Should place at least one part");
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01,
|
||||
$"Part right edge {part.BoundingBox.Right} exceeds work area {workArea.Right}");
|
||||
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01,
|
||||
$"Part top edge {part.BoundingBox.Top} exceeds work area {workArea.Top}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_PartTooLarge_ReturnsEmpty()
|
||||
{
|
||||
var workArea = new Box(0, 0, 5, 5);
|
||||
var filler = new FillExtents(workArea, 0.5);
|
||||
var drawing = MakeRect(10, 10);
|
||||
|
||||
var parts = filler.Fill(drawing);
|
||||
|
||||
Assert.NotNull(parts);
|
||||
Assert.Empty(parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_Triangle_ColumnFillsHeight()
|
||||
{
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
var filler = new FillExtents(workArea, 0.5);
|
||||
var drawing = MakeRightTriangle(10, 8);
|
||||
|
||||
var parts = filler.Fill(drawing);
|
||||
|
||||
Assert.True(parts.Count > 0);
|
||||
|
||||
// The topmost part should be close to the work area top edge.
|
||||
var topEdge = 0.0;
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.BoundingBox.Top > topEdge)
|
||||
topEdge = part.BoundingBox.Top;
|
||||
}
|
||||
|
||||
// After adjustment, the gap should be small (within one part spacing).
|
||||
var gap = workArea.Top - topEdge;
|
||||
Assert.True(gap < 1.0,
|
||||
$"Gap of {gap:F2} is too large — adjustment should fill close to the top");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_Triangle_FillsWidthWithMultipleColumns()
|
||||
{
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
var filler = new FillExtents(workArea, 0.5);
|
||||
var drawing = MakeRightTriangle(10, 8);
|
||||
|
||||
var parts = filler.Fill(drawing);
|
||||
|
||||
// With a 120-wide sheet and ~10-wide parts, we should get multiple columns.
|
||||
Assert.True(parts.Count >= 8,
|
||||
$"Expected multiple columns but got only {parts.Count} parts");
|
||||
|
||||
// Verify all parts are within bounds.
|
||||
foreach (var part in parts)
|
||||
{
|
||||
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01);
|
||||
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01);
|
||||
Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01);
|
||||
Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_Rect_ReturnsNonEmpty()
|
||||
{
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
var filler = new FillExtents(workArea, 0.5);
|
||||
var drawing = MakeRect(15, 10);
|
||||
|
||||
var parts = filler.Fill(drawing);
|
||||
|
||||
Assert.NotNull(parts);
|
||||
Assert.True(parts.Count > 0, "Rectangle should produce results");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_NonZeroOriginWorkArea_PartsWithinBounds()
|
||||
{
|
||||
// Simulate a remnant sub-region with non-zero origin.
|
||||
var workArea = new Box(30, 10, 80, 40);
|
||||
var filler = new FillExtents(workArea, 0.5);
|
||||
var drawing = MakeRightTriangle(10, 8);
|
||||
|
||||
var parts = filler.Fill(drawing);
|
||||
|
||||
Assert.True(parts.Count > 0);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01,
|
||||
$"Part left {part.BoundingBox.Left} below work area left {workArea.Left}");
|
||||
Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01,
|
||||
$"Part bottom {part.BoundingBox.Bottom} below work area bottom {workArea.Bottom}");
|
||||
Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01);
|
||||
Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_RespectsCancellation()
|
||||
{
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
var filler = new FillExtents(workArea, 0.5);
|
||||
var drawing = MakeRightTriangle(10, 8);
|
||||
|
||||
var parts = filler.Fill(drawing, token: cts.Token);
|
||||
|
||||
Assert.NotNull(parts);
|
||||
}
|
||||
}
|
||||
50
OpenNest.Tests/Fill/FillPolicyTests.cs
Normal file
50
OpenNest.Tests/Fill/FillPolicyTests.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Engine.Strategies;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class FillWithDirectionPreferenceTests
|
||||
{
|
||||
private readonly IFillComparer comparer = new DefaultFillComparer();
|
||||
private readonly Box workArea = new(0, 0, 100, 100);
|
||||
|
||||
[Fact]
|
||||
public void NullPreference_TriesBothDirections_ReturnsBetter()
|
||||
{
|
||||
var hParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(12, 0, 10) };
|
||||
var vParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => dir == NestDirection.Horizontal ? hParts : vParts,
|
||||
null, comparer, workArea);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreferredDirection_UsedFirst_WhenProducesResults()
|
||||
{
|
||||
var hParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(12, 0, 10) };
|
||||
var vParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(0, 12, 10), TestHelpers.MakePartAt(0, 24, 10) };
|
||||
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => dir == NestDirection.Horizontal ? hParts : vParts,
|
||||
NestDirection.Horizontal, comparer, workArea);
|
||||
|
||||
Assert.Equal(2, result.Count); // H has results, so H is returned (preferred)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreferredDirection_FallsBack_WhenPreferredReturnsEmpty()
|
||||
{
|
||||
var vParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
dir => dir == NestDirection.Horizontal ? new List<Part>() : vParts,
|
||||
NestDirection.Horizontal, comparer, workArea);
|
||||
|
||||
Assert.Equal(1, result.Count); // Falls back to V
|
||||
}
|
||||
}
|
||||
92
OpenNest.Tests/Fill/FillScoreTests.cs
Normal file
92
OpenNest.Tests/Fill/FillScoreTests.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Engine.Fill;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class FillScoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void HigherCount_WinsOverLowerCount()
|
||||
{
|
||||
var a = new FillScore(10, 0.5);
|
||||
var b = new FillScore(5, 0.9);
|
||||
|
||||
Assert.True(a > b);
|
||||
Assert.False(b > a);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameCount_HigherDensityWins()
|
||||
{
|
||||
var a = new FillScore(10, 0.8);
|
||||
var b = new FillScore(10, 0.5);
|
||||
|
||||
Assert.True(a > b);
|
||||
Assert.False(b > a);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualScores_AreNotGreaterOrLess()
|
||||
{
|
||||
var a = new FillScore(10, 0.5);
|
||||
var b = new FillScore(10, 0.5);
|
||||
|
||||
Assert.False(a > b);
|
||||
Assert.False(a < b);
|
||||
Assert.True(a >= b);
|
||||
Assert.True(a <= b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_IsZero()
|
||||
{
|
||||
var score = default(FillScore);
|
||||
|
||||
Assert.Equal(0, score.Count);
|
||||
Assert.Equal(0, score.Density);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compute_NullParts_ReturnsDefault()
|
||||
{
|
||||
var score = FillScore.Compute(null, new Box(0, 0, 100, 100));
|
||||
|
||||
Assert.Equal(0, score.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compute_EmptyParts_ReturnsDefault()
|
||||
{
|
||||
var score = FillScore.Compute(new System.Collections.Generic.List<Part>(), new Box(0, 0, 100, 100));
|
||||
|
||||
Assert.Equal(0, score.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compute_WithParts_ReturnsCorrectCount()
|
||||
{
|
||||
var parts = new System.Collections.Generic.List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(20, 0, 10),
|
||||
TestHelpers.MakePartAt(40, 0, 10)
|
||||
};
|
||||
var score = FillScore.Compute(parts, new Box(0, 0, 100, 100));
|
||||
|
||||
Assert.Equal(3, score.Count);
|
||||
Assert.True(score.Density > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareTo_IsConsistentWithOperators()
|
||||
{
|
||||
var a = new FillScore(10, 0.8);
|
||||
var b = new FillScore(5, 0.9);
|
||||
|
||||
Assert.True(a.CompareTo(b) > 0);
|
||||
Assert.True(a > b);
|
||||
Assert.True(b < a);
|
||||
Assert.True(a >= b);
|
||||
Assert.True(b <= a);
|
||||
}
|
||||
}
|
||||
147
OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs
Normal file
147
OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class IterativeShrinkFillerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Fill_NullItems_ReturnsEmpty()
|
||||
{
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
|
||||
var result = IterativeShrinkFiller.Fill(null, new Box(0, 0, 100, 100), fillFunc, 1.0);
|
||||
|
||||
Assert.Empty(result.Parts);
|
||||
Assert.Empty(result.Leftovers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_EmptyItems_ReturnsEmpty()
|
||||
{
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
|
||||
var result = IterativeShrinkFiller.Fill(new List<NestItem>(), new Box(0, 0, 100, 100), fillFunc, 1.0);
|
||||
|
||||
Assert.Empty(result.Parts);
|
||||
Assert.Empty(result.Leftovers);
|
||||
}
|
||||
|
||||
private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing(name, pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_SingleItem_PlacesParts()
|
||||
{
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = drawing, Quantity = 5 }
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
||||
|
||||
Assert.True(result.Parts.Count > 0, "Should place parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_MultipleItems_PlacesFromBoth()
|
||||
{
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 5 },
|
||||
new NestItem { Drawing = MakeRectDrawing(8, 5, "small"), Quantity = 5 },
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
||||
|
||||
var largeCount = result.Parts.Count(p => p.BaseDrawing.Name == "large");
|
||||
var smallCount = result.Parts.Count(p => p.BaseDrawing.Name == "small");
|
||||
|
||||
Assert.True(largeCount > 0, "Should place large parts");
|
||||
Assert.True(smallCount > 0, "Should place small parts in remaining space");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_UnfilledQuantity_ReturnsLeftovers()
|
||||
{
|
||||
// Huge quantity that can't all fit on a small plate
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 1000 },
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 60, 30), fillFunc, 1.0);
|
||||
|
||||
Assert.True(result.Parts.Count > 0, "Should place some parts");
|
||||
Assert.True(result.Leftovers.Count > 0, "Should have leftovers");
|
||||
Assert.True(result.Leftovers[0].Quantity > 0, "Leftover quantity should be positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_UnlimitedQuantity_PlacesParts()
|
||||
{
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 0 }
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
||||
|
||||
Assert.True(result.Parts.Count > 0, "Unlimited qty items should still be placed");
|
||||
Assert.Empty(result.Leftovers); // unlimited items never produce leftovers
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_RespectsCancellation()
|
||||
{
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 10 }
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 100, 100), fillFunc, 1.0, cts.Token);
|
||||
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
}
|
||||
64
OpenNest.Tests/Fill/PairFillerTests.cs
Normal file
64
OpenNest.Tests/Fill/PairFillerTests.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class PairFillerTests
|
||||
{
|
||||
private static Drawing MakeRectDrawing(double w, double h)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
private static Plate MakePlate(double width, double length, double spacing = 0.5)
|
||||
{
|
||||
return new Plate { Size = new Size(width, length), PartSpacing = spacing };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_ReturnsPartsForSimpleDrawing()
|
||||
{
|
||||
var filler = new PairFiller(MakePlate(120, 60));
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
|
||||
var result = filler.Fill(item, workArea);
|
||||
|
||||
Assert.NotNull(result.Parts);
|
||||
Assert.NotNull(result.BestFits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_EmptyResult_WhenPartTooLarge()
|
||||
{
|
||||
var filler = new PairFiller(MakePlate(10, 10));
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 20) };
|
||||
var workArea = new Box(0, 0, 10, 10);
|
||||
|
||||
var result = filler.Fill(item, workArea);
|
||||
|
||||
Assert.NotNull(result.Parts);
|
||||
Assert.Empty(result.Parts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_RespectsCancellation()
|
||||
{
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var filler = new PairFiller(MakePlate(120, 60));
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
var workArea = new Box(0, 0, 120, 60);
|
||||
|
||||
var result = filler.Fill(item, workArea, token: cts.Token);
|
||||
|
||||
Assert.NotNull(result.Parts);
|
||||
}
|
||||
}
|
||||
238
OpenNest.Tests/Fill/PairOverlapDiagnosticTests.cs
Normal file
238
OpenNest.Tests/Fill/PairOverlapDiagnosticTests.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class PairOverlapDiagnosticTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public PairOverlapDiagnosticTests(ITestOutputHelper output) => _output = output;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 5x3.31 rectangle with rounded corners on the top-right and bottom-right
|
||||
/// (radius 0.5), similar to "4526 A14 PT13".
|
||||
/// </summary>
|
||||
private static Drawing MakeRoundedRect(double w = 5.0, double h = 3.31, double r = 0.5)
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
// Bottom edge
|
||||
pgm.Codes.Add(new LinearMove(new Vector(w - r, 0)));
|
||||
// Bottom-right rounded corner
|
||||
pgm.Codes.Add(new ArcMove(new Vector(w, r), new Vector(w - r, r), RotationType.CW));
|
||||
// Right edge
|
||||
pgm.Codes.Add(new LinearMove(new Vector(w, h - r)));
|
||||
// Top-right rounded corner
|
||||
pgm.Codes.Add(new ArcMove(new Vector(w - r, h), new Vector(w - r, h - r), RotationType.CW));
|
||||
// Top edge
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||
// Left edge back to start
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rounded-rect", pgm);
|
||||
}
|
||||
|
||||
private static Drawing MakeSimpleRect(double w = 5.0, double h = 3.31)
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)] // 0 degrees
|
||||
[InlineData(90)] // 90 degrees
|
||||
[InlineData(180)] // 180 degrees
|
||||
[InlineData(270)] // 270 degrees
|
||||
public void PartBoundary_HasEdgesAtAllRotations_RoundedRect(double angleDeg)
|
||||
{
|
||||
var drawing = MakeRoundedRect();
|
||||
var part = new Part(drawing);
|
||||
if (angleDeg != 0)
|
||||
part.Rotate(Angle.ToRadians(angleDeg));
|
||||
|
||||
var boundary = new PartBoundary(part, 0.125);
|
||||
|
||||
var left = boundary.GetEdges(PushDirection.Left);
|
||||
var right = boundary.GetEdges(PushDirection.Right);
|
||||
var up = boundary.GetEdges(PushDirection.Up);
|
||||
var down = boundary.GetEdges(PushDirection.Down);
|
||||
|
||||
_output.WriteLine($"Rotation: {angleDeg}°");
|
||||
_output.WriteLine($" Left edges: {left.Length}");
|
||||
_output.WriteLine($" Right edges: {right.Length}");
|
||||
_output.WriteLine($" Up edges: {up.Length}");
|
||||
_output.WriteLine($" Down edges: {down.Length}");
|
||||
|
||||
Assert.True(left.Length > 0, $"No left edges at {angleDeg}°");
|
||||
Assert.True(right.Length > 0, $"No right edges at {angleDeg}°");
|
||||
Assert.True(up.Length > 0, $"No up edges at {angleDeg}°");
|
||||
Assert.True(down.Length > 0, $"No down edges at {angleDeg}°");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(90)]
|
||||
[InlineData(180)]
|
||||
[InlineData(270)]
|
||||
public void PartBoundary_HasEdgesAtAllRotations_SimpleRect(double angleDeg)
|
||||
{
|
||||
var drawing = MakeSimpleRect();
|
||||
var part = new Part(drawing);
|
||||
if (angleDeg != 0)
|
||||
part.Rotate(Angle.ToRadians(angleDeg));
|
||||
|
||||
var boundary = new PartBoundary(part, 0.125);
|
||||
|
||||
var left = boundary.GetEdges(PushDirection.Left);
|
||||
var right = boundary.GetEdges(PushDirection.Right);
|
||||
var up = boundary.GetEdges(PushDirection.Up);
|
||||
var down = boundary.GetEdges(PushDirection.Down);
|
||||
|
||||
_output.WriteLine($"Rotation: {angleDeg}°");
|
||||
_output.WriteLine($" Left edges: {left.Length}");
|
||||
_output.WriteLine($" Right edges: {right.Length}");
|
||||
_output.WriteLine($" Up edges: {up.Length}");
|
||||
_output.WriteLine($" Down edges: {down.Length}");
|
||||
|
||||
Assert.True(left.Length > 0, $"No left edges at {angleDeg}°");
|
||||
Assert.True(right.Length > 0, $"No right edges at {angleDeg}°");
|
||||
Assert.True(up.Length > 0, $"No up edges at {angleDeg}°");
|
||||
Assert.True(down.Length > 0, $"No down edges at {angleDeg}°");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)] // simple rect
|
||||
[InlineData(true)] // rounded rect
|
||||
public void FillExtents_NoPairOverlap_At90Degrees(bool rounded)
|
||||
{
|
||||
var drawing = rounded ? MakeRoundedRect() : MakeSimpleRect();
|
||||
var workArea = new Box(0, 0, 20, 20);
|
||||
var partSpacing = 0.25;
|
||||
|
||||
var filler = new FillExtents(workArea, partSpacing);
|
||||
var parts = filler.Fill(drawing, Angle.ToRadians(90));
|
||||
|
||||
_output.WriteLine($"Shape: {(rounded ? "rounded rect" : "simple rect")}");
|
||||
_output.WriteLine($"Parts: {parts.Count}");
|
||||
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var p = parts[i];
|
||||
_output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " +
|
||||
$"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})");
|
||||
}
|
||||
|
||||
// Check for overlapping bounding boxes
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var b1 = parts[i].BoundingBox;
|
||||
for (var j = i + 1; j < parts.Count; j++)
|
||||
{
|
||||
var b2 = parts[j].BoundingBox;
|
||||
var overlapX = System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
|
||||
var overlapY = System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
|
||||
|
||||
if (overlapX > 0.01 && overlapY > 0.01)
|
||||
_output.WriteLine($" OVERLAP: [{i}] and [{j}] overlap by ({overlapX:F3}, {overlapY:F3})");
|
||||
|
||||
Assert.False(overlapX > 0.01 && overlapY > 0.01,
|
||||
$"Parts [{i}] and [{j}] have overlapping bounding boxes " +
|
||||
$"({overlapX:F3} x {overlapY:F3})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void FillLinear_PairPattern_NoPairOverlap_At90Degrees(bool rounded)
|
||||
{
|
||||
var drawing = rounded ? MakeRoundedRect() : MakeSimpleRect();
|
||||
var workArea = new Box(0, 0, 20, 20);
|
||||
var partSpacing = 0.25;
|
||||
|
||||
// Build a pair at 90°/270°
|
||||
var part1 = Part.CreateAtOrigin(drawing, Angle.ToRadians(90));
|
||||
var part2 = Part.CreateAtOrigin(drawing, Angle.ToRadians(270));
|
||||
|
||||
// Slide part2 right of part1
|
||||
var offset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing;
|
||||
part2.Offset(offset, 0);
|
||||
part2.UpdateBounds();
|
||||
|
||||
// Slide part2 left toward part1 using geometry
|
||||
var b1 = new PartBoundary(part1, partSpacing / 2);
|
||||
var b2 = new PartBoundary(part2, partSpacing / 2);
|
||||
|
||||
_output.WriteLine($"Part1 (90°) boundary edges: L={b1.GetEdges(PushDirection.Left).Length} R={b1.GetEdges(PushDirection.Right).Length}");
|
||||
_output.WriteLine($"Part2 (270°) boundary edges: L={b2.GetEdges(PushDirection.Left).Length} R={b2.GetEdges(PushDirection.Right).Length}");
|
||||
|
||||
var movingLines = b2.GetLines(part2.Location, PushDirection.Left);
|
||||
var stationaryLines = b1.GetLines(part1.Location, PushDirection.Right);
|
||||
|
||||
_output.WriteLine($"Part1 loc: ({part1.Location.X:F4},{part1.Location.Y:F4})");
|
||||
_output.WriteLine($"Part2 loc: ({part2.Location.X:F4},{part2.Location.Y:F4})");
|
||||
|
||||
_output.WriteLine($"Moving lines (part2 left): {movingLines.Count}");
|
||||
foreach (var l in movingLines)
|
||||
_output.WriteLine($" ({l.pt1.X:F4},{l.pt1.Y:F4})->({l.pt2.X:F4},{l.pt2.Y:F4})");
|
||||
|
||||
_output.WriteLine($"Stationary lines (part1 right): {stationaryLines.Count}");
|
||||
foreach (var l in stationaryLines)
|
||||
_output.WriteLine($" ({l.pt1.X:F4},{l.pt1.Y:F4})->({l.pt2.X:F4},{l.pt2.Y:F4})");
|
||||
|
||||
var slideDist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left);
|
||||
_output.WriteLine($"Slide distance: {slideDist:F4}");
|
||||
|
||||
if (slideDist < double.MaxValue && slideDist > 0)
|
||||
{
|
||||
part2.Offset(-slideDist, 0);
|
||||
part2.UpdateBounds();
|
||||
}
|
||||
|
||||
_output.WriteLine($"Part1 bbox: ({part1.BoundingBox.Left:F2},{part1.BoundingBox.Bottom:F2})-({part1.BoundingBox.Right:F2},{part1.BoundingBox.Top:F2})");
|
||||
_output.WriteLine($"Part2 bbox: ({part2.BoundingBox.Left:F2},{part2.BoundingBox.Bottom:F2})-({part2.BoundingBox.Right:F2},{part2.BoundingBox.Top:F2})");
|
||||
|
||||
// Now tile this pair pattern
|
||||
var pattern = new Pattern();
|
||||
pattern.Parts.Add(part1);
|
||||
pattern.Parts.Add(part2);
|
||||
pattern.UpdateBounds();
|
||||
|
||||
_output.WriteLine($"Pattern bbox width: {pattern.BoundingBox.Width:F2}");
|
||||
|
||||
var engine = new FillLinear(workArea, partSpacing);
|
||||
var parts = engine.Fill(pattern, NestDirection.Horizontal);
|
||||
|
||||
_output.WriteLine($"Total parts: {parts.Count}");
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var p = parts[i];
|
||||
_output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " +
|
||||
$"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})");
|
||||
}
|
||||
|
||||
// Check for overlaps
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
var bi = parts[i].BoundingBox;
|
||||
for (var j = i + 1; j < parts.Count; j++)
|
||||
{
|
||||
var bj = parts[j].BoundingBox;
|
||||
var ox = System.Math.Min(bi.Right, bj.Right) - System.Math.Max(bi.Left, bj.Left);
|
||||
var oy = System.Math.Min(bi.Top, bj.Top) - System.Math.Max(bi.Bottom, bj.Bottom);
|
||||
|
||||
Assert.False(ox > 0.01 && oy > 0.01,
|
||||
$"Parts [{i}] and [{j}] overlap ({ox:F3} x {oy:F3})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
OpenNest.Tests/Fill/PatternTilerTests.cs
Normal file
104
OpenNest.Tests/Fill/PatternTilerTests.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class PatternTilerTests
|
||||
{
|
||||
private static Drawing MakeSquareDrawing(double size)
|
||||
{
|
||||
var pgm = new CNC.Program();
|
||||
pgm.Codes.Add(new CNC.LinearMove(size, 0));
|
||||
pgm.Codes.Add(new CNC.LinearMove(size, size));
|
||||
pgm.Codes.Add(new CNC.LinearMove(0, size));
|
||||
pgm.Codes.Add(new CNC.LinearMove(0, 0));
|
||||
return new Drawing("square", pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tile_SinglePart_FillsGrid()
|
||||
{
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
|
||||
var plateSize = new Size(30, 20);
|
||||
var partSpacing = 0.0;
|
||||
|
||||
var result = PatternTiler.Tile(cell, plateSize, partSpacing);
|
||||
|
||||
Assert.Equal(6, result.Count);
|
||||
|
||||
foreach (var part in result)
|
||||
{
|
||||
Assert.True(part.BoundingBox.Right <= plateSize.Width + 0.001);
|
||||
Assert.True(part.BoundingBox.Top <= plateSize.Length + 0.001);
|
||||
Assert.True(part.BoundingBox.Left >= -0.001);
|
||||
Assert.True(part.BoundingBox.Bottom >= -0.001);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tile_TwoParts_TilesUnitCell()
|
||||
{
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var partA = Part.CreateAtOrigin(drawing);
|
||||
var partB = Part.CreateAtOrigin(drawing);
|
||||
partB.Offset(10, 0);
|
||||
|
||||
var cell = new List<Part> { partA, partB };
|
||||
var plateSize = new Size(40, 20);
|
||||
var partSpacing = 0.0;
|
||||
|
||||
var result = PatternTiler.Tile(cell, plateSize, partSpacing);
|
||||
|
||||
Assert.Equal(8, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tile_WithSpacing_ReducesCount()
|
||||
{
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
|
||||
var plateSize = new Size(30, 20);
|
||||
var partSpacing = 2.0;
|
||||
|
||||
var result = PatternTiler.Tile(cell, plateSize, partSpacing);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tile_EmptyCell_ReturnsEmpty()
|
||||
{
|
||||
var result = PatternTiler.Tile(new List<Part>(), new Size(100, 100), 0);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tile_NonSquarePlate_CorrectAxes()
|
||||
{
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
|
||||
var plateSize = new Size(50, 10);
|
||||
|
||||
var result = PatternTiler.Tile(cell, plateSize, 0);
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
|
||||
var maxRight = result.Max(p => p.BoundingBox.Right);
|
||||
var maxTop = result.Max(p => p.BoundingBox.Top);
|
||||
Assert.True(maxRight <= 50.001);
|
||||
Assert.True(maxTop <= 10.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tile_CellLargerThanPlate_ReturnsEmpty()
|
||||
{
|
||||
var drawing = MakeSquareDrawing(50);
|
||||
var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
|
||||
var plateSize = new Size(30, 30);
|
||||
|
||||
var result = PatternTiler.Tile(cell, plateSize, 0);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
106
OpenNest.Tests/Fill/RemnantFillerTests2.cs
Normal file
106
OpenNest.Tests/Fill/RemnantFillerTests2.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class RemnantFillerTests2
|
||||
{
|
||||
private static Drawing MakeSquareDrawing(double size)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, size)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, size)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("sq", pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillItems_PlacesPartsInRemnants()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var filler = new RemnantFiller(workArea, 1.0);
|
||||
|
||||
// Place a large obstacle leaving a 40x100 strip on the right
|
||||
filler.AddObstacles(new[] { TestHelpers.MakePartAt(0, 0, 50) });
|
||||
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = drawing, Quantity = 5 }
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var placed = filler.FillItems(items, fillFunc);
|
||||
|
||||
Assert.True(placed.Count > 0, "Should place parts in remaining space");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillItems_DoesNotMutateItemQuantities()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var filler = new RemnantFiller(workArea, 1.0);
|
||||
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = drawing, Quantity = 3 }
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
filler.FillItems(items, fillFunc);
|
||||
|
||||
Assert.Equal(3, items[0].Quantity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillItems_EmptyItems_ReturnsEmpty()
|
||||
{
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var filler = new RemnantFiller(workArea, 1.0);
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
|
||||
|
||||
var result = filler.FillItems(new List<NestItem>(), fillFunc);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillItems_RespectsCancellation()
|
||||
{
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var filler = new RemnantFiller(workArea, 1.0);
|
||||
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = drawing, Quantity = 5 }
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
|
||||
var result = filler.FillItems(items, fillFunc, cts.Token);
|
||||
|
||||
// Should not throw, returns whatever was placed
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
}
|
||||
371
OpenNest.Tests/Fill/RemnantFinderTests.cs
Normal file
371
OpenNest.Tests/Fill/RemnantFinderTests.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class RemnantFinderTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmptyPlate_ReturnsWholeWorkArea()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.Single(remnants);
|
||||
Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleObstacle_InCorner_FindsLShapedRemnants()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 40, 40));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.True(remnants.Count >= 2);
|
||||
var largest = remnants[0];
|
||||
Assert.Equal(60 * 100, largest.Area(), 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleObstacle_InCenter_FindsFourRemnants()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(30, 30, 40, 40));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.True(remnants.Count >= 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinDimension_FiltersSmallRemnants()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 95, 100));
|
||||
var all = finder.FindRemnants(0);
|
||||
var filtered = finder.FindRemnants(10);
|
||||
|
||||
Assert.True(all.Count > filtered.Count);
|
||||
foreach (var r in filtered)
|
||||
{
|
||||
Assert.True(r.Width >= 10);
|
||||
Assert.True(r.Length >= 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResultsSortedByAreaDescending()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
for (var i = 1; i < remnants.Count; i++)
|
||||
Assert.True(remnants[i - 1].Area() >= remnants[i].Area());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddObstacle_UpdatesResults()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
var before = finder.FindRemnants();
|
||||
Assert.Single(before);
|
||||
|
||||
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||
var after = finder.FindRemnants();
|
||||
Assert.True(after.Count > 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearObstacles_ResetsToFullWorkArea()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||
finder.ClearObstacles();
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.Single(remnants);
|
||||
Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullyCovered_ReturnsEmpty()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 100, 100));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.Empty(remnants);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleObstacles_FindsGapBetween()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 40, 100));
|
||||
finder.AddObstacle(new Box(60, 0, 40, 100));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var gap = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 19.9 && r.Width <= 20.1 &&
|
||||
r.Length >= 99.9);
|
||||
Assert.NotNull(gap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPlate_CreatesFinderWithPartsAsObstacles()
|
||||
{
|
||||
var plate = TestHelpers.MakePlate(60, 120,
|
||||
TestHelpers.MakePartAt(0, 0, 20));
|
||||
var finder = RemnantFinder.FromPlate(plate);
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.True(remnants.Count >= 1);
|
||||
Assert.True(remnants[0].Area() < plate.WorkArea().Area());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObstacleOutsideWorkArea_IsIgnored()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(200, 200, 50, 50));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.Single(remnants);
|
||||
Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObstaclePartiallyOutsideWorkArea_IsClipped()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
// Obstacle extends 20 units past the right edge
|
||||
finder.AddObstacle(new Box(80, 0, 40, 100));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
// Should find the 80x100 strip on the left
|
||||
var left = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 79.9 && r.Width <= 80.1 &&
|
||||
r.Length >= 99.9);
|
||||
Assert.NotNull(left);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverlappingObstacles_HandledCorrectly()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 60, 60));
|
||||
finder.AddObstacle(new Box(40, 40, 60, 60)); // overlaps first
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
// No remnant should overlap either obstacle
|
||||
foreach (var r in remnants)
|
||||
{
|
||||
Assert.False(
|
||||
r.Left < 60 && r.Right > 0 && r.Bottom < 60 && r.Top > 0
|
||||
&& r.Left < 100 && r.Right > 40 && r.Bottom < 100 && r.Top > 40,
|
||||
"Remnant should not overlap both obstacles simultaneously in their shared region");
|
||||
}
|
||||
|
||||
// Total remnant area + obstacle coverage should not exceed work area
|
||||
var totalRemnantArea = remnants.Sum(r => r.Area());
|
||||
Assert.True(totalRemnantArea < 100 * 100);
|
||||
Assert.True(totalRemnantArea > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithObstaclesList()
|
||||
{
|
||||
var obstacles = new List<Box>
|
||||
{
|
||||
new Box(0, 0, 40, 100),
|
||||
new Box(60, 0, 40, 100)
|
||||
};
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100), obstacles);
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var gap = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 19.9 && r.Width <= 20.1);
|
||||
Assert.NotNull(gap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddObstacles_Plural_AddsMultiple()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacles(new[]
|
||||
{
|
||||
new Box(0, 0, 40, 100),
|
||||
new Box(60, 0, 40, 100)
|
||||
});
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var gap = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 19.9 && r.Width <= 20.1);
|
||||
Assert.NotNull(gap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeWorkflow_AddObstacleThenRequery()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
|
||||
// First fill: obstacle in bottom-left
|
||||
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||
var pass1 = finder.FindRemnants();
|
||||
Assert.True(pass1.Count >= 2);
|
||||
|
||||
// Simulate filling the largest remnant by adding another obstacle
|
||||
var largest = pass1[0];
|
||||
finder.AddObstacle(new Box(largest.X, largest.Y, largest.Width / 2, largest.Length));
|
||||
var pass2 = finder.FindRemnants();
|
||||
|
||||
// Should have more, smaller remnants now
|
||||
var pass2TotalArea = pass2.Sum(r => r.Area());
|
||||
var pass1TotalArea = pass1.Sum(r => r.Area());
|
||||
Assert.True(pass2TotalArea < pass1TotalArea);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoRemnantOverlapsObstacle()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(20, 20, 30, 30));
|
||||
finder.AddObstacle(new Box(60, 10, 25, 80));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
foreach (var r in remnants)
|
||||
{
|
||||
// Check no remnant overlaps obstacle 1
|
||||
var overlaps1 = r.Left < 50 && r.Right > 20 && r.Bottom < 50 && r.Top > 20;
|
||||
Assert.False(overlaps1, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 1");
|
||||
|
||||
// Check no remnant overlaps obstacle 2
|
||||
var overlaps2 = r.Left < 85 && r.Right > 60 && r.Bottom < 90 && r.Top > 10;
|
||||
Assert.False(overlaps2, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManyObstacles_GridPattern()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
|
||||
// Place a 5x5 grid of 10x10 obstacles with 10-unit gaps
|
||||
for (var row = 0; row < 5; row++)
|
||||
for (var col = 0; col < 5; col++)
|
||||
finder.AddObstacle(new Box(col * 20, row * 20, 10, 10));
|
||||
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
// All remnants should be within the work area
|
||||
foreach (var r in remnants)
|
||||
{
|
||||
Assert.True(r.Left >= 0);
|
||||
Assert.True(r.Bottom >= 0);
|
||||
Assert.True(r.Right <= 100);
|
||||
Assert.True(r.Top <= 100);
|
||||
}
|
||||
|
||||
// Should find gaps between obstacles
|
||||
Assert.True(remnants.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleObstacle_NearEdge_FindsRemnantsOnAllSides()
|
||||
{
|
||||
// Obstacle near top-left: should find remnants above, below, and to the right.
|
||||
var finder = new RemnantFinder(new Box(0, 0, 120, 60));
|
||||
finder.AddObstacle(new Box(0, 47, 21, 6));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
var above = remnants.FirstOrDefault(r => r.Bottom >= 53 - 0.1 && r.Width > 50);
|
||||
var below = remnants.FirstOrDefault(r => r.Top <= 47 + 0.1 && r.Width > 50);
|
||||
var right = remnants.FirstOrDefault(r => r.Left >= 21 - 0.1 && r.Length > 50);
|
||||
|
||||
Assert.NotNull(above);
|
||||
Assert.NotNull(below);
|
||||
Assert.NotNull(right);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadNestFile_FindsGapAboveMainGrid()
|
||||
{
|
||||
var nestFile = @"C:\Users\AJ\Desktop\no_remnant_found.nest";
|
||||
if (!File.Exists(nestFile))
|
||||
return; // Skip if file not available.
|
||||
|
||||
var reader = new NestReader(nestFile);
|
||||
var nest = reader.Read();
|
||||
var plate = nest.Plates[0];
|
||||
|
||||
var finder = RemnantFinder.FromPlate(plate);
|
||||
|
||||
// Use smallest drawing bbox dimension as minDim (same as UI).
|
||||
var minDim = nest.Drawings.Min(d =>
|
||||
System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length));
|
||||
|
||||
var tiered = finder.FindTieredRemnants(minDim);
|
||||
|
||||
// Should find a remnant near (0.25, 53.13) — the gap above the main grid.
|
||||
var topGap = tiered.FirstOrDefault(t =>
|
||||
t.Box.Bottom > 50 && t.Box.Bottom < 55 &&
|
||||
t.Box.Left < 1 &&
|
||||
t.Box.Width > 100 &&
|
||||
t.Box.Length > 5);
|
||||
|
||||
Assert.True(topGap.Box.Width > 0, "Expected remnant above main grid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DensePack_FindsGapAtTop()
|
||||
{
|
||||
// Reproduce real plate: 120x60, 68 parts of SULLYS-004.
|
||||
// Main grid tops out at y=53.14 (obstacle). Two rotated parts on the
|
||||
// right extend to y=58.49 but only at x > 106. The gap at x < 106
|
||||
// from y=53.14 to y=59.8 is ~106 x 6.66 — should be found.
|
||||
var workArea = new Box(0.2, 0.8, 119.5, 59.0);
|
||||
var obstacles = new List<Box>();
|
||||
var spacing = 0.25;
|
||||
|
||||
// Main grid: 5 columns x 12 rows (6 pairs).
|
||||
// Even rows: bbox bottom offsets, odd rows: different offsets.
|
||||
double[] colX = { 0.25, 21.08, 41.90, 62.73, 83.56 };
|
||||
double[] colXOdd = { 0.81, 21.64, 42.46, 63.29, 84.12 };
|
||||
double[] evenY = { 3.67, 12.41, 21.14, 29.87, 38.60, 47.33 };
|
||||
double[] oddY = { 0.75, 9.48, 18.21, 26.94, 35.67, 44.40 };
|
||||
|
||||
foreach (var cx in colX)
|
||||
foreach (var ey in evenY)
|
||||
obstacles.Add(new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
|
||||
foreach (var cx in colXOdd)
|
||||
foreach (var oy in oddY)
|
||||
obstacles.Add(new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
|
||||
|
||||
// Right-side rotated parts (only 2 extend high: parts 62 and 66).
|
||||
obstacles.Add(new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
// Parts 63, 67 (lower rotated)
|
||||
obstacles.Add(new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
// Parts 60, 64 (upper-right rotated, lower)
|
||||
obstacles.Add(new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
// Parts 61, 65
|
||||
obstacles.Add(new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
obstacles.Add(new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
|
||||
|
||||
var finder = new RemnantFinder(workArea, obstacles);
|
||||
var remnants = finder.FindRemnants(5.375);
|
||||
|
||||
// The gap at x < 106 from y=53.14 to y=59.8 should be found.
|
||||
Assert.True(remnants.Count > 0, "Should find gap above main grid");
|
||||
var topRemnant = remnants.FirstOrDefault(r => r.Length >= 5.375 && r.Width > 50);
|
||||
Assert.NotNull(topRemnant);
|
||||
|
||||
// Verify dimensions are close to the expected ~104 x 6.6 gap.
|
||||
Assert.True(topRemnant.Width > 100, $"Expected width > 100, got {topRemnant.Width:F1}");
|
||||
Assert.True(topRemnant.Length > 6, $"Expected length > 6, got {topRemnant.Length:F1}");
|
||||
}
|
||||
}
|
||||
148
OpenNest.Tests/Fill/ShrinkFillerTests.cs
Normal file
148
OpenNest.Tests/Fill/ShrinkFillerTests.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class ShrinkFillerTests
|
||||
{
|
||||
private static Drawing MakeSquareDrawing(double size)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, size)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, size)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("square", pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shrink_FillsAndReturnsDimension()
|
||||
{
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var box = new Box(0, 0, 100, 50);
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Length);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Parts.Count > 0);
|
||||
Assert.True(result.Dimension <= 50, "Dimension should be <= original");
|
||||
Assert.True(result.Dimension > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shrink_Width_ReducesHorizontally()
|
||||
{
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var box = new Box(0, 0, 100, 50);
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Width);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Parts.Count > 0);
|
||||
Assert.True(result.Dimension <= 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shrink_RespectsCancellation()
|
||||
{
|
||||
var cts = new System.Threading.CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
var drawing = MakeSquareDrawing(10);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var box = new Box(0, 0, 100, 50);
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
|
||||
|
||||
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0,
|
||||
ShrinkAxis.Length, token: cts.Token);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Parts.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimToCount_Width_KeepsPartsNearestToOrigin()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5), // Right = 5
|
||||
TestHelpers.MakePartAt(10, 0, 5), // Right = 15
|
||||
TestHelpers.MakePartAt(20, 0, 5), // Right = 25
|
||||
TestHelpers.MakePartAt(30, 0, 5), // Right = 35
|
||||
};
|
||||
|
||||
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Width);
|
||||
|
||||
Assert.Equal(2, trimmed.Count);
|
||||
Assert.True(trimmed.All(p => p.BoundingBox.Right <= 15));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimToCount_Length_KeepsPartsNearestToOrigin()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5), // Top = 5
|
||||
TestHelpers.MakePartAt(0, 10, 5), // Top = 15
|
||||
TestHelpers.MakePartAt(0, 20, 5), // Top = 25
|
||||
TestHelpers.MakePartAt(0, 30, 5), // Top = 35
|
||||
};
|
||||
|
||||
var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Length);
|
||||
|
||||
Assert.Equal(2, trimmed.Count);
|
||||
Assert.True(trimmed.All(p => p.BoundingBox.Top <= 15));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimToCount_ReturnsInput_WhenCountAtOrBelowTarget()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
};
|
||||
|
||||
var same = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Width);
|
||||
Assert.Same(parts, same);
|
||||
|
||||
var fewer = ShrinkFiller.TrimToCount(parts, 5, ShrinkAxis.Width);
|
||||
Assert.Same(parts, fewer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrimToCount_DoesNotMutateInput()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
TestHelpers.MakePartAt(20, 0, 5),
|
||||
};
|
||||
|
||||
var originalCount = parts.Count;
|
||||
var trimmed = ShrinkFiller.TrimToCount(parts, 1, ShrinkAxis.Width);
|
||||
|
||||
Assert.Equal(originalCount, parts.Count);
|
||||
Assert.Equal(1, trimmed.Count);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user