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:
@@ -0,0 +1,83 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class EngineOverlapTests
|
||||
{
|
||||
private const string DxfPath = @"C:\Users\AJ\Desktop\Templates\4526 A14 PT15.dxf";
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public EngineOverlapTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
private static Drawing ImportDxf()
|
||||
{
|
||||
var importer = new DxfImporter();
|
||||
importer.GetGeometry(DxfPath, out var geometry);
|
||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||
return new Drawing("PT15", pgm);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Default")]
|
||||
[InlineData("Strip")]
|
||||
[InlineData("Vertical Remnant")]
|
||||
[InlineData("Horizontal Remnant")]
|
||||
public void FillPlate_NoOverlaps(string engineName)
|
||||
{
|
||||
var drawing = ImportDxf();
|
||||
var plate = new Plate(60, 120);
|
||||
|
||||
NestEngineRegistry.ActiveEngineName = engineName;
|
||||
var engine = NestEngineRegistry.Create(plate);
|
||||
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
var success = engine.Fill(item);
|
||||
|
||||
_output.WriteLine($"Engine: {engine.Name}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}");
|
||||
|
||||
if (engine is DefaultNestEngine defaultEngine)
|
||||
{
|
||||
_output.WriteLine($"Winner phase: {defaultEngine.WinnerPhase}");
|
||||
foreach (var pr in defaultEngine.PhaseResults)
|
||||
_output.WriteLine($" Phase {pr.Phase}: {pr.PartCount} parts in {pr.TimeMs}ms");
|
||||
}
|
||||
|
||||
// Show rotation distribution
|
||||
var rotGroups = plate.Parts
|
||||
.GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1))
|
||||
.OrderBy(g => g.Key);
|
||||
foreach (var g in rotGroups)
|
||||
_output.WriteLine($" Rotation {g.Key:F1}°: {g.Count()} parts");
|
||||
|
||||
var hasOverlaps = plate.HasOverlappingParts(out var collisionPoints);
|
||||
_output.WriteLine($"Overlaps: {hasOverlaps} ({collisionPoints.Count} collision pts)");
|
||||
|
||||
if (hasOverlaps)
|
||||
{
|
||||
for (var i = 0; i < System.Math.Min(collisionPoints.Count, 10); i++)
|
||||
_output.WriteLine($" ({collisionPoints[i].X:F2}, {collisionPoints[i].Y:F2})");
|
||||
}
|
||||
|
||||
Assert.False(hasOverlaps,
|
||||
$"Engine '{engineName}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjacentParts_ShouldNotOverlap()
|
||||
{
|
||||
var plate = TestHelpers.MakePlate(60, 120,
|
||||
TestHelpers.MakePartAt(0, 0, 10),
|
||||
TestHelpers.MakePartAt(10, 0, 10));
|
||||
|
||||
var hasOverlaps = plate.HasOverlappingParts(out var pts);
|
||||
_output.WriteLine($"Adjacent squares: overlaps={hasOverlaps}, collision count={pts.Count}");
|
||||
|
||||
Assert.False(hasOverlaps, "Adjacent edge-touching parts should not be reported as overlapping");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class EngineRefactorSmokeTests
|
||||
{
|
||||
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 DefaultEngine_FillNestItem_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEngine_FillGroupParts_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
var groupParts = new List<Part> { new Part(drawing) };
|
||||
|
||||
var parts = engine.Fill(groupParts, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "DefaultNestEngine group fill should produce parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEngine_ForceFullAngleSweep_StillWorks()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
engine.ForceFullAngleSweep = true;
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "ForceFullAngleSweep should still produce results");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripEngine_Nest_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new StripNestEngine(plate);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
|
||||
new NestItem { Drawing = MakeRectDrawing(8, 5, "small"), Quantity = 5 },
|
||||
};
|
||||
|
||||
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "StripNestEngine should nest parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEngine_Nest_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
|
||||
new NestItem { Drawing = MakeRectDrawing(15, 8, "b"), Quantity = 3 },
|
||||
};
|
||||
|
||||
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "Base Nest method should place parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BruteForceRunner_StillWorks()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
|
||||
var result = OpenNest.Engine.ML.BruteForceRunner.Run(drawing, plate, forceFullAngleSweep: true);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.PartCount > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class NestPhaseExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(NestPhase.Linear, "Trying rotations...")]
|
||||
[InlineData(NestPhase.RectBestFit, "Trying best fit...")]
|
||||
[InlineData(NestPhase.Pairs, "Trying pairs...")]
|
||||
[InlineData(NestPhase.Nfp, "Trying NFP...")]
|
||||
[InlineData(NestPhase.Extents, "Trying extents...")]
|
||||
[InlineData(NestPhase.Custom, "Custom")]
|
||||
public void DisplayName_ReturnsDescription(NestPhase phase, string expected)
|
||||
{
|
||||
Assert.Equal(expected, phase.DisplayName());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(NestPhase.Linear, "Linear")]
|
||||
[InlineData(NestPhase.RectBestFit, "BestFit")]
|
||||
[InlineData(NestPhase.Pairs, "Pairs")]
|
||||
[InlineData(NestPhase.Nfp, "NFP")]
|
||||
[InlineData(NestPhase.Extents, "Extents")]
|
||||
[InlineData(NestPhase.Custom, "Custom")]
|
||||
public void ShortName_ReturnsShortLabel(NestPhase phase, string expected)
|
||||
{
|
||||
Assert.Equal(expected, phase.ShortName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class NestProgressTests
|
||||
{
|
||||
[Fact]
|
||||
public void BestPartCount_NullParts_ReturnsZero()
|
||||
{
|
||||
var progress = new NestProgress { BestParts = null };
|
||||
Assert.Equal(0, progress.BestPartCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BestPartCount_ReturnsBestPartsCount()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
};
|
||||
var progress = new NestProgress { BestParts = parts };
|
||||
Assert.Equal(2, progress.BestPartCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BestDensity_NullParts_ReturnsZero()
|
||||
{
|
||||
var progress = new NestProgress { BestParts = null };
|
||||
Assert.Equal(0, progress.BestDensity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BestDensity_MatchesFillScoreFormula()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(5, 0, 5),
|
||||
};
|
||||
var workArea = new Box(0, 0, 100, 100);
|
||||
var progress = new NestProgress { BestParts = parts, ActiveWorkArea = workArea };
|
||||
Assert.Equal(1.0, progress.BestDensity, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedWidth_ReturnsPartsSpan()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
};
|
||||
var progress = new NestProgress { BestParts = parts };
|
||||
Assert.Equal(15, progress.NestedWidth, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedLength_ReturnsPartsSpan()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(0, 10, 5),
|
||||
};
|
||||
var progress = new NestProgress { BestParts = parts };
|
||||
Assert.Equal(15, progress.NestedLength, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedArea_ReturnsSumOfPartAreas()
|
||||
{
|
||||
var parts = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
};
|
||||
var progress = new NestProgress { BestParts = parts };
|
||||
Assert.Equal(50, progress.NestedArea, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingBestParts_InvalidatesCache()
|
||||
{
|
||||
var parts1 = new List<Part> { TestHelpers.MakePartAt(0, 0, 5) };
|
||||
var parts2 = new List<Part>
|
||||
{
|
||||
TestHelpers.MakePartAt(0, 0, 5),
|
||||
TestHelpers.MakePartAt(10, 0, 5),
|
||||
};
|
||||
|
||||
var progress = new NestProgress { BestParts = parts1 };
|
||||
Assert.Equal(1, progress.BestPartCount);
|
||||
Assert.Equal(25, progress.NestedArea, precision: 4);
|
||||
|
||||
progress.BestParts = parts2;
|
||||
Assert.Equal(2, progress.BestPartCount);
|
||||
Assert.Equal(50, progress.NestedArea, precision: 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class PartClassifierTests
|
||||
{
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static Drawing MakeRectDrawing(double w, double h)
|
||||
{
|
||||
var pgm = new OpenNest.CNC.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);
|
||||
}
|
||||
|
||||
// ── tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Classify_PureRectangle_ReturnsRectangle()
|
||||
{
|
||||
var drawing = MakeRectDrawing(100, 50);
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Rectangle, result.Type);
|
||||
Assert.True(result.Rectangularity >= 0.99, $"Expected rectangularity>=0.99, got {result.Rectangularity:F4}");
|
||||
Assert.True(result.PerimeterRatio >= 0.99, $"Expected perimeterRatio>=0.99, got {result.PerimeterRatio:F4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_RoundedRectangle_ReturnsRectangle()
|
||||
{
|
||||
// Use the built-in shape builder so arc geometry is constructed correctly.
|
||||
var shape = new RoundedRectangleShape { Length = 100, Width = 50, Radius = 5 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Rectangle, result.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_RectWithSmallNotches_ReturnsRectangle()
|
||||
{
|
||||
// 100x50 rectangle with a 5x2 notch cut into the bottom edge near the centre.
|
||||
// The notch is small relative to the overall perimeter so both metrics still pass.
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
// Bottom edge left section -> notch -> bottom edge right section
|
||||
pgm.Codes.Add(new LinearMove(new Vector(45, 0))); // along bottom to notch start
|
||||
pgm.Codes.Add(new LinearMove(new Vector(45, 2))); // up into notch
|
||||
pgm.Codes.Add(new LinearMove(new Vector(50, 2))); // across notch (5 wide)
|
||||
pgm.Codes.Add(new LinearMove(new Vector(50, 0))); // back down
|
||||
pgm.Codes.Add(new LinearMove(new Vector(100, 0))); // remainder of bottom edge
|
||||
pgm.Codes.Add(new LinearMove(new Vector(100, 50))); // right edge
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 50))); // top edge
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0))); // left edge back to start
|
||||
var drawing = new Drawing("rect-notch", pgm);
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Rectangle, result.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_Circle_ReturnsCircle()
|
||||
{
|
||||
var shape = new CircleShape { Diameter = 50 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Circle, result.Type);
|
||||
Assert.True(result.Circularity >= PartClassifier.CircularityThreshold,
|
||||
$"Expected circularity>={PartClassifier.CircularityThreshold}, got {result.Circularity:F4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_LShape_ReturnsIrregular()
|
||||
{
|
||||
// 100x80 L-shape: full rect minus a 50x40 block from the top-right corner.
|
||||
// Outline (CCW): (0,0) → (100,0) → (100,40) → (50,40) → (50,80) → (0,80) → (0,0)
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(100, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(100, 40)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(50, 40)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(50, 80)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 80)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("lshape", pgm);
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Irregular, result.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_Triangle_ReturnsIrregular()
|
||||
{
|
||||
// Right triangle: base 100, height 80.
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(100, 0)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 80)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("triangle", pgm);
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Irregular, result.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_SerratedEdge_CaughtByPerimeterRatio()
|
||||
{
|
||||
// 100x30 rectangle with 20 teeth of depth 6 along the bottom edge.
|
||||
// Each tooth is 5 wide, 6 deep → adds 12 units of extra perimeter per tooth.
|
||||
// Total extra = 20 * 12 = 240 mm extra over a plain 100mm bottom edge.
|
||||
// MBR perimeter ≈ 2*(100+30) = 260. Actual perimeter ≈ 260 - 100 + 100 + 240 = 500.
|
||||
// PerimeterRatio ≈ 260/500 = 0.52 — well below the 0.85 threshold.
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
|
||||
// Serrated bottom edge: 20 teeth, each 5 wide and 6 deep.
|
||||
var toothCount = 20;
|
||||
var toothWidth = 5.0;
|
||||
var toothDepth = 6.0;
|
||||
var w = toothCount * toothWidth; // = 100
|
||||
var h = 30.0;
|
||||
|
||||
for (var i = 0; i < toothCount; i++)
|
||||
{
|
||||
var x0 = i * toothWidth;
|
||||
pgm.Codes.Add(new LinearMove(new Vector(x0 + toothWidth / 2, -toothDepth)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(x0 + toothWidth, 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)));
|
||||
var drawing = new Drawing("serrated", pgm);
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Irregular, result.Type);
|
||||
Assert.True(result.PerimeterRatio < PartClassifier.PerimeterRatioThreshold,
|
||||
$"Expected perimeterRatio<{PartClassifier.PerimeterRatioThreshold}, got {result.PerimeterRatio:F4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_PrimaryAngle_MatchesMbrAlignment()
|
||||
{
|
||||
// A rectangle rotated 30° around the origin — no edge is axis-aligned, so
|
||||
// RotatingCalipers must find a non-zero MBR angle.
|
||||
var tiltDeg = 30.0;
|
||||
var tiltRad = Angle.ToRadians(tiltDeg);
|
||||
var w = 80.0;
|
||||
var h = 30.0;
|
||||
var cos = System.Math.Cos(tiltRad);
|
||||
var sin = System.Math.Sin(tiltRad);
|
||||
|
||||
// Rotate each corner of an 80×30 rectangle by 30°.
|
||||
Vector Rot(double x, double y) => new Vector(x * cos - y * sin, x * sin + y * cos);
|
||||
|
||||
var p0 = Rot(0, 0);
|
||||
var p1 = Rot(w, 0);
|
||||
var p2 = Rot(w, h);
|
||||
var p3 = Rot(0, h);
|
||||
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new RapidMove(p0));
|
||||
pgm.Codes.Add(new LinearMove(p1));
|
||||
pgm.Codes.Add(new LinearMove(p2));
|
||||
pgm.Codes.Add(new LinearMove(p3));
|
||||
pgm.Codes.Add(new LinearMove(p0));
|
||||
var drawing = new Drawing("tilted-rect", pgm);
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
// The MBR must be tilted — primary angle should be non-zero.
|
||||
Assert.True(System.Math.Abs(result.PrimaryAngle) > 0.01,
|
||||
$"Expected non-zero primary angle for 30°-tilted rect, got {result.PrimaryAngle:F4} rad");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_EmptyDrawing_ReturnsIrregularDefault()
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
var drawing = new Drawing("empty", pgm);
|
||||
|
||||
var result = PartClassifier.Classify(drawing);
|
||||
|
||||
Assert.Equal(PartType.Irregular, result.Type);
|
||||
Assert.Equal(0.0, result.Rectangularity);
|
||||
Assert.Equal(0.0, result.Circularity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class PartFlagTests
|
||||
{
|
||||
[Fact]
|
||||
public void HasManualLeadIns_DefaultsFalse()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("test", pgm);
|
||||
var part = new Part(drawing);
|
||||
|
||||
Assert.False(part.HasManualLeadIns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasManualLeadIns_CanBeSet()
|
||||
{
|
||||
var pgm = new Program();
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
||||
var drawing = new Drawing("test", pgm);
|
||||
var part = new Part(drawing);
|
||||
|
||||
part.HasManualLeadIns = true;
|
||||
|
||||
Assert.True(part.HasManualLeadIns);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.RapidPlanning;
|
||||
using OpenNest.Engine.Sequencing;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class PlateProcessorTests
|
||||
{
|
||||
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y, size: 2);
|
||||
|
||||
[Fact]
|
||||
public void Process_ReturnsAllParts()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(MakePartAt(10, 10));
|
||||
plate.Parts.Add(MakePartAt(30, 30));
|
||||
plate.Parts.Add(MakePartAt(50, 50));
|
||||
|
||||
var processor = new PlateProcessor
|
||||
{
|
||||
Sequencer = new RightSideSequencer(),
|
||||
RapidPlanner = new SafeHeightRapidPlanner()
|
||||
};
|
||||
|
||||
var result = processor.Process(plate);
|
||||
|
||||
Assert.Equal(3, result.Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_PreservesSequenceOrder()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var left = MakePartAt(5, 10);
|
||||
var right = MakePartAt(50, 10);
|
||||
plate.Parts.Add(left);
|
||||
plate.Parts.Add(right);
|
||||
|
||||
var processor = new PlateProcessor
|
||||
{
|
||||
Sequencer = new RightSideSequencer(),
|
||||
RapidPlanner = new SafeHeightRapidPlanner()
|
||||
};
|
||||
|
||||
var result = processor.Process(plate);
|
||||
|
||||
Assert.Same(right, result.Parts[0].Part);
|
||||
Assert.Same(left, result.Parts[1].Part);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_SkipsCuttingStrategy_WhenManualLeadIns()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var part = MakePartAt(10, 10);
|
||||
part.HasManualLeadIns = true;
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var processor = new PlateProcessor
|
||||
{
|
||||
Sequencer = new LeftSideSequencer(),
|
||||
CuttingStrategy = new ContourCuttingStrategy
|
||||
{
|
||||
Parameters = new CuttingParameters()
|
||||
},
|
||||
RapidPlanner = new SafeHeightRapidPlanner()
|
||||
};
|
||||
|
||||
var result = processor.Process(plate);
|
||||
|
||||
Assert.Same(part.Program, result.Parts[0].ProcessedProgram);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_DoesNotMutatePart()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var part = MakePartAt(10, 10);
|
||||
var originalProgram = part.Program;
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var processor = new PlateProcessor
|
||||
{
|
||||
Sequencer = new LeftSideSequencer(),
|
||||
RapidPlanner = new SafeHeightRapidPlanner()
|
||||
};
|
||||
|
||||
var result = processor.Process(plate);
|
||||
|
||||
Assert.Same(originalProgram, part.Program);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_NoCuttingStrategy_PassesProgramThrough()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var part = MakePartAt(10, 10);
|
||||
plate.Parts.Add(part);
|
||||
|
||||
var processor = new PlateProcessor
|
||||
{
|
||||
Sequencer = new LeftSideSequencer(),
|
||||
RapidPlanner = new SafeHeightRapidPlanner()
|
||||
};
|
||||
|
||||
var result = processor.Process(plate);
|
||||
|
||||
Assert.Same(part.Program, result.Parts[0].ProcessedProgram);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_EmptyPlate_ReturnsEmptyResult()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
|
||||
var processor = new PlateProcessor
|
||||
{
|
||||
Sequencer = new LeftSideSequencer(),
|
||||
RapidPlanner = new SafeHeightRapidPlanner()
|
||||
};
|
||||
|
||||
var result = processor.Process(plate);
|
||||
|
||||
Assert.Empty(result.Parts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Engine;
|
||||
|
||||
public class RemnantEngineTests
|
||||
{
|
||||
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 VerticalRemnantEngine_UsesVerticalRemnantComparer()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new VerticalRemnantEngine(plate);
|
||||
Assert.Equal("Vertical Remnant", engine.Name);
|
||||
Assert.Equal(NestDirection.Horizontal, engine.PreferredDirection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalRemnantEngine_UsesHorizontalRemnantComparer()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new HorizontalRemnantEngine(plate);
|
||||
Assert.Equal("Horizontal Remnant", engine.Name);
|
||||
Assert.Equal(NestDirection.Vertical, engine.PreferredDirection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerticalRemnantEngine_Fill_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new VerticalRemnantEngine(plate);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "VerticalRemnantEngine should fill parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalRemnantEngine_Fill_ProducesResults()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var engine = new HorizontalRemnantEngine(plate);
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
|
||||
|
||||
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(parts.Count > 0, "HorizontalRemnantEngine should fill parts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registry_ContainsBothRemnantEngines()
|
||||
{
|
||||
var names = NestEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
|
||||
Assert.Contains("Vertical Remnant", names);
|
||||
Assert.Contains("Horizontal Remnant", names);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerticalRemnantEngine_ProducesTighterXExtent_ThanDefault()
|
||||
{
|
||||
var plate = new Plate(60, 120);
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
var item = new NestItem { Drawing = drawing };
|
||||
|
||||
var defaultEngine = new DefaultNestEngine(plate);
|
||||
var remnantEngine = new VerticalRemnantEngine(plate);
|
||||
|
||||
var defaultParts = defaultEngine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
var remnantParts = remnantEngine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.True(defaultParts.Count > 0);
|
||||
Assert.True(remnantParts.Count > 0);
|
||||
|
||||
var defaultXExtent = defaultParts.Max(p => p.BoundingBox.Right) - defaultParts.Min(p => p.BoundingBox.Left);
|
||||
var remnantXExtent = remnantParts.Max(p => p.BoundingBox.Right) - remnantParts.Min(p => p.BoundingBox.Left);
|
||||
|
||||
Assert.True(remnantXExtent <= defaultXExtent + 0.01,
|
||||
$"Remnant X-extent ({remnantXExtent:F1}) should be <= default ({defaultXExtent:F1})");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user