style: apply CSharpier formatting to all C# sources

Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.

Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).

Formatting is now enforceable: dotnet csharpier check . passes.
This commit is contained in:
aj
2026-09-20 16:41:50 -04:00
parent 8e6fa677fb
commit aec0523062
476 changed files with 16592 additions and 7800 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ public class CutParametersTests
PierceTime = TimeSpan.FromSeconds(1.0),
LeadInLength = 0.25,
PostProcessor = "CL-707",
Units = Units.Millimeters
Units = Units.Millimeters,
};
Assert.Equal(200, cp.Feedrate);
+4 -4
View File
@@ -27,7 +27,7 @@ public class NestRequestTests
{
var request = new NestRequest
{
Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }]
Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }],
};
Assert.Single(request.Parts);
@@ -60,9 +60,9 @@ public class NestRequestTests
Quantity = 3,
PartSpacing = 0.2,
EdgeSpacing = new Spacing(1, 2, 3, 4),
Quadrant = 3
}
]
Quadrant = 3,
},
],
};
var plate = Assert.Single(request.Plates!);
@@ -17,11 +17,28 @@ public class NestResponsePersistenceTests
var nest = CreateNest("test-part", new Size(60, 120));
var request = new NestRequest
{
Parts = [new NestRequestPart { Id = "test-part", DxfPath = "test.dxf", Quantity = 5 }],
Plates = [new NestRequestPlate { Id = "sheet", Size = new Size(60, 120), Quantity = 1, PartSpacing = 0.1 }],
Parts =
[
new NestRequestPart
{
Id = "test-part",
DxfPath = "test.dxf",
Quantity = 5,
},
],
Plates =
[
new NestRequestPlate
{
Id = "sheet",
Size = new Size(60, 120),
Quantity = 1,
PartSpacing = 0.1,
},
],
Material = "Steel",
Thickness = 0.125,
Spacing = 0.1
Spacing = 0.1,
};
var original = new NestResponse
{
@@ -35,7 +52,7 @@ public class NestResponsePersistenceTests
StockUsage = [new NestStockUsage("sheet", 1, 0)],
PlateStockMappings = [new NestPlateStockMapping(0, "sheet")],
Nest = nest,
Request = request
Request = request,
};
var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.nestquote");
@@ -118,9 +135,25 @@ public class NestResponsePersistenceTests
Nest = CreateNest("custom-id", new Size(10, 10)),
Request = new NestRequest
{
Parts = [new NestRequestPart { Id = "custom-id", DxfPath = dxfPath, Quantity = 3 }],
Plates = [new NestRequestPlate { Id = "finite-stock", Size = new Size(10, 10), Quantity = 1 }]
}
Parts =
[
new NestRequestPart
{
Id = "custom-id",
DxfPath = dxfPath,
Quantity = 3,
},
],
Plates =
[
new NestRequestPlate
{
Id = "finite-stock",
Size = new Size(10, 10),
Quantity = 1,
},
],
},
};
try
@@ -131,9 +164,18 @@ public class NestResponsePersistenceTests
Assert.False(File.Exists(dxfPath));
Assert.Equal(NestJobStatus.Incomplete, loaded.Status);
Assert.Equal(NestJobStopReason.StockExhausted, loaded.StopReason);
Assert.Equal(new NestPartFulfillment("custom-id", 3, 1, 2), Assert.Single(loaded.Fulfillment));
Assert.Equal(new NestStockUsage("finite-stock", 1, 0), Assert.Single(loaded.StockUsage));
Assert.Equal(new NestPlateStockMapping(0, "finite-stock"), Assert.Single(loaded.PlateStockMappings));
Assert.Equal(
new NestPartFulfillment("custom-id", 3, 1, 2),
Assert.Single(loaded.Fulfillment)
);
Assert.Equal(
new NestStockUsage("finite-stock", 1, 0),
Assert.Single(loaded.StockUsage)
);
Assert.Equal(
new NestPlateStockMapping(0, "finite-stock"),
Assert.Single(loaded.PlateStockMappings)
);
Assert.Equal("custom-id", Assert.Single(loaded.Request.Parts).Id);
Assert.Equal("finite-stock", Assert.Single(loaded.Request.Plates!).Id);
Assert.Single(loaded.Nest.Drawings);
@@ -159,12 +201,20 @@ public class NestResponsePersistenceTests
{
using var fs = new FileStream(path, FileMode.Create);
using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
await WriteEntryAsync(zip, "request.json", """
await WriteEntryAsync(
zip,
"request.json",
"""
{"parts":[{"dxfPath":"legacy-missing.dxf","quantity":2}],"sheetSize":{"width":60,"length":120},"material":"Steel","thickness":0.06,"spacing":0.1,"strategy":0}
""");
await WriteEntryAsync(zip, "response.json", """
"""
);
await WriteEntryAsync(
zip,
"response.json",
"""
{"sheetCount":1,"utilization":0.75,"cutTimeTicks":120000,"elapsedTicks":340000}
""");
"""
);
var nestEntry = zip.CreateEntry("nest.nest");
await using var stream = nestEntry.Open();
+108 -39
View File
@@ -22,7 +22,7 @@ public class NestRunnerTests
{
Parts = [new NestRequestPart { DxfPath = dxfPath, Quantity = 4 }],
SheetSize = new Size(10, 10),
Spacing = 0.1
Spacing = 0.1,
};
var response = await NestRunner.RunAsync(request);
@@ -34,7 +34,10 @@ public class NestRunnerTests
var stock = Assert.Single(response.StockUsage);
Assert.Equal("legacy-sheet", stock.StockId);
Assert.Null(stock.Remaining);
Assert.All(response.PlateStockMappings, mapping => Assert.Equal("legacy-sheet", mapping.StockId));
Assert.All(
response.PlateStockMappings,
mapping => Assert.Equal("legacy-sheet", mapping.StockId)
);
Assert.Equal(response.SheetCount, response.PlateStockMappings.Count);
Assert.NotNull(response.Nest);
Assert.Contains(response.Nest.Drawings, drawing => drawing.Name == "part-0");
@@ -54,21 +57,44 @@ public class NestRunnerTests
try
{
var response = await NestRunner.RunAsync(new NestRequest
{
Parts = [new NestRequestPart { Id = "square", DxfPath = dxfPath, Quantity = 5 }],
Plates =
[
new NestRequestPlate { Id = "small", Size = new Size(5, 5), Quantity = 1 },
new NestRequestPlate { Id = "large", Size = new Size(9, 9), Quantity = 1 }
]
});
var response = await NestRunner.RunAsync(
new NestRequest
{
Parts =
[
new NestRequestPart
{
Id = "square",
DxfPath = dxfPath,
Quantity = 5,
},
],
Plates =
[
new NestRequestPlate
{
Id = "small",
Size = new Size(5, 5),
Quantity = 1,
},
new NestRequestPlate
{
Id = "large",
Size = new Size(9, 9),
Quantity = 1,
},
],
}
);
Assert.Equal(NestJobStatus.Complete, response.Status);
Assert.Equal(2, response.SheetCount);
Assert.Equal(5, Assert.Single(response.Fulfillment).Placed);
Assert.Equal(0, response.Fulfillment[0].Unplaced);
Assert.Equal(new[] { "large", "small" }, response.PlateStockMappings.Select(mapping => mapping.StockId).Order());
Assert.Equal(
new[] { "large", "small" },
response.PlateStockMappings.Select(mapping => mapping.StockId).Order()
);
Assert.Equal(1, response.StockUsage.Single(usage => usage.StockId == "small").Used);
Assert.Equal(1, response.StockUsage.Single(usage => usage.StockId == "large").Used);
Assert.All(response.StockUsage, usage => Assert.Equal(0, usage.Remaining));
@@ -86,11 +112,21 @@ public class NestRunnerTests
try
{
var response = await NestRunner.RunAsync(new NestRequest
{
Parts = [new NestRequestPart { Id = "square", DxfPath = dxfPath, Quantity = 1 }],
Plates = []
});
var response = await NestRunner.RunAsync(
new NestRequest
{
Parts =
[
new NestRequestPart
{
Id = "square",
DxfPath = dxfPath,
Quantity = 1,
},
],
Plates = [],
}
);
Assert.Equal(NestJobStatus.Incomplete, response.Status);
Assert.Equal(NestJobStopReason.StockExhausted, response.StopReason);
@@ -115,17 +151,30 @@ public class NestRunnerTests
try
{
var response = await NestRunner.RunAsync(new NestRequest
{
Parts = [new NestRequestPart
var response = await NestRunner.RunAsync(
new NestRequest
{
Id = "locked-square",
DxfPath = dxfPath,
Quantity = 2,
AllowRotation = false
}],
Plates = [new NestRequestPlate { Id = "only-sheet", Size = new Size(5, 5), Quantity = 1 }]
});
Parts =
[
new NestRequestPart
{
Id = "locked-square",
DxfPath = dxfPath,
Quantity = 2,
AllowRotation = false,
},
],
Plates =
[
new NestRequestPlate
{
Id = "only-sheet",
Size = new Size(5, 5),
Quantity = 1,
},
],
}
);
Assert.Equal(NestJobStatus.Incomplete, response.Status);
Assert.Equal(NestJobStopReason.StockExhausted, response.StopReason);
@@ -153,15 +202,35 @@ public class NestRunnerTests
try
{
var response = await NestRunner.RunAsync(new NestRequest
{
Parts = [new NestRequestPart { Id = "square", DxfPath = dxfPath, Quantity = 5 }],
Plates =
[
new NestRequestPlate { Id = "small", Size = new Size(5, 5), Quantity = 1 },
new NestRequestPlate { Id = "large", Size = new Size(9, 9), Quantity = 1 }
]
});
var response = await NestRunner.RunAsync(
new NestRequest
{
Parts =
[
new NestRequestPart
{
Id = "square",
DxfPath = dxfPath,
Quantity = 5,
},
],
Plates =
[
new NestRequestPlate
{
Id = "small",
Size = new Size(5, 5),
Quantity = 1,
},
new NestRequestPlate
{
Id = "large",
Size = new Size(9, 9),
Quantity = 1,
},
],
}
);
Assert.Equal(2, response.SheetCount);
Assert.Equal(80d / 106d, response.Utilization, precision: 6);
@@ -184,8 +253,8 @@ public class NestRunnerTests
Parts =
[
new NestRequestPart { Id = "duplicate", DxfPath = dxfPath },
new NestRequestPart { Id = "duplicate", DxfPath = dxfPath }
]
new NestRequestPart { Id = "duplicate", DxfPath = dxfPath },
],
};
await Assert.ThrowsAsync<ArgumentException>(() => NestRunner.RunAsync(request));
@@ -201,7 +270,7 @@ public class NestRunnerTests
{
var request = new NestRequest
{
Parts = [new NestRequestPart { DxfPath = "nonexistent.dxf", Quantity = 1 }]
Parts = [new NestRequestPart { DxfPath = "nonexistent.dxf", Quantity = 1 }],
};
await Assert.ThrowsAsync<FileNotFoundException>(() => NestRunner.RunAsync(request));
+6 -14
View File
@@ -15,7 +15,7 @@ public class BendModelTests
Direction = BendDirection.Up,
Angle = 90,
Radius = 0.06,
NoteText = "UP 90° R0.06"
NoteText = "UP 90° R0.06",
};
Assert.Equal(0, bend.StartPoint.X);
@@ -29,11 +29,7 @@ public class BendModelTests
[Fact]
public void Bend_ToLine_ReturnsGeometryLine()
{
var bend = new Bend
{
StartPoint = new Vector(0, 5),
EndPoint = new Vector(10, 5)
};
var bend = new Bend { StartPoint = new Vector(0, 5), EndPoint = new Vector(10, 5) };
var line = bend.ToLine();
@@ -46,11 +42,7 @@ public class BendModelTests
[Fact]
public void Bend_Length_ComputesCorrectly()
{
var bend = new Bend
{
StartPoint = new Vector(0, 0),
EndPoint = new Vector(3, 4)
};
var bend = new Bend { StartPoint = new Vector(0, 0), EndPoint = new Vector(3, 4) };
Assert.Equal(5.0, bend.Length, 0.001);
}
@@ -78,7 +70,7 @@ public class BendModelTests
{
Direction = BendDirection.Up,
Angle = 90,
Radius = 0.06
Radius = 0.06,
};
var str = bend.ToString();
@@ -96,7 +88,7 @@ public class BendModelTests
StartPoint = new Vector(0, 0),
EndPoint = new Vector(10, 0),
Direction = BendDirection.Down,
Angle = 90
Angle = 90,
};
Assert.Null(bend.SourceEntity);
}
@@ -109,7 +101,7 @@ public class BendModelTests
{
StartPoint = line.StartPoint,
EndPoint = line.EndPoint,
SourceEntity = line
SourceEntity = line,
};
Assert.Same(line, bend.SourceEntity);
}
+28 -8
View File
@@ -14,14 +14,26 @@ public class CadBendNoteTests
public void DetectedNote_HidesOnlyItsSourceText_AndReturnsWhenBendRemoved()
{
var doc = new CadDocument();
doc.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0))
doc.Entities.Add(
new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0))
{
Layer = new Layer("BEND"),
LineType = new LineType("CENTER"),
}
);
var note = new MText
{
Layer = new Layer("BEND"),
LineType = new LineType("CENTER")
});
var note = new MText { Value = "UP 90° R0.125", InsertPoint = new XYZ(5, 0.1, 0), Height = 0.2 };
Value = "UP 90° R0.125",
InsertPoint = new XYZ(5, 0.1, 0),
Height = 0.2,
};
doc.Entities.Add(note);
var unrelated = new MText { Value = note.Value, InsertPoint = new XYZ(50, 50, 0), Height = 0.2 };
var unrelated = new MText
{
Value = note.Value,
InsertPoint = new XYZ(50, 50, 0),
Height = 0.2,
};
doc.Entities.Add(unrelated);
var bends = new SolidWorksBendDetector().DetectBends(doc);
@@ -29,7 +41,13 @@ public class CadBendNoteTests
Assert.Equal(note.Handle, bend.SourceNoteHandle);
var text = new CadText { SourceHandle = note.Handle, Value = note.Value };
Assert.True(text.IsReplacedByBendNote(bends));
Assert.False(new CadText { SourceHandle = unrelated.Handle, Value = note.Value }.IsReplacedByBendNote(bends));
Assert.False(
new CadText
{
SourceHandle = unrelated.Handle,
Value = note.Value,
}.IsReplacedByBendNote(bends)
);
bends.Clear();
Assert.False(text.IsReplacedByBendNote(bends));
@@ -42,6 +60,8 @@ public class CadBendNoteTests
Assert.False(text.IsReplacedByBendNote(null));
Assert.False(text.IsReplacedByBendNote(new[] { new Bend { NoteText = text.Value } }));
Assert.False(text.IsReplacedByBendNote(new[] { new Bend { SourceNoteHandle = 42 } }));
Assert.False(new CadText().IsReplacedByBendNote(new[] { new Bend { NoteText = text.Value } }));
Assert.False(
new CadText().IsReplacedByBendNote(new[] { new Bend { NoteText = text.Value } })
);
}
}
@@ -17,8 +17,7 @@ public class SolidWorksBendDetectorTests
[Fact]
public void Registry_ContainsSolidWorksDetector()
{
Assert.Contains(BendDetectorRegistry.Detectors,
d => d.Name == "SolidWorks");
Assert.Contains(BendDetectorRegistry.Detectors, d => d.Name == "SolidWorks");
}
[Fact]
@@ -32,7 +31,12 @@ public class SolidWorksBendDetectorTests
[Fact]
public void EllipseConverter_ProducesArcsDirectly()
{
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11 Test.dxf");
var path = Path.Combine(
AppContext.BaseDirectory,
"Bending",
"TestData",
"4526 A14 PT11 Test.dxf"
);
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
var result = OpenNest.IO.Dxf.Import(path);
@@ -50,14 +54,21 @@ public class SolidWorksBendDetectorTests
var simplifier = new OpenNest.Geometry.GeometrySimplifier();
var candidates = simplifier.Analyze(shape);
Assert.True(candidates.Count <= 10,
$"Expected <=10 simplifier candidates but got {candidates.Count}");
Assert.True(
candidates.Count <= 10,
$"Expected <=10 simplifier candidates but got {candidates.Count}"
);
}
[Fact]
public void Import_TrimmedEllipse_NoClosingChord()
{
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11.dxf");
var path = Path.Combine(
AppContext.BaseDirectory,
"Bending",
"TestData",
"4526 A14 PT11.dxf"
);
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
var result = OpenNest.IO.Dxf.Import(path);
@@ -78,7 +89,12 @@ public class SolidWorksBendDetectorTests
[Fact]
public void DetectBends_SplitBendLine_PropagatesNote()
{
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT23.dxf");
var path = Path.Combine(
AppContext.BaseDirectory,
"Bending",
"TestData",
"4526 A14 PT23.dxf"
);
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
using var reader = new DxfReader(path);
@@ -88,22 +104,32 @@ public class SolidWorksBendDetectorTests
var bends = detector.DetectBends(doc);
Assert.Equal(5, bends.Count);
Assert.All(bends, b =>
{
Assert.NotNull(b.NoteText);
Assert.NotNull(b.SourceNoteHandle);
Assert.Contains(doc.Entities, e => e.Handle == b.SourceNoteHandle
&& e is ACadSharp.Entities.MText);
Assert.Equal(BendDirection.Up, b.Direction);
Assert.Equal(90.0, b.Angle);
Assert.Equal(0.125, b.Radius);
});
Assert.All(
bends,
b =>
{
Assert.NotNull(b.NoteText);
Assert.NotNull(b.SourceNoteHandle);
Assert.Contains(
doc.Entities,
e => e.Handle == b.SourceNoteHandle && e is ACadSharp.Entities.MText
);
Assert.Equal(BendDirection.Up, b.Direction);
Assert.Equal(90.0, b.Angle);
Assert.Equal(0.125, b.Radius);
}
);
}
[Fact]
public void DetectBends_RealDxf_ParsesNotesCorrectly()
{
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT45.dxf");
var path = Path.Combine(
AppContext.BaseDirectory,
"Bending",
"TestData",
"4526 A14 PT45.dxf"
);
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
using var reader = new DxfReader(path);
@@ -58,9 +58,11 @@ public class BestFitOverlapTests
if (parts[0].Intersects(parts[1], out var pts))
{
overlapping++;
_output.WriteLine($" OVERLAP #{overlapping}: Test {result.Candidate.TestNumber} " +
$"Part2Rot={OpenNest.Math.Angle.ToDegrees(result.Candidate.Part2Rotation):F1}° " +
$"collision pts={pts.Count}");
_output.WriteLine(
$" OVERLAP #{overlapping}: Test {result.Candidate.TestNumber} "
+ $"Part2Rot={OpenNest.Math.Angle.ToDegrees(result.Candidate.Part2Rotation):F1}° "
+ $"collision pts={pts.Count}"
);
}
}
@@ -16,8 +16,10 @@ public class BestFitResultFrameTests
var result = EvaluateOffsetPair(canonical, new Vector(40, 30));
Assert.True(IsNonAxisAligned(result.OptimalRotation),
$"Expected a non-axis-aligned result, got {Angle.ToDegrees(result.OptimalRotation):F2} degrees.");
Assert.True(
IsNonAxisAligned(result.OptimalRotation),
$"Expected a non-axis-aligned result, got {Angle.ToDegrees(result.OptimalRotation):F2} degrees."
);
var parts = result.BuildCanonicalParts();
var bounds = result.GetCutBounds(parts);
@@ -55,7 +57,7 @@ public class BestFitResultFrameTests
Part1Rotation = 0,
Part2Rotation = System.Math.PI,
Part2Offset = offset,
Spacing = 0.25
Spacing = 0.25,
};
return new PairEvaluator().Evaluate(candidate);
@@ -38,9 +38,7 @@ public class NfpBestFitIntegrationTests
var drawing = TestHelpers.MakeLShapeDrawing();
var results = finder.FindBestFits(drawing);
var bestUtilization = results
.Where(r => r.Keep)
.Max(r => r.Utilization);
var bestUtilization = results.Where(r => r.Keep).Max(r => r.Utilization);
Assert.True(bestUtilization > 0.5);
}
@@ -51,7 +49,6 @@ public class NfpBestFitIntegrationTests
var drawing = TestHelpers.MakeSquareDrawing();
var results = finder.FindBestFits(drawing);
Assert.All(results.Where(r => r.Keep), r =>
Assert.Equal("Valid", r.Reason));
Assert.All(results.Where(r => r.Keep), r => Assert.Equal("Valid", r.Reason));
}
}
@@ -119,6 +119,9 @@ public class NfpSlideStrategyTests
validCount++;
}
Assert.True(validCount > 0, $"No non-overlapping candidates found out of {candidates.Count} total. Candidate 0 offset: {candidates[0].Part2Offset}");
Assert.True(
validCount > 0,
$"No non-overlapping candidates found out of {candidates.Count} total. Candidate 0 offset: {candidates[0].Part2Offset}"
);
}
}
+136 -21
View File
@@ -9,9 +9,27 @@ public class BomAnalyzerTests
{
var items = new List<BomItem>
{
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "AISI 304", Qty = 3 },
new BomItem { FileName = "PT03", Thickness = 0.375, Material = "AISI 304", Qty = 1 },
new BomItem
{
FileName = "PT01",
Thickness = 0.25,
Material = "AISI 304",
Qty = 2,
},
new BomItem
{
FileName = "PT02",
Thickness = 0.25,
Material = "AISI 304",
Qty = 3,
},
new BomItem
{
FileName = "PT03",
Thickness = 0.375,
Material = "AISI 304",
Qty = 1,
},
};
var result = BomAnalyzer.Analyze(items, "C:\\fake");
@@ -26,9 +44,27 @@ public class BomAnalyzerTests
{
var items = new List<BomItem>
{
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
new BomItem { FileName = null, Thickness = 0.25, Material = "AISI 304", Qty = 3 },
new BomItem { FileName = "", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
new BomItem
{
FileName = "PT01",
Thickness = 0.25,
Material = "AISI 304",
Qty = 2,
},
new BomItem
{
FileName = null,
Thickness = 0.25,
Material = "AISI 304",
Qty = 3,
},
new BomItem
{
FileName = "",
Thickness = 0.25,
Material = "AISI 304",
Qty = 1,
},
};
var result = BomAnalyzer.Analyze(items, "C:\\fake");
@@ -41,8 +77,20 @@ public class BomAnalyzerTests
{
var items = new List<BomItem>
{
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
new BomItem { FileName = "PT02", Thickness = null, Material = "AISI 304", Qty = 3 },
new BomItem
{
FileName = "PT01",
Thickness = 0.25,
Material = "AISI 304",
Qty = 2,
},
new BomItem
{
FileName = "PT02",
Thickness = null,
Material = "AISI 304",
Qty = 3,
},
};
var result = BomAnalyzer.Analyze(items, "C:\\fake");
@@ -56,8 +104,20 @@ public class BomAnalyzerTests
{
var items = new List<BomItem>
{
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "aisi 304", Qty = 1 },
new BomItem
{
FileName = "PT01",
Thickness = 0.25,
Material = "AISI 304",
Qty = 1,
},
new BomItem
{
FileName = "PT02",
Thickness = 0.25,
Material = "aisi 304",
Qty = 1,
},
};
var result = BomAnalyzer.Analyze(items, "C:\\fake");
@@ -69,7 +129,10 @@ public class BomAnalyzerTests
[Fact]
public void Analyze_MatchesDxfFiles_WithAndWithoutExtension()
{
var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N"));
var tempDir = Path.Combine(
Path.GetTempPath(),
"BomAnalyzerTest_" + Guid.NewGuid().ToString("N")
);
Directory.CreateDirectory(tempDir);
try
@@ -78,14 +141,24 @@ public class BomAnalyzerTests
var items = new List<BomItem>
{
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
new BomItem
{
FileName = "PT01",
Thickness = 0.25,
Material = "AISI 304",
Qty = 2,
},
};
var result = BomAnalyzer.Analyze(items, tempDir);
Assert.Single(result.Groups);
Assert.Single(result.Groups[0].Parts);
Assert.EndsWith(".dxf", result.Groups[0].Parts[0].DxfPath, StringComparison.OrdinalIgnoreCase);
Assert.EndsWith(
".dxf",
result.Groups[0].Parts[0].DxfPath,
StringComparison.OrdinalIgnoreCase
);
Assert.Empty(result.Unmatched);
}
finally
@@ -97,14 +170,23 @@ public class BomAnalyzerTests
[Fact]
public void Analyze_ReportsUnmatchedItems_WhenDxfNotFound()
{
var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N"));
var tempDir = Path.Combine(
Path.GetTempPath(),
"BomAnalyzerTest_" + Guid.NewGuid().ToString("N")
);
Directory.CreateDirectory(tempDir);
try
{
var items = new List<BomItem>
{
new BomItem { FileName = "PT99", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
new BomItem
{
FileName = "PT99",
Thickness = 0.25,
Material = "AISI 304",
Qty = 1,
},
};
var result = BomAnalyzer.Analyze(items, tempDir);
@@ -123,8 +205,20 @@ public class BomAnalyzerTests
{
var items = new List<BomItem>
{
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 1 },
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "Plain Carbon Steel", Qty = 1 },
new BomItem
{
FileName = "PT01",
Thickness = 0.25,
Material = "AISI 304",
Qty = 1,
},
new BomItem
{
FileName = "PT02",
Thickness = 0.25,
Material = "Plain Carbon Steel",
Qty = 1,
},
};
var result = BomAnalyzer.Analyze(items, "C:\\fake");
@@ -135,7 +229,10 @@ public class BomAnalyzerTests
[Fact]
public void Analyze_GroupPartsCount_MatchesBomItems()
{
var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N"));
var tempDir = Path.Combine(
Path.GetTempPath(),
"BomAnalyzerTest_" + Guid.NewGuid().ToString("N")
);
Directory.CreateDirectory(tempDir);
try
@@ -146,9 +243,27 @@ public class BomAnalyzerTests
var items = new List<BomItem>
{
new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 },
new BomItem { FileName = "PT02", Thickness = 0.25, Material = "AISI 304", Qty = 5 },
new BomItem { FileName = "PT03", Thickness = 0.375, Material = "AISI 304", Qty = 1 },
new BomItem
{
FileName = "PT01",
Thickness = 0.25,
Material = "AISI 304",
Qty = 2,
},
new BomItem
{
FileName = "PT02",
Thickness = 0.25,
Material = "AISI 304",
Qty = 5,
},
new BomItem
{
FileName = "PT03",
Thickness = 0.375,
Material = "AISI 304",
Qty = 1,
},
};
var result = BomAnalyzer.Analyze(items, tempDir);
+21 -11
View File
@@ -14,7 +14,11 @@ namespace OpenNest.Tests.CNC
pgm.Codes.Add(new LinearMove(2, 0));
pgm.Codes.Add(new RapidMove(3, 3));
var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0));
var segments = RapidEnumerator.Enumerate(
pgm,
basePos: new Vector(100, 200),
startPos: new Vector(0, 0)
);
// Origin → first pierce, then interior rapid from contour end to next rapid target.
Assert.Equal(2, segments.Count);
@@ -35,7 +39,11 @@ namespace OpenNest.Tests.CNC
pgm.Codes.Add(new LinearMove(0, 5));
pgm.Codes.Add(new RapidMove(1, 1));
var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0));
var segments = RapidEnumerator.Enumerate(
pgm,
basePos: new Vector(100, 200),
startPos: new Vector(0, 0)
);
Assert.Equal(2, segments.Count);
// First rapid: plate origin → part pierce at basePos.
@@ -56,16 +64,18 @@ namespace OpenNest.Tests.CNC
sub.Codes.Add(new LinearMove(0, 0.1));
var pgm = new Program(Mode.Absolute);
pgm.Codes.Add(new RapidMove(0.2, 0.3)); // first pierce (perimeter lead-in)
pgm.Codes.Add(new LinearMove(1.0, 1.0)); // contour move
pgm.Codes.Add(new SubProgramCall
{
Id = 1,
Program = sub,
Offset = new Vector(2, 2), // hole center (drawing-local)
});
pgm.Codes.Add(new RapidMove(0.2, 0.3)); // first pierce (perimeter lead-in)
pgm.Codes.Add(new LinearMove(1.0, 1.0)); // contour move
pgm.Codes.Add(
new SubProgramCall
{
Id = 1,
Program = sub,
Offset = new Vector(2, 2), // hole center (drawing-local)
}
);
var basePos = new Vector(100, 200); // part.Location
var basePos = new Vector(100, 200); // part.Location
var segments = RapidEnumerator.Enumerate(pgm, basePos, startPos: new Vector(0, 0));
// Expected rapids:
@@ -6,39 +6,43 @@ namespace OpenNest.Tests.Cincinnati;
public class CincinnatiFeatureWriterTests
{
private static CincinnatiPostConfig DefaultConfig() => new()
{
UseLineNumbers = true,
FeatureLineNumberStart = 1,
UseAntiDive = true,
KerfCompensation = KerfMode.ControllerSide,
DefaultKerfSide = KerfSide.Left,
ProcessParameterMode = G89Mode.LibraryFile,
InteriorM47 = M47Mode.Always,
ExteriorM47 = M47Mode.Always,
UseSpeedGas = false,
PostedAccuracy = 4,
SafetyHeadraiseDistance = 2000
};
private static FeatureContext SimpleContext(List<ICode>? codes = null) => new()
{
Codes = codes ?? new List<ICode>
private static CincinnatiPostConfig DefaultConfig() =>
new()
{
new RapidMove(13.401, 57.4895),
new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin },
new LinearMove(20.0, 57.5) { Layer = LayerType.Cut }
},
FeatureNumber = 1,
PartName = "BRACKET",
IsFirstFeatureOfPart = true,
IsLastFeatureOnSheet = false,
IsSafetyHeadraise = false,
IsExteriorFeature = false,
LibraryFile = "MILD10",
CutDistance = 18.0,
SheetDiagonal = 30.0
};
UseLineNumbers = true,
FeatureLineNumberStart = 1,
UseAntiDive = true,
KerfCompensation = KerfMode.ControllerSide,
DefaultKerfSide = KerfSide.Left,
ProcessParameterMode = G89Mode.LibraryFile,
InteriorM47 = M47Mode.Always,
ExteriorM47 = M47Mode.Always,
UseSpeedGas = false,
PostedAccuracy = 4,
SafetyHeadraiseDistance = 2000,
};
private static FeatureContext SimpleContext(List<ICode>? codes = null) =>
new()
{
Codes =
codes
?? new List<ICode>
{
new RapidMove(13.401, 57.4895),
new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin },
new LinearMove(20.0, 57.5) { Layer = LayerType.Cut },
},
FeatureNumber = 1,
PartName = "BRACKET",
IsFirstFeatureOfPart = true,
IsLastFeatureOnSheet = false,
IsSafetyHeadraise = false,
IsExteriorFeature = false,
LibraryFile = "MILD10",
CutDistance = 18.0,
SheetDiagonal = 30.0,
};
private static string WriteFeature(CincinnatiPostConfig config, FeatureContext ctx)
{
@@ -229,7 +233,10 @@ public class CincinnatiFeatureWriterTests
endPoint: new Vector(10.0, 20.0),
centerPoint: new Vector(15.0, 20.0),
rotation: RotationType.CW
) { Layer = LayerType.Cut }
)
{
Layer = LayerType.Cut,
},
};
var ctx = SimpleContext(codes);
@@ -247,12 +254,18 @@ public class CincinnatiFeatureWriterTests
var cwCodes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW)
{
Layer = LayerType.Cut,
},
};
var ccwCodes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW) { Layer = LayerType.Cut }
new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW)
{
Layer = LayerType.Cut,
},
};
var cwOutput = WriteFeature(config, SimpleContext(cwCodes));
@@ -350,7 +363,7 @@ public class CincinnatiFeatureWriterTests
{
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin },
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut }
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut },
};
var ctx = SimpleContext(codes);
ctx.IsEtch = true;
@@ -372,7 +385,7 @@ public class CincinnatiFeatureWriterTests
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Cut },
new LinearMove(3.0, 1.0) { Layer = LayerType.Cut },
new LinearMove(4.0, 1.0) { Layer = LayerType.Cut }
new LinearMove(4.0, 1.0) { Layer = LayerType.Cut },
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -390,7 +403,7 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin }
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin },
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -407,7 +420,10 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW)
{
Layer = LayerType.Cut,
},
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -478,7 +494,7 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(1.0, 1.0),
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadout }
new LinearMove(2.0, 1.0) { Layer = LayerType.Leadout },
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -494,7 +510,10 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(12.0, 20.0), new Vector(11.0, 20.0), RotationType.CCW) { Layer = LayerType.Leadin }
new ArcMove(new Vector(12.0, 20.0), new Vector(11.0, 20.0), RotationType.CCW)
{
Layer = LayerType.Leadin,
},
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -512,7 +531,10 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(11.0, 20.0), new Vector(10.5, 20.0), RotationType.CW) { Layer = LayerType.Cut }
new ArcMove(new Vector(11.0, 20.0), new Vector(10.5, 20.0), RotationType.CW)
{
Layer = LayerType.Cut,
},
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -530,7 +552,10 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut }
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW)
{
Layer = LayerType.Cut,
},
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -548,7 +573,10 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(0.0, 0.0),
new ArcMove(new Vector(20.0, 0.0), new Vector(10.0, 0.0), RotationType.CCW) { Layer = LayerType.Cut }
new ArcMove(new Vector(20.0, 0.0), new Vector(10.0, 0.0), RotationType.CCW)
{
Layer = LayerType.Cut,
},
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -566,7 +594,10 @@ public class CincinnatiFeatureWriterTests
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut }
new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW)
{
Layer = LayerType.Cut,
},
};
var ctx = SimpleContext(codes);
var output = WriteFeature(config, ctx);
@@ -14,11 +14,7 @@ public class CincinnatiPostProcessorTests
public void Post_ProducesOutput_ForSinglePlateNest()
{
var nest = CreateTestNest();
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
PostedAccuracy = 4
};
var config = new CincinnatiPostConfig { ConfigurationName = "CL940", PostedAccuracy = 4 };
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
@@ -64,7 +60,7 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig
{
PostedAccuracy = 4,
ArcFeedrate = ArcFeedrateMode.Variables
ArcFeedrate = ArcFeedrateMode.Variables,
};
var post = new CincinnatiPostProcessor(config);
@@ -84,7 +80,7 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig
{
PostedAccuracy = 4,
ArcFeedrate = ArcFeedrateMode.None
ArcFeedrate = ArcFeedrateMode.None,
};
var post = new CincinnatiPostProcessor(config);
@@ -189,18 +185,24 @@ public class CincinnatiPostProcessorTests
UseAntiDive = true,
MaterialLibraries = new()
{
new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.135, Gas = "N2", Library = "MS135N2PANEL.lib" }
new MaterialLibraryEntry
{
Material = "Mild Steel",
Thickness = 0.135,
Gas = "N2",
Library = "MS135N2PANEL.lib",
},
},
EtchLibraries = new()
{
new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" }
}
new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" },
},
};
var opts = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
Converters = { new JsonStringEnumConverter() },
};
var json = JsonSerializer.Serialize(config, opts);
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
@@ -239,7 +241,7 @@ public class CincinnatiPostProcessorTests
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
PartSubprogramStart = 200,
};
var post = new CincinnatiPostProcessor(config);
@@ -279,7 +281,7 @@ public class CincinnatiPostProcessorTests
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
PartSubprogramStart = 200,
};
var post = new CincinnatiPostProcessor(config);
@@ -317,7 +319,7 @@ public class CincinnatiPostProcessorTests
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
PartSubprogramStart = 200,
};
var post = new CincinnatiPostProcessor(config);
@@ -349,7 +351,7 @@ public class CincinnatiPostProcessorTests
{
PostedAccuracy = 4,
UsePartSubprograms = true,
PartSubprogramStart = 200
PartSubprogramStart = 200,
};
var post = new CincinnatiPostProcessor(config);
@@ -372,13 +374,13 @@ public class CincinnatiPostProcessorTests
var config = new CincinnatiPostConfig
{
UsePartSubprograms = true,
PartSubprogramStart = 300
PartSubprogramStart = 300,
};
var opts = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
Converters = { new JsonStringEnumConverter() },
};
var json = JsonSerializer.Serialize(config, opts);
var deserialized = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, opts);
@@ -394,10 +396,10 @@ public class CincinnatiPostProcessorTests
// first segment in the CNC output because the feature writer uses
// the first LinearMove endpoint as the pierce point.
var pgm = new Program(Mode.Incremental);
pgm.Codes.Add(new LinearMove(0, 2)); // (0,0) → (0,2)
pgm.Codes.Add(new LinearMove(2, 0)); // (0,2) → (2,2)
pgm.Codes.Add(new LinearMove(0, -2)); // (2,2) → (2,0)
pgm.Codes.Add(new LinearMove(-2, 0)); // (2,0) → (0,0)
pgm.Codes.Add(new LinearMove(0, 2)); // (0,0) → (0,2)
pgm.Codes.Add(new LinearMove(2, 0)); // (0,2) → (2,2)
pgm.Codes.Add(new LinearMove(0, -2)); // (2,2) → (2,0)
pgm.Codes.Add(new LinearMove(-2, 0)); // (2,0) → (0,0)
var drawing = new Drawing("ClosedSquare", pgm);
var nest = new Nest("TestClosure");
@@ -406,11 +408,7 @@ public class CincinnatiPostProcessorTests
plate.Parts.Add(new Part(drawing, new Vector(1, 1)));
nest.Plates.Add(plate);
var config = new CincinnatiPostConfig
{
UsePartSubprograms = true,
PostedAccuracy = 4
};
var config = new CincinnatiPostConfig { UsePartSubprograms = true, PostedAccuracy = 4 };
var post = new CincinnatiPostProcessor(config);
using var ms = new MemoryStream();
@@ -14,7 +14,7 @@ public class CincinnatiPreambleWriterTests
var config = new CincinnatiPostConfig
{
ConfigurationName = "CL940",
PostedUnits = Units.Inches
PostedUnits = Units.Inches,
};
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
@@ -152,7 +152,7 @@ public class CincinnatiPreambleWriterTests
{
new(48, 96) { Quantity = 5 },
new(72, 48) { Quantity = 2 },
new(36, 48) { Quantity = 1 }
new(36, 48) { Quantity = 1 },
};
writer.WriteMainProgram(sw, "Test", "", plates, "");
@@ -13,10 +13,7 @@ public class CincinnatiSheetWriterTests
[Fact]
public void WriteSheet_EmitsSheetHeader()
{
var config = new CincinnatiPostConfig
{
PostedAccuracy = 4
};
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
@@ -42,7 +39,7 @@ public class CincinnatiSheetWriterTests
var config = new CincinnatiPostConfig
{
PalletExchange = PalletMode.EndOfSheet,
PostedAccuracy = 4
PostedAccuracy = 4,
};
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
@@ -147,7 +144,7 @@ public class CincinnatiSheetWriterTests
var config = new CincinnatiPostConfig
{
PalletExchange = PalletMode.StartAndEnd,
PostedAccuracy = 4
PostedAccuracy = 4,
};
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
@@ -168,7 +165,7 @@ public class CincinnatiSheetWriterTests
var config = new CincinnatiPostConfig
{
PalletExchange = PalletMode.None,
PostedAccuracy = 4
PostedAccuracy = 4,
};
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
@@ -189,7 +186,7 @@ public class CincinnatiSheetWriterTests
var config = new CincinnatiPostConfig
{
PalletExchange = PalletMode.EndOfSheet,
PostedAccuracy = 4
PostedAccuracy = 4,
};
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram())));
@@ -234,7 +231,10 @@ public class CincinnatiSheetWriterTests
var codes = new List<ICode>
{
new RapidMove(10.0, 20.0),
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut }
new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW)
{
Layer = LayerType.Cut,
},
};
var distance = FeatureUtils.ComputeCutDistance(codes);
@@ -249,7 +249,7 @@ public class CincinnatiSheetWriterTests
{
new RapidMove(0, 0),
new LinearMove(1, 0) { Layer = LayerType.Scribe },
new LinearMove(1, 1) { Layer = LayerType.Scribe }
new LinearMove(1, 1) { Layer = LayerType.Scribe },
};
Assert.True(FeatureUtils.IsEtch(codes));
@@ -262,7 +262,7 @@ public class CincinnatiSheetWriterTests
{
new RapidMove(0, 0),
new LinearMove(1, 0) { Layer = LayerType.Cut },
new LinearMove(1, 1) { Layer = LayerType.Cut }
new LinearMove(1, 1) { Layer = LayerType.Cut },
};
Assert.False(FeatureUtils.IsEtch(codes));
@@ -271,10 +271,7 @@ public class CincinnatiSheetWriterTests
[Fact]
public void IsFeatureEtch_ReturnsFalseForRapidsOnly()
{
var codes = new List<ICode>
{
new RapidMove(0, 0)
};
var codes = new List<ICode> { new RapidMove(0, 0) };
Assert.False(FeatureUtils.IsEtch(codes));
}
@@ -303,11 +300,11 @@ public class CincinnatiSheetWriterTests
var output = sb.ToString();
// Under G90, coordinates must be plate-absolute (part coords + part location)
Assert.Contains("G0 X10.5 Y5.25", output); // rapid to pierce
Assert.Contains("G1 X12.5 Y5.25", output); // (2,0) + (10.5,5.25)
Assert.Contains("G1 X12.5 Y7.25", output); // (2,2) + (10.5,5.25)
Assert.Contains("G1 X10.5 Y7.25", output); // (0,2) + (10.5,5.25)
Assert.Contains("G1 X10.5 Y5.25", output); // (0,0) + (10.5,5.25)
Assert.Contains("G0 X10.5 Y5.25", output); // rapid to pierce
Assert.Contains("G1 X12.5 Y5.25", output); // (2,0) + (10.5,5.25)
Assert.Contains("G1 X12.5 Y7.25", output); // (2,2) + (10.5,5.25)
Assert.Contains("G1 X10.5 Y7.25", output); // (0,2) + (10.5,5.25)
Assert.Contains("G1 X10.5 Y5.25", output); // (0,0) + (10.5,5.25)
}
[Fact]
@@ -4,24 +4,49 @@ namespace OpenNest.Tests.Cincinnati;
public class MaterialLibraryResolverTests
{
private static CincinnatiPostConfig ConfigWithLibraries() => new()
{
DefaultAssistGas = "O2",
DefaultEtchGas = "N2",
MaterialLibraries = new()
private static CincinnatiPostConfig ConfigWithLibraries() =>
new()
{
new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.250, Gas = "O2", Library = "MS250O2.lib" },
new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.250, Gas = "N2", Library = "MS250N2.lib" },
new MaterialLibraryEntry { Material = "Aluminum", Thickness = 0.125, Gas = "N2", Library = "AL125N2.lib" },
new MaterialLibraryEntry { Material = "Stainless Steel", Thickness = 0.375, Gas = "AIR", Library = "SS375AIR.lib" }
},
EtchLibraries = new()
{
new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" },
new EtchLibraryEntry { Gas = "O2", Library = "EtchO2.lib" },
new EtchLibraryEntry { Gas = "AIR", Library = "EtchAIR.lib" }
}
};
DefaultAssistGas = "O2",
DefaultEtchGas = "N2",
MaterialLibraries = new()
{
new MaterialLibraryEntry
{
Material = "Mild Steel",
Thickness = 0.250,
Gas = "O2",
Library = "MS250O2.lib",
},
new MaterialLibraryEntry
{
Material = "Mild Steel",
Thickness = 0.250,
Gas = "N2",
Library = "MS250N2.lib",
},
new MaterialLibraryEntry
{
Material = "Aluminum",
Thickness = 0.125,
Gas = "N2",
Library = "AL125N2.lib",
},
new MaterialLibraryEntry
{
Material = "Stainless Steel",
Thickness = 0.375,
Gas = "AIR",
Library = "SS375AIR.lib",
},
},
EtchLibraries = new()
{
new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" },
new EtchLibraryEntry { Gas = "O2", Library = "EtchO2.lib" },
new EtchLibraryEntry { Gas = "AIR", Library = "EtchAIR.lib" },
},
};
[Fact]
public void ResolveCutLibrary_ExactMatch()
@@ -9,7 +9,11 @@ public class SpeedClassifierTests
[InlineData(5.0, 10.0, "FAST")]
[InlineData(4.9, 10.0, "MEDIUM")]
[InlineData(0.5, 10.0, "SLOW")]
public void Classify_ReturnsExpectedClass(double contourLength, double sheetDiagonal, string expected)
public void Classify_ReturnsExpectedClass(
double contourLength,
double sheetDiagonal,
string expected
)
{
var classifier = new SpeedClassifier();
Assert.Equal(expected, classifier.Classify(contourLength, sheetDiagonal));
@@ -19,7 +23,11 @@ public class SpeedClassifierTests
[InlineData(0.8702, 3.927, "CutDist=.8702/3.927")]
[InlineData(18.9722, 3.927, "CutDist=18.9722/3.927")]
[InlineData(0.0, 10.0, "CutDist=0/10")]
public void FormatCutDist_IncludesLengthAndDiagonal(double contour, double diag, string expected)
public void FormatCutDist_IncludesLengthAndDiagonal(
double contour,
double diag,
string expected
)
{
var classifier = new SpeedClassifier();
Assert.Equal(expected, classifier.FormatCutDist(contour, diag));
@@ -67,7 +67,8 @@ public class UserVariablePostTests
var output = PostToString(post, nest);
// Both should use the same #200 — only one declaration
var declarationCount = output.Split('\n')
var declarationCount = output
.Split('\n')
.Count(l => l.Contains("#200=") && l.ToUpper().Contains("SHEET WIDTH"));
Assert.Equal(1, declarationCount);
}
@@ -112,7 +113,11 @@ public class UserVariablePostTests
public void CutOff_VerticalCut_UsesSheetWidthVariable()
{
// Create a plate with a vertical cutoff
var config = new CincinnatiPostConfig { SheetWidthVariable = 110, SheetLengthVariable = 111 };
var config = new CincinnatiPostConfig
{
SheetWidthVariable = 110,
SheetLengthVariable = 111,
};
var nest = new Nest { Name = "Test" };
var plate = new Plate(new Size(48, 96));
@@ -158,7 +163,7 @@ public class UserVariablePostTests
partPgm.Codes.Add(new LinearMove(0, 0));
var drawing = new Drawing("Part1", partPgm);
nest.Drawings.Add(drawing);
plate.Parts.Add(new Part(drawing, new Vector(15, 20))); // Part at Y=20-30, should create gap
plate.Parts.Add(new Part(drawing, new Vector(15, 20))); // Part at Y=20-30, should create gap
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
@@ -16,7 +16,14 @@ public class SubProgramExpansionTests
// Main program: call sub at offset (10,20)
var main = new Program(Mode.Absolute);
main.SubPrograms[1] = sub;
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 20) });
main.Codes.Add(
new SubProgramCall
{
Id = 1,
Program = sub,
Offset = new Vector(10, 20),
}
);
var geometry = ConvertProgram.ToGeometry(main);
@@ -38,8 +45,22 @@ public class SubProgramExpansionTests
var main = new Program(Mode.Absolute);
main.SubPrograms[1] = sub;
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(0, 0) });
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(5, 5) });
main.Codes.Add(
new SubProgramCall
{
Id = 1,
Program = sub,
Offset = new Vector(0, 0),
}
);
main.Codes.Add(
new SubProgramCall
{
Id = 1,
Program = sub,
Offset = new Vector(5, 5),
}
);
var geometry = ConvertProgram.ToGeometry(main);
var lines = geometry.OfType<Line>().ToList();
+27 -12
View File
@@ -14,12 +14,12 @@ public class CutOffGeometryTests
var total = 0.0;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
if (program.Codes[i] is RapidMove rapid && program.Codes[i + 1] is LinearMove linear)
{
total += axis == CutOffAxis.Vertical
? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y)
: System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X);
total +=
axis == CutOffAxis.Vertical
? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y)
: System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X);
}
}
return total;
@@ -113,7 +113,10 @@ public class CutOffGeometryTests
// cover more of the plate than with BB.
// Total cut length should be greater than 80 (BB would give 100-20=80)
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
Assert.True(totalCutLength > 80, $"Geometry should give more cut length than BB. Got {totalCutLength:F2}");
Assert.True(
totalCutLength > 80,
$"Geometry should give more cut length than BB. Got {totalCutLength:F2}"
);
}
[Fact]
@@ -136,7 +139,10 @@ public class CutOffGeometryTests
// BB would exclude full 20 → cut length = 80.
// Geometry excludes only 10 → cut length = 90.
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
Assert.True(totalCutLength > 85, $"Diamond geometry should give more cut than BB. Got {totalCutLength:F2}");
Assert.True(
totalCutLength > 85,
$"Diamond geometry should give more cut than BB. Got {totalCutLength:F2}"
);
}
[Fact]
@@ -159,7 +165,10 @@ public class CutOffGeometryTests
// BB would exclude [10,40] = 30 → cut = 70.
// Geometry excludes [10,30] = 20 → cut = 80.
var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
Assert.True(totalCutLength > 75, $"Triangle geometry should give more cut than BB. Got {totalCutLength:F2}");
Assert.True(
totalCutLength > 75,
$"Triangle geometry should give more cut than BB. Got {totalCutLength:F2}"
);
}
[Fact]
@@ -197,7 +206,10 @@ public class CutOffGeometryTests
// BB would exclude X=[0,20] → cut = 80.
// Circle chord at Y=2 is much shorter → cut > 80.
var totalCutLength = TotalCutLength(cutoff.Drawing.Program, CutOffAxis.Horizontal);
Assert.True(totalCutLength > 80, $"Circle horizontal cut should use geometry. Got {totalCutLength:F2}");
Assert.True(
totalCutLength > 80,
$"Circle horizontal cut should use geometry. Got {totalCutLength:F2}"
);
}
[Fact]
@@ -283,7 +295,7 @@ public class CutOffGeometryTests
var entities = new List<Entity>
{
new Line(new Vector(0, 0), new Vector(10, 0)),
new Arc(new Vector(5, 5), 5, 0, System.Math.PI)
new Arc(new Vector(5, 5), 5, 0, System.Math.PI),
};
var points = entities.CollectPoints();
@@ -333,7 +345,10 @@ public class CutOffGeometryTests
var cutPart = plate.Parts.First(p => p.BaseDrawing.IsCutOff);
// BB would give 80 (100 - 20). Geometry should give more.
var totalCutLength = TotalCutLength(cutPart.BaseDrawing.Program);
Assert.True(totalCutLength > 80, $"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}");
Assert.True(
totalCutLength > 80,
$"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}"
);
}
[Fact]
@@ -357,7 +372,7 @@ public class CutOffGeometryTests
// Combine all entities (simulating what ShapeBuilder.GetShapes would produce)
var entities = new List<Entity>();
entities.AddRange(inner.Entities); // inner first — worst case for old heuristic
entities.AddRange(inner.Entities); // inner first — worst case for old heuristic
entities.AddRange(outer.Entities);
var profile = new ShapeProfile(entities);
@@ -97,8 +97,12 @@ public class CutOffSerializationTests
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 });
plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 });
plate.CutOffs.Add(
new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 }
);
plate.CutOffs.Add(
new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 }
);
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
+4 -16
View File
@@ -165,10 +165,7 @@ public class CutOffTests
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical)
{
StartLimit = 20.0
};
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical) { StartLimit = 20.0 };
cutoff.Regenerate(plate, settings);
// AwayFromOrigin: RapidMove to near end (StartLimit=20), LinearMove to far end (100).
@@ -182,10 +179,7 @@ public class CutOffTests
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical)
{
EndLimit = 80.0
};
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical) { EndLimit = 80.0 };
cutoff.Regenerate(plate, settings);
// AwayFromOrigin: RapidMove to near end (0), LinearMove to far end (EndLimit=80).
@@ -200,16 +194,10 @@ public class CutOffTests
var plate = new Plate(60, 120);
var settings = new CutOffSettings { PartClearance = 0 };
var hCut = new CutOff(new Vector(85, 30), CutOffAxis.Horizontal)
{
EndLimit = 85.0
};
var hCut = new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 };
hCut.Regenerate(plate, settings);
var vCut = new CutOff(new Vector(85, 30), CutOffAxis.Vertical)
{
StartLimit = 30.0
};
var vCut = new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 };
vCut.Regenerate(plate, settings);
Assert.True(hCut.Drawing.Program.Codes.Count > 0);
@@ -51,15 +51,17 @@ public class ApplySingleTests
{
Parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
}
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
},
};
var clickPoint = new Vector(5, 0);
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External);
var hasLeadin = result.Program.Codes.OfType<LinearMove>().Any(m => m.Layer == LayerType.Leadin);
var hasLeadin = result
.Program.Codes.OfType<LinearMove>()
.Any(m => m.Layer == LayerType.Leadin);
Assert.True(hasLeadin);
}
@@ -71,8 +73,8 @@ public class ApplySingleTests
{
Parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
}
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
},
};
var clickPoint = new Vector(5, 0);
@@ -82,7 +84,8 @@ public class ApplySingleTests
// Convert back to absolute to check positions
result.Program.Mode = Mode.Absolute;
var firstLinear = result.Program.Codes.OfType<LinearMove>()
var firstLinear = result
.Program.Codes.OfType<LinearMove>()
.First(m => m.Layer == LayerType.Leadin);
Assert.Equal(clickPoint.X, firstLinear.EndPoint.X, 4);
Assert.Equal(clickPoint.Y, firstLinear.EndPoint.Y, 4);
@@ -96,8 +99,8 @@ public class ApplySingleTests
{
Parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
}
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
},
};
var clickPoint = new Vector(5, 0);
@@ -116,8 +119,8 @@ public class ApplySingleTests
Parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
}
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
},
};
var clickPoint = new Vector(10, 0);
@@ -12,7 +12,7 @@ public class CuttingParametersSerializerTests
{
AutoTabMinSize = 0.5,
AutoTabMaxSize = 3.0,
ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
};
var json = CuttingParametersSerializer.Serialize(original);
@@ -25,7 +25,8 @@ public class CuttingParametersSerializerTests
[Fact]
public void Deserialize_MissingAutoTabFields_DefaultsToZero()
{
var json = "{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625}";
var json =
"{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625}";
var restored = CuttingParametersSerializer.Deserialize(json);
@@ -1,8 +1,8 @@
using System.Linq;
using OpenNest.CNC;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Converters;
using OpenNest.Geometry;
using System.Linq;
namespace OpenNest.Tests.CuttingStrategy;
@@ -47,7 +47,12 @@ public class HoleSubProgramTests
[Fact]
public void SubProgramCall_ToString_IncludesOffsetAndRotation()
{
var call = new SubProgramCall { Id = 1000, Offset = new Vector(1.5, 2.5), Rotation = 30 };
var call = new SubProgramCall
{
Id = 1000,
Offset = new Vector(1.5, 2.5),
Rotation = 30,
};
var str = call.ToString();
Assert.Contains("P1000", str);
Assert.Contains("X1.5", str);
@@ -119,8 +124,8 @@ public class HoleSubProgramTests
Parameters = new CuttingParameters
{
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
ArcCircleLeadOut = new NoLeadOut()
}
ArcCircleLeadOut = new NoLeadOut(),
},
};
var result = strategy.Apply(pgm, new Vector(10, 10));
@@ -163,8 +168,8 @@ public class HoleSubProgramTests
RoundLeadInAngles = true,
LeadInAngleIncrement = 5.0,
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
ArcCircleLeadOut = new NoLeadOut()
}
ArcCircleLeadOut = new NoLeadOut(),
},
};
var result = strategy.Apply(pgm, new Vector(10, 10));
@@ -196,22 +201,30 @@ public class HoleSubProgramTests
pgm.Codes.Add(new LinearMove(0, 0));
// Hole 1 at (3, 3)
pgm.Codes.Add(new RapidMove(holeCenter1.X + holeRadius, holeCenter1.Y));
pgm.Codes.Add(new ArcMove(
new Vector(holeCenter1.X + holeRadius, holeCenter1.Y),
holeCenter1, RotationType.CW));
pgm.Codes.Add(
new ArcMove(
new Vector(holeCenter1.X + holeRadius, holeCenter1.Y),
holeCenter1,
RotationType.CW
)
);
// Hole 2 at (7, 5)
pgm.Codes.Add(new RapidMove(holeCenter2.X + holeRadius, holeCenter2.Y));
pgm.Codes.Add(new ArcMove(
new Vector(holeCenter2.X + holeRadius, holeCenter2.Y),
holeCenter2, RotationType.CW));
pgm.Codes.Add(
new ArcMove(
new Vector(holeCenter2.X + holeRadius, holeCenter2.Y),
holeCenter2,
RotationType.CW
)
);
var strategy = new ContourCuttingStrategy
{
Parameters = new CuttingParameters
{
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
ArcCircleLeadOut = new NoLeadOut()
}
ArcCircleLeadOut = new NoLeadOut(),
},
};
var result = strategy.Apply(pgm, new Vector(10, 10));
@@ -247,13 +260,21 @@ public class HoleSubProgramTests
pgm.Codes.Add(new LinearMove(0, 10));
pgm.Codes.Add(new LinearMove(0, 0));
pgm.Codes.Add(new RapidMove(holeCenter1.X + holeRadius, holeCenter1.Y));
pgm.Codes.Add(new ArcMove(
new Vector(holeCenter1.X + holeRadius, holeCenter1.Y),
holeCenter1, RotationType.CW));
pgm.Codes.Add(
new ArcMove(
new Vector(holeCenter1.X + holeRadius, holeCenter1.Y),
holeCenter1,
RotationType.CW
)
);
pgm.Codes.Add(new RapidMove(holeCenter2.X + holeRadius, holeCenter2.Y));
pgm.Codes.Add(new ArcMove(
new Vector(holeCenter2.X + holeRadius, holeCenter2.Y),
holeCenter2, RotationType.CW));
pgm.Codes.Add(
new ArcMove(
new Vector(holeCenter2.X + holeRadius, holeCenter2.Y),
holeCenter2,
RotationType.CW
)
);
var drawing = new Drawing("TestPart") { Program = pgm };
var part = new Part(drawing);
@@ -265,7 +286,7 @@ public class HoleSubProgramTests
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
ArcCircleLeadOut = new NoLeadOut(),
ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
ExternalLeadOut = new NoLeadOut()
ExternalLeadOut = new NoLeadOut(),
};
part.ApplyLeadIns(parameters, new Vector(10, 10));
@@ -289,14 +310,22 @@ public class HoleSubProgramTests
// by the last hole's position.
foreach (var line in lines)
{
Assert.True(line.StartPoint.X >= -1 && line.StartPoint.X <= 11,
$"Perimeter line start X={line.StartPoint.X} is outside the 10x10 part bounds");
Assert.True(line.StartPoint.Y >= -1 && line.StartPoint.Y <= 11,
$"Perimeter line start Y={line.StartPoint.Y} is outside the 10x10 part bounds");
Assert.True(line.EndPoint.X >= -1 && line.EndPoint.X <= 11,
$"Perimeter line end X={line.EndPoint.X} is outside the 10x10 part bounds");
Assert.True(line.EndPoint.Y >= -1 && line.EndPoint.Y <= 11,
$"Perimeter line end Y={line.EndPoint.Y} is outside the 10x10 part bounds");
Assert.True(
line.StartPoint.X >= -1 && line.StartPoint.X <= 11,
$"Perimeter line start X={line.StartPoint.X} is outside the 10x10 part bounds"
);
Assert.True(
line.StartPoint.Y >= -1 && line.StartPoint.Y <= 11,
$"Perimeter line start Y={line.StartPoint.Y} is outside the 10x10 part bounds"
);
Assert.True(
line.EndPoint.X >= -1 && line.EndPoint.X <= 11,
$"Perimeter line end X={line.EndPoint.X} is outside the 10x10 part bounds"
);
Assert.True(
line.EndPoint.Y >= -1 && line.EndPoint.Y <= 11,
$"Perimeter line end Y={line.EndPoint.Y} is outside the 10x10 part bounds"
);
}
}
@@ -308,7 +337,14 @@ public class HoleSubProgramTests
var main = new Program(Mode.Absolute);
main.SubPrograms[1] = sub;
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 20) });
main.Codes.Add(
new SubProgramCall
{
Id = 1,
Program = sub,
Offset = new Vector(10, 20),
}
);
var box = main.BoundingBox();
@@ -325,7 +361,14 @@ public class HoleSubProgramTests
var main = new Program(Mode.Absolute);
main.SubPrograms[1] = sub;
main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 0) });
main.Codes.Add(
new SubProgramCall
{
Id = 1,
Program = sub,
Offset = new Vector(10, 0),
}
);
// Rotate 90 degrees CCW around origin
main.Rotate(System.Math.PI / 2);
@@ -28,7 +28,7 @@ public class LeadInAssignerTests
plate.Parts.Add(MakeSquarePartAt(30, 30));
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -50,7 +50,7 @@ public class LeadInAssignerTests
plate.Parts.Add(MakeSquarePartAt(30, 30));
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -66,7 +66,7 @@ public class LeadInAssignerTests
plate.Parts.Add(MakeSquarePartAt(10, 10));
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -86,13 +86,16 @@ public class LeadInAssignerTests
plate.Parts.Add(MakeSquarePartAt(10, 10));
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
assigner.Assign(plate);
var hasLeadin = plate.Parts[0].Program.Codes.OfType<LinearMove>().Any(m => m.Layer == LayerType.Leadin);
var hasLeadin = plate
.Parts[0]
.Program.Codes.OfType<LinearMove>()
.Any(m => m.Layer == LayerType.Leadin);
Assert.True(hasLeadin);
}
@@ -108,7 +111,7 @@ public class LeadInAssignerTests
plate.Parts.Add(part);
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -129,7 +132,7 @@ public class LeadInAssignerTests
plate.Parts.Add(part);
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -152,7 +155,7 @@ public class LeadInAssignerTests
plate.Parts.Add(part);
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -175,7 +178,7 @@ public class LeadInAssignerTests
plate.Parts.Add(part);
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -204,7 +207,7 @@ public class LeadInAssignerTests
plate.Parts.Add(part);
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -213,10 +216,13 @@ public class LeadInAssignerTests
// The lead-in program should produce geometry that contains the
// original rotated shape (plus lead-in/out extensions)
var leadInGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program);
var leadInNonRapid = leadInGeometry.Where(e =>
e.Layer != SpecialLayers.Rapid &&
e.Layer != SpecialLayers.Leadin &&
e.Layer != SpecialLayers.Leadout).ToList();
var leadInNonRapid = leadInGeometry
.Where(e =>
e.Layer != SpecialLayers.Rapid
&& e.Layer != SpecialLayers.Leadin
&& e.Layer != SpecialLayers.Leadout
)
.ToList();
// The bounding box of the cut geometry should be close to original
var origBbox = GetEntityBounds(originalNonRapid);
@@ -242,7 +248,7 @@ public class LeadInAssignerTests
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -264,7 +270,7 @@ public class LeadInAssignerTests
plate.Parts.Add(part);
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -291,7 +297,7 @@ public class LeadInAssignerTests
plate.Parts.Add(part);
plate.CuttingParameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() };
@@ -329,8 +335,10 @@ public class LeadInAssignerTests
private static Box GetEntityBounds(List<OpenNest.Geometry.Entity> entities)
{
double minX = double.MaxValue, minY = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue;
double minX = double.MaxValue,
minY = double.MaxValue;
double maxX = double.MinValue,
maxY = double.MinValue;
foreach (var entity in entities)
{
@@ -344,11 +352,21 @@ public class LeadInAssignerTests
return new Box(minX, minY, maxX - minX, maxY - minY);
}
private static void UpdateBounds(Vector pt, ref double minX, ref double minY, ref double maxX, ref double maxY)
private static void UpdateBounds(
Vector pt,
ref double minX,
ref double minY,
ref double maxX,
ref double maxY
)
{
if (pt.X < minX) minX = pt.X;
if (pt.Y < minY) minY = pt.Y;
if (pt.X > maxX) maxX = pt.X;
if (pt.Y > maxY) maxY = pt.Y;
if (pt.X < minX)
minX = pt.X;
if (pt.Y < minY)
minY = pt.Y;
if (pt.X > maxX)
maxX = pt.X;
if (pt.Y > maxY)
maxY = pt.Y;
}
}
@@ -30,7 +30,12 @@ public class LeadInLayerTagTests
[Fact]
public void LineArcLeadIn_SetsLeadinLayerOnAllMoves()
{
var leadIn = new LineArcLeadIn { LineLength = 0.5, ArcRadius = 0.25, ApproachAngle = 135 };
var leadIn = new LineArcLeadIn
{
LineLength = 0.5,
ArcRadius = 0.25,
ApproachAngle = 135,
};
var codes = leadIn.Generate(Point, Normal);
Assert.All(codes.OfType<LinearMove>(), m => Assert.Equal(LayerType.Leadin, m.Layer));
Assert.All(codes.OfType<ArcMove>(), m => Assert.Equal(LayerType.Leadin, m.Layer));
@@ -39,7 +44,12 @@ public class LeadInLayerTagTests
[Fact]
public void CleanHoleLeadIn_SetsLeadinLayerOnAllMoves()
{
var leadIn = new CleanHoleLeadIn { LineLength = 0.5, ArcRadius = 0.25, Kerf = 0.05 };
var leadIn = new CleanHoleLeadIn
{
LineLength = 0.5,
ArcRadius = 0.25,
Kerf = 0.05,
};
var codes = leadIn.Generate(Point, Normal);
Assert.All(codes.OfType<LinearMove>(), m => Assert.Equal(LayerType.Leadin, m.Layer));
Assert.All(codes.OfType<ArcMove>(), m => Assert.Equal(LayerType.Leadin, m.Layer));
@@ -48,7 +58,13 @@ public class LeadInLayerTagTests
[Fact]
public void LineLineLeadIn_SetsLeadinLayerOnAllMoves()
{
var leadIn = new LineLineLeadIn { Length1 = 0.5, Length2 = 0.3, ApproachAngle1 = 90, ApproachAngle2 = 90 };
var leadIn = new LineLineLeadIn
{
Length1 = 0.5,
Length2 = 0.3,
ApproachAngle1 = 90,
ApproachAngle2 = 90,
};
var codes = leadIn.Generate(Point, Normal);
Assert.All(codes.OfType<LinearMove>(), m => Assert.Equal(LayerType.Leadin, m.Layer));
}
@@ -25,7 +25,7 @@ public class PartLeadInTests
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
};
part.ApplyLeadIns(parameters, new Vector(-5, -5));
@@ -40,7 +40,7 @@ public class PartLeadInTests
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 },
};
part.ApplyLeadIns(parameters, new Vector(-5, -5));
@@ -54,12 +54,14 @@ public class PartLeadInTests
var part = MakeSquarePart();
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
part.ApplyLeadIns(parameters, new Vector(-5, -5));
var hasLeadin = part.Program.Codes.OfType<LinearMove>().Any(m => m.Layer == LayerType.Leadin);
var hasLeadin = part
.Program.Codes.OfType<LinearMove>()
.Any(m => m.Layer == LayerType.Leadin);
Assert.True(hasLeadin);
}
@@ -70,7 +72,7 @@ public class PartLeadInTests
var originalCodeCount = part.Program.Codes.Count;
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
part.ApplyLeadIns(parameters, new Vector(-5, -5));
@@ -90,7 +92,7 @@ public class PartLeadInTests
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
part.ApplyLeadIns(parameters, new Vector(-5, -5));
@@ -108,7 +110,7 @@ public class PartLeadInTests
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
part.ApplyLeadIns(parameters, new Vector(-5, -5));
@@ -131,7 +133,7 @@ public class PartLeadInTests
var part = MakeSquarePart();
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
@@ -146,13 +148,15 @@ public class PartLeadInTests
var part = MakeSquarePart();
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
part.ApplySingleLeadIn(parameters, new Vector(5, 0), entity, ContourType.External);
var hasLeadin = part.Program.Codes.OfType<LinearMove>().Any(m => m.Layer == LayerType.Leadin);
var hasLeadin = part
.Program.Codes.OfType<LinearMove>()
.Any(m => m.Layer == LayerType.Leadin);
Assert.True(hasLeadin);
}
@@ -163,7 +167,7 @@ public class PartLeadInTests
var originalCodeCount = part.Program.Codes.Count;
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
var entity = new Line(new Vector(10, 0), new Vector(0, 0));
@@ -183,7 +187,7 @@ public class PartLeadInTests
var parameters = new CuttingParameters
{
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
};
// After rotation, the edges change. Use a point on the rotated bottom edge.
+13 -7
View File
@@ -51,20 +51,26 @@ public class LocalJsonProviderTests : IDisposable
Value = 0.250,
Kerf = 0.012,
AssistGas = "O2",
LeadIn = new LeadConfig { Type = "Arc", Length = 0.25, Angle = 90.0, Radius = 0.125 },
LeadIn = new LeadConfig
{
Type = "Arc",
Length = 0.25,
Angle = 90.0,
Radius = 0.125,
},
LeadOut = new LeadConfig { Type = "Line", Length = 0.125 },
CutOff = new CutOffConfig
{
PartClearance = 0.5,
Overtravel = 0.25,
Direction = "AwayFromOrigin",
MinSegmentLength = 1.0
MinSegmentLength = 1.0,
},
PlateSizes = new List<string> { "60x120", "48x96" }
}
}
}
}
PlateSizes = new List<string> { "60x120", "48x96" },
},
},
},
},
};
provider.SaveMachine(machine);
+9 -9
View File
@@ -28,7 +28,7 @@ public class MachineConfigTests
AssistGas = "O2",
LeadIn = new LeadConfig { Type = "Arc", Radius = 0.25 },
LeadOut = new LeadConfig { Type = "Line", Length = 0.125 },
PlateSizes = new List<string> { "60x120", "48x96" }
PlateSizes = new List<string> { "60x120", "48x96" },
},
new()
{
@@ -37,9 +37,9 @@ public class MachineConfigTests
AssistGas = "O2",
LeadIn = new LeadConfig { Type = "Arc", Radius = 0.375 },
LeadOut = new LeadConfig { Type = "Line", Length = 0.25 },
PlateSizes = new List<string> { "60x120" }
}
}
PlateSizes = new List<string> { "60x120" },
},
},
},
new()
{
@@ -52,11 +52,11 @@ public class MachineConfigTests
{
Value = 0.250,
Kerf = 0.014,
AssistGas = "N2"
}
}
}
}
AssistGas = "N2",
},
},
},
},
};
}
+18 -8
View File
@@ -44,7 +44,9 @@ public class EngineOverlapTests
var item = new NestItem { Drawing = drawing };
var success = engine.Fill(item);
_output.WriteLine($"Engine: {engine.Name}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}");
_output.WriteLine(
$"Engine: {engine.Name}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}"
);
if (engine is DefaultNestEngine defaultEngine)
{
@@ -54,8 +56,8 @@ public class EngineOverlapTests
}
// Show rotation distribution
var rotGroups = plate.Parts
.GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1))
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");
@@ -69,20 +71,28 @@ public class EngineOverlapTests
_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");
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,
var plate = TestHelpers.MakePlate(
60,
120,
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(10, 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");
Assert.False(
hasOverlaps,
"Adjacent edge-touching parts should not be reported as overlapping"
);
}
}
@@ -22,7 +22,12 @@ public class EngineRefactorSmokeTests
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);
var parts = engine.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts");
}
@@ -35,7 +40,12 @@ public class EngineRefactorSmokeTests
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);
var parts = engine.Fill(
groupParts,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0, "DefaultNestEngine group fill should produce parts");
}
@@ -48,7 +58,12 @@ public class EngineRefactorSmokeTests
engine.ForceFullAngleSweep = true;
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
var parts = engine.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0, "ForceFullAngleSweep should still produce results");
}
@@ -91,7 +106,11 @@ public class EngineRefactorSmokeTests
var plate = new Plate(60, 120);
var drawing = MakeRectDrawing(20, 10);
var result = OpenNest.Engine.ML.BruteForceRunner.Run(drawing, plate, forceFullAngleSweep: true);
var result = OpenNest.Engine.ML.BruteForceRunner.Run(
drawing,
plate,
forceFullAngleSweep: true
);
Assert.NotNull(result);
Assert.True(result.PartCount > 0);
+173 -74
View File
@@ -1,10 +1,10 @@
using OpenNest.Geometry;
using OpenNest.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
using OpenNest.IO;
using Xunit;
using Xunit.Abstractions;
@@ -18,6 +18,7 @@ public class MultiPlateNesterTests
{
_output = output;
}
private static Drawing MakeDrawing(string name, double width, double length)
{
var program = new OpenNest.CNC.Program();
@@ -33,11 +34,7 @@ public class MultiPlateNesterTests
private static NestItem MakeItem(string name, double width, double length, int qty = 1)
{
return new NestItem
{
Drawing = MakeDrawing(name, width, length),
Quantity = qty,
};
return new NestItem { Drawing = MakeDrawing(name, width, length), Quantity = qty };
}
[Fact]
@@ -62,9 +59,9 @@ public class MultiPlateNesterTests
{
var items = new List<NestItem>
{
MakeItem("short-wide", 50, 20), // longest = 50
MakeItem("tall-narrow", 10, 80), // longest = 80
MakeItem("square", 30, 30), // longest = 30
MakeItem("short-wide", 50, 20), // longest = 50
MakeItem("tall-narrow", 10, 80), // longest = 80
MakeItem("square", 30, 30), // longest = 30
};
var sorted = MultiPlateNester.SortItems(items, PartSortOrder.Size);
@@ -153,8 +150,10 @@ public class MultiPlateNesterTests
// All returned zones should have both dims < 12
foreach (var zone in scrap)
{
Assert.True(zone.Width < 12.0 && zone.Length < 12.0,
$"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12");
Assert.True(
zone.Width < 12.0 && zone.Length < 12.0,
$"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12"
);
}
}
@@ -164,7 +163,13 @@ public class MultiPlateNesterTests
public void CreatePlate_UsesTemplateWhenNoOptions()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
template.EdgeSpacing = new Spacing
{
Left = 1,
Right = 1,
Top = 1,
Bottom = 1,
};
var plate = MultiPlateNester.CreatePlate(template, null, null);
@@ -178,13 +183,34 @@ public class MultiPlateNesterTests
public void CreatePlate_PicksSmallestFittingOption()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
template.EdgeSpacing = new Spacing
{
Left = 1,
Right = 1,
Top = 1,
Bottom = 1,
};
var options = new List<PlateOption>
{
new() { Width = 48, Length = 96, Cost = 100 },
new() { Width = 60, Length = 120, Cost = 200 },
new() { Width = 72, Length = 144, Cost = 300 },
new()
{
Width = 48,
Length = 96,
Cost = 100,
},
new()
{
Width = 60,
Length = 120,
Cost = 200,
},
new()
{
Width = 72,
Length = 144,
Cost = 300,
},
};
// Part needs 50x50 work area — 48x96 (after edge spacing: 46x94) — 46 < 50, doesn't fit.
@@ -200,9 +226,24 @@ public class MultiPlateNesterTests
[Fact]
public void EvaluateUpgrade_PrefersCheaperOption()
{
var currentOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
var upgradeOption = new PlateOption { Width = 60, Length = 120, Cost = 160 };
var newPlateOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
var currentOption = new PlateOption
{
Width = 48,
Length = 96,
Cost = 100,
};
var upgradeOption = new PlateOption
{
Width = 60,
Length = 120,
Cost = 160,
};
var newPlateOption = new PlateOption
{
Width = 48,
Length = 96,
Cost = 100,
};
// Upgrade cost = 160 - 100 = 60
// New plate cost with 50% utilization, 50% salvage:
@@ -210,7 +251,12 @@ public class MultiPlateNesterTests
// netNewCost = 100 - 25 = 75
// Upgrade (60) < new plate (75), so upgrade wins
var decision = MultiPlateNester.EvaluateUpgradeVsNew(
currentOption, upgradeOption, newPlateOption, 0.5, 0.5);
currentOption,
upgradeOption,
newPlateOption,
0.5,
0.5
);
Assert.True(decision.ShouldUpgrade);
}
@@ -223,19 +269,17 @@ public class MultiPlateNesterTests
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big1", 80, 40, 1),
MakeItem("big2", 70, 35, 1),
};
var items = new List<NestItem> { MakeItem("big1", 80, 40, 1), MakeItem("big2", 70, 35, 1) };
var options = new MultiPlateNestOptions { Template = template };
var result = MultiPlateNester.Nest(items, options);
// Each large part should be on its own plate.
Assert.True(result.Plates.Count >= 2,
$"Expected at least 2 plates, got {result.Plates.Count}");
Assert.True(
result.Plates.Count >= 2,
$"Expected at least 2 plates, got {result.Plates.Count}"
);
}
[Fact]
@@ -260,8 +304,10 @@ public class MultiPlateNesterTests
// Both small drawing types should share space — not each on their own plate.
// With consolidation, they pack into remaining space alongside the big part.
Assert.True(result.Plates.Count <= 2,
$"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}");
Assert.True(
result.Plates.Count <= 2,
$"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}"
);
Assert.Equal(0, result.UnplacedItems.Count);
}
@@ -271,17 +317,9 @@ public class MultiPlateNesterTests
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big1", 80, 40, 1),
MakeItem("big2", 70, 35, 1),
};
var items = new List<NestItem> { MakeItem("big1", 80, 40, 1), MakeItem("big2", 70, 35, 1) };
var options = new MultiPlateNestOptions
{
Template = template,
AllowPlateCreation = false,
};
var options = new MultiPlateNestOptions { Template = template, AllowPlateCreation = false };
var result = MultiPlateNester.Nest(items, options);
@@ -303,15 +341,15 @@ public class MultiPlateNesterTests
// Plate WorkArea: Width=96, Length=48. Half: 48, 24.
// Part 24x22: Length=24 (not > 24), Width=22 (not > 48) — not Large.
// Area = 528 > 4608/9 = 512 — Medium.
var items = new List<NestItem>
{
MakeItem("medium", 24, 22, 1),
};
var items = new List<NestItem> { MakeItem("medium", 24, 22, 1) };
var options = new MultiPlateNestOptions { Template = template };
var result = MultiPlateNester.Nest(items, options,
existingPlates: new List<Plate> { existingPlate });
var result = MultiPlateNester.Nest(
items,
options,
existingPlates: new List<Plate> { existingPlate }
);
// Part should be placed on the existing plate, not a new one.
Assert.Single(result.Plates);
@@ -331,34 +369,43 @@ public class MultiPlateNesterTests
var nest = new NestReader(nestPath).Read();
var template = nest.PlateDefaults.CreateNew();
_output.WriteLine($"Plate: {template.Size.Width}x{template.Size.Length}, " +
$"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})");
_output.WriteLine(
$"Plate: {template.Size.Width}x{template.Size.Length}, "
+ $"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})"
);
var wa = template.WorkArea();
_output.WriteLine($"Work area: {wa.Width:F1}x{wa.Length:F1}");
_output.WriteLine($"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, " +
$"Medium if area > {wa.Width * wa.Length / 9:F0}");
_output.WriteLine(
$"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, "
+ $"Medium if area > {wa.Width * wa.Length / 9:F0}"
);
_output.WriteLine("---");
var items = new List<NestItem>();
foreach (var d in nest.Drawings)
{
var qty = d.Quantity.Required > 0 ? d.Quantity.Required : d.Quantity.Remaining;
if (qty <= 0) qty = 1;
if (qty <= 0)
qty = 1;
var bb = d.Program.BoundingBox();
var classification = MultiPlateNester.Classify(bb, wa);
_output.WriteLine($" {d.Name,-25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}");
_output.WriteLine(
$" {d.Name, -25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}"
);
items.Add(new NestItem
{
Drawing = d,
Quantity = qty,
StepAngle = d.Constraints.StepAngle,
RotationStart = d.Constraints.StartAngle,
RotationEnd = d.Constraints.EndAngle,
});
items.Add(
new NestItem
{
Drawing = d,
Quantity = qty,
StepAngle = d.Constraints.StepAngle,
RotationStart = d.Constraints.StartAngle,
RotationEnd = d.Constraints.EndAngle,
}
);
}
_output.WriteLine("---");
@@ -366,18 +413,65 @@ public class MultiPlateNesterTests
var plateOptions = new List<PlateOption>
{
new() { Width = 48, Length = 96, Cost = 0 },
new() { Width = 48, Length = 120, Cost = 0 },
new() { Width = 48, Length = 144, Cost = 0 },
new() { Width = 60, Length = 96, Cost = 0 },
new() { Width = 60, Length = 120, Cost = 0 },
new() { Width = 60, Length = 144, Cost = 0 },
new() { Width = 72, Length = 96, Cost = 0 },
new() { Width = 72, Length = 120, Cost = 0 },
new() { Width = 72, Length = 144, Cost = 0 },
new()
{
Width = 48,
Length = 96,
Cost = 0,
},
new()
{
Width = 48,
Length = 120,
Cost = 0,
},
new()
{
Width = 48,
Length = 144,
Cost = 0,
},
new()
{
Width = 60,
Length = 96,
Cost = 0,
},
new()
{
Width = 60,
Length = 120,
Cost = 0,
},
new()
{
Width = 60,
Length = 144,
Cost = 0,
},
new()
{
Width = 72,
Length = 96,
Cost = 0,
},
new()
{
Width = 72,
Length = 120,
Cost = 0,
},
new()
{
Width = 72,
Length = 144,
Cost = 0,
},
};
_output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
_output.WriteLine(
$"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}"
);
_output.WriteLine("");
var options = new MultiPlateNestOptions
@@ -393,16 +487,21 @@ public class MultiPlateNesterTests
for (var i = 0; i < result.Plates.Count; i++)
{
var pr = result.Plates[i];
var groups = pr.Parts.GroupBy(p => p.BaseDrawing.Name)
var groups = pr
.Parts.GroupBy(p => p.BaseDrawing.Name)
.Select(g => $"{g.Key} x{g.Count()}")
.ToList();
_output.WriteLine($" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): " +
$"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]");
_output.WriteLine(
$" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): "
+ $"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]"
);
}
if (result.UnplacedItems.Count > 0)
{
_output.WriteLine($" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}");
_output.WriteLine(
$" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}"
);
}
_output.WriteLine($"\nTotal parts placed: {result.Plates.Sum(p => p.Parts.Count)}");
+9 -7
View File
@@ -1,9 +1,9 @@
using System.Threading;
using OpenNest.CNC;
using OpenNest.Engine;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Threading;
namespace OpenNest.Tests.Engine;
@@ -31,18 +31,20 @@ public class NestInvarianceTests
return new Drawing("L", pgm);
}
private static Plate MakePlate() => new Plate(new Size(500, 500))
{
Quadrant = 1,
PartSpacing = 2,
};
private static Plate MakePlate() =>
new Plate(new Size(500, 500)) { Quadrant = 1, PartSpacing = 2 };
private static int RunFillCount(Drawing drawing, Plate plate)
{
BestFitCache.Clear();
var engine = new DefaultNestEngine(plate);
var item = new NestItem { Drawing = drawing };
var parts = engine.Fill(item, plate.WorkArea(), progress: null, token: CancellationToken.None);
var parts = engine.Fill(
item,
plate.WorkArea(),
progress: null,
token: CancellationToken.None
);
return parts?.Count ?? 0;
}
+33 -16
View File
@@ -30,15 +30,26 @@ public class PartClassifierTests
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}");
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 shape = new RoundedRectangleShape
{
Length = 100,
Width = 50,
Radius = 5,
};
var drawing = shape.GetDrawing();
var result = PartClassifier.Classify(drawing);
@@ -54,14 +65,14 @@ public class PartClassifierTests
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(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
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);
@@ -78,8 +89,10 @@ public class PartClassifierTests
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}");
Assert.True(
result.Circularity >= PartClassifier.CircularityThreshold,
$"Expected circularity>={PartClassifier.CircularityThreshold}, got {result.Circularity:F4}"
);
}
[Fact]
@@ -151,8 +164,10 @@ public class PartClassifierTests
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}");
Assert.True(
result.PerimeterRatio < PartClassifier.PerimeterRatioThreshold,
$"Expected perimeterRatio<{PartClassifier.PerimeterRatioThreshold}, got {result.PerimeterRatio:F4}"
);
}
[Fact]
@@ -186,8 +201,10 @@ public class PartClassifierTests
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");
Assert.True(
System.Math.Abs(result.PrimaryAngle) > 0.01,
$"Expected non-zero primary angle for 30°-tilted rect, got {result.PrimaryAngle:F4} rad"
);
}
[Fact]
+59 -14
View File
@@ -20,14 +20,24 @@ public class PlateOptimizerTests
{
var options = new List<PlateOption>
{
new() { Width = 20, Length = 20, Cost = 100 },
new() { Width = 40, Length = 40, Cost = 400 },
new()
{
Width = 20,
Length = 20,
Cost = 100,
},
new()
{
Width = 40,
Length = 40,
Cost = 400,
},
};
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 }
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 },
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
@@ -42,14 +52,24 @@ public class PlateOptimizerTests
{
var options = new List<PlateOption>
{
new() { Width = 12, Length = 12, Cost = 50 },
new() { Width = 24, Length = 12, Cost = 100 },
new()
{
Width = 12,
Length = 12,
Cost = 50,
},
new()
{
Width = 24,
Length = 12,
Cost = 100,
},
};
var templatePlate = new Plate(24, 12) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 2 }
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 2 },
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
@@ -68,15 +88,25 @@ public class PlateOptimizerTests
// Net = 800 - 1500*(800/1600)*1.0 = 800-750 = 50
var options = new List<PlateOption>
{
new() { Width = 20, Length = 20, Cost = 400 },
new() { Width = 40, Length = 40, Cost = 800 },
new()
{
Width = 20,
Length = 20,
Cost = 400,
},
new()
{
Width = 40,
Length = 40,
Cost = 800,
},
};
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
templatePlate.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 }
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 },
};
var result = PlateOptimizer.Optimize(items, options, 1.0, templatePlate);
@@ -90,14 +120,24 @@ public class PlateOptimizerTests
{
var options = new List<PlateOption>
{
new() { Width = 20, Length = 20, Cost = 100 },
new() { Width = 40, Length = 40, Cost = 400 },
new()
{
Width = 20,
Length = 20,
Cost = 100,
},
new()
{
Width = 40,
Length = 40,
Cost = 400,
},
};
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(30, 30), Quantity = 1 }
new() { Drawing = MakeRectDrawing(30, 30), Quantity = 1 },
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
@@ -111,13 +151,18 @@ public class PlateOptimizerTests
{
var options = new List<PlateOption>
{
new() { Width = 10, Length = 10, Cost = 50 },
new()
{
Width = 10,
Length = 10,
Cost = 50,
},
};
var templatePlate = new Plate(10, 10) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(20, 20), Quantity = 1 }
new() { Drawing = MakeRectDrawing(20, 20), Quantity = 1 },
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
+7 -10
View File
@@ -20,7 +20,7 @@ public class PlateProcessorTests
var processor = new PlateProcessor
{
Sequencer = new RightSideSequencer(),
RapidPlanner = new SafeHeightRapidPlanner()
RapidPlanner = new SafeHeightRapidPlanner(),
};
var result = processor.Process(plate);
@@ -40,7 +40,7 @@ public class PlateProcessorTests
var processor = new PlateProcessor
{
Sequencer = new RightSideSequencer(),
RapidPlanner = new SafeHeightRapidPlanner()
RapidPlanner = new SafeHeightRapidPlanner(),
};
var result = processor.Process(plate);
@@ -60,11 +60,8 @@ public class PlateProcessorTests
var processor = new PlateProcessor
{
Sequencer = new LeftSideSequencer(),
CuttingStrategy = new ContourCuttingStrategy
{
Parameters = new CuttingParameters()
},
RapidPlanner = new SafeHeightRapidPlanner()
CuttingStrategy = new ContourCuttingStrategy { Parameters = new CuttingParameters() },
RapidPlanner = new SafeHeightRapidPlanner(),
};
var result = processor.Process(plate);
@@ -83,7 +80,7 @@ public class PlateProcessorTests
var processor = new PlateProcessor
{
Sequencer = new LeftSideSequencer(),
RapidPlanner = new SafeHeightRapidPlanner()
RapidPlanner = new SafeHeightRapidPlanner(),
};
var result = processor.Process(plate);
@@ -101,7 +98,7 @@ public class PlateProcessorTests
var processor = new PlateProcessor
{
Sequencer = new LeftSideSequencer(),
RapidPlanner = new SafeHeightRapidPlanner()
RapidPlanner = new SafeHeightRapidPlanner(),
};
var result = processor.Process(plate);
@@ -117,7 +114,7 @@ public class PlateProcessorTests
var processor = new PlateProcessor
{
Sequencer = new LeftSideSequencer(),
RapidPlanner = new SafeHeightRapidPlanner()
RapidPlanner = new SafeHeightRapidPlanner(),
};
var result = processor.Process(plate);
+32 -8
View File
@@ -42,7 +42,12 @@ public class RemnantEngineTests
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);
var parts = engine.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0, "VerticalRemnantEngine should fill parts");
}
@@ -54,7 +59,12 @@ public class RemnantEngineTests
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);
var parts = engine.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0, "HorizontalRemnantEngine should fill parts");
}
@@ -77,16 +87,30 @@ public class RemnantEngineTests
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);
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);
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})");
Assert.True(
remnantXExtent <= defaultXExtent + 0.01,
$"Remnant X-extent ({remnantXExtent:F1}) should be <= default ({defaultXExtent:F1})"
);
}
}
@@ -7,6 +7,7 @@ public class AccumulatingProgressTests
private class CapturingProgress : IProgress<NestProgress>
{
public NestProgress Last { get; private set; }
public void Report(NestProgress value) => Last = value;
}
@@ -18,8 +18,10 @@ public class AngleCandidateBuilderTests
return new Drawing("rect", pgm);
}
private static ClassificationResult MakeClassification(double primaryAngle = 0, PartType type = PartType.Irregular)
=> new ClassificationResult { PrimaryAngle = primaryAngle, Type = type };
private static ClassificationResult MakeClassification(
double primaryAngle = 0,
PartType type = PartType.Irregular
) => new ClassificationResult { PrimaryAngle = primaryAngle, Type = type };
[Fact]
public void Build_ReturnsAtLeastTwoAngles()
@@ -81,8 +83,10 @@ public class AngleCandidateBuilderTests
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})");
Assert.True(
secondAngles.Count < firstAngles.Count,
$"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})"
);
}
[Fact]
@@ -128,8 +132,10 @@ public class AngleCandidateBuilderTests
var angles = builder.Build(item, classification, workArea);
Assert.True(angles.Count > 2,
$"User constraints should override rect classification, got {angles.Count} angles");
Assert.True(
angles.Count > 2,
$"User constraints should override rect classification, got {angles.Count} angles"
);
}
[Fact]
@@ -149,7 +155,9 @@ public class AngleCandidateBuilderTests
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}");
Assert.True(
angles.Count > 2,
$"0-to-PI constraint should produce multiple angles, got {angles.Count}"
);
}
}
+100 -42
View File
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using OpenNest;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using Xunit;
using System.Collections.Generic;
namespace OpenNest.Tests.Fill
{
@@ -31,8 +31,7 @@ namespace OpenNest.Tests.Fill
// Verify: after moving, the closest point on the arc should be within
// tolerance of the line, not past it.
var theta = System.Math.Atan2(
line.pt2.X - line.pt1.X, -(line.pt2.Y - line.pt1.Y));
var theta = System.Math.Atan2(line.pt2.X - line.pt1.X, -(line.pt2.Y - line.pt1.Y));
theta = OpenNest.Math.Angle.NormalizeRad(theta + System.Math.PI);
var qx = arc.Center.X + arc.Radius * System.Math.Cos(theta);
var qy = arc.Center.Y + arc.Radius * System.Math.Sin(theta) + dist;
@@ -41,9 +40,11 @@ namespace OpenNest.Tests.Fill
// Line equation: (y - 4) / (x - 3) = (6 - 4) / (7 - 3) = 0.5
// y = 0.5x + 2.5
var lineYAtQx = 0.5 * qx + 2.5;
Assert.True(qy <= lineYAtQx + 0.001,
$"Arc point ({qx:F4}, {qy:F4}) should not be past line (line Y={lineYAtQx:F4} at X={qx:F4}). " +
$"dist={dist:F6}, overshot by {qy - lineYAtQx:F6}");
Assert.True(
qy <= lineYAtQx + 0.001,
$"Arc point ({qx:F4}, {qy:F4}) should not be past line (line Y={lineYAtQx:F4} at X={qx:F4}). "
+ $"dist={dist:F6}, overshot by {qy - lineYAtQx:F6}"
);
}
[Fact]
@@ -57,17 +58,26 @@ namespace OpenNest.Tests.Fill
// Phase 1/2 vertex-only distance: sample arc endpoints + cardinal extreme.
var vertices = new[]
{
new Vector(7, 0), // arc endpoint θ=0
new Vector(3, 0), // arc endpoint θ=π
new Vector(5, 2), // cardinal extreme θ=π/2
new Vector(7, 0), // arc endpoint θ=0
new Vector(3, 0), // arc endpoint θ=π
new Vector(5, 2), // cardinal extreme θ=π/2
};
var vertexMin = double.MaxValue;
foreach (var v in vertices)
{
var d = SpatialQuery.RayEdgeDistance(v.X, v.Y,
line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y, 0, 1);
if (d < vertexMin) vertexMin = d;
var d = SpatialQuery.RayEdgeDistance(
v.X,
v.Y,
line.pt1.X,
line.pt1.Y,
line.pt2.X,
line.pt2.Y,
0,
1
);
if (d < vertexMin)
vertexMin = d;
}
// Full directional distance (includes Phase 3 arc-to-line).
@@ -75,9 +85,12 @@ namespace OpenNest.Tests.Fill
var stationary = new List<Entity> { line };
var fullDist = SpatialQuery.DirectionalDistance(moving, stationary, new Vector(0, 1));
Assert.True(fullDist < vertexMin,
$"Full distance ({fullDist:F6}) should be less than vertex-only ({vertexMin:F6})");
Assert.True(
fullDist < vertexMin,
$"Full distance ({fullDist:F6}) should be less than vertex-only ({vertexMin:F6})"
);
}
private static Drawing MakeRectDrawing(double w, double h)
{
var pgm = new OpenNest.CNC.Program();
@@ -187,12 +200,24 @@ namespace OpenNest.Tests.Fill
// 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);
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);
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);
@@ -235,11 +260,19 @@ namespace OpenNest.Tests.Fill
public void Push_WithSpacing_StopsBeforeNearMissOutsideRawBounds(double degrees)
{
var obstacle = MakeRectPart(20, 20, 10, 10);
var moving = Part.CreateAtOrigin(MakeRectDrawing(10, 10), OpenNest.Math.Angle.ToRadians(degrees));
var moving = Part.CreateAtOrigin(
MakeRectDrawing(10, 10),
OpenNest.Math.Angle.ToRadians(degrees)
);
moving.Offset(60, 31);
Compactor.Push(new List<Part> { moving }, new List<Part> { obstacle },
new Box(0, 0, 100, 100), 2, PushDirection.Left);
Compactor.Push(
new List<Part> { moving },
new List<Part> { obstacle },
new Box(0, 0, 100, 100),
2,
PushDirection.Left
);
// Must stop at the first clearance boundary, not pass the obstacle
// and finish in a clear position on the far side.
@@ -253,8 +286,13 @@ namespace OpenNest.Tests.Fill
var obstacle = MakeRectPart(20, 20, 10, 10);
var moving = MakeRectPart(60, 20, 10, 10);
Compactor.Push(new List<Part> { moving }, new List<Part> { obstacle },
new Box(31, 0, 100, 100), 2, PushDirection.Left);
Compactor.Push(
new List<Part> { moving },
new List<Part> { obstacle },
new Box(31, 0, 100, 100),
2,
PushDirection.Left
);
AssertClearance(moving, obstacle, 2);
Assert.Equal(32, moving.BoundingBox.Left, 7);
@@ -267,31 +305,39 @@ namespace OpenNest.Tests.Fill
foreach (var b in PartGeometry.GetPartLines(obstacle))
{
Assert.False(Intersect.Intersects(a, b, out _));
clearance = System.Math.Min(clearance, a.StartPoint.DistanceTo(b.ClosestPointTo(a.StartPoint)));
clearance = System.Math.Min(clearance, b.StartPoint.DistanceTo(a.ClosestPointTo(b.StartPoint)));
clearance = System.Math.Min(
clearance,
a.StartPoint.DistanceTo(b.ClosestPointTo(a.StartPoint))
);
clearance = System.Math.Min(
clearance,
b.StartPoint.DistanceTo(a.ClosestPointTo(b.StartPoint))
);
}
Assert.True(clearance >= spacing - 1e-7, $"Clearance {clearance:R} is less than spacing {spacing:R}");
Assert.True(
clearance >= spacing - 1e-7,
$"Clearance {clearance:R} is less than spacing {spacing:R}"
);
}
[Fact]
public void Push_Up_AllowsSharedDiagonalEdgeToSeparate()
{
var workArea = new Box(0, 0, 20, 20);
var obstacle = MakeTrianglePart(
new Vector(0, 0),
new Vector(10, 0),
new Vector(0, 10));
var obstacle = MakeTrianglePart(new Vector(0, 0), new Vector(10, 0), new Vector(0, 10));
var movingPart = MakeTrianglePart(
new Vector(0, 10),
new Vector(10, 0),
new Vector(10, 10));
new Vector(10, 10)
);
var distance = Compactor.Push(
new List<Part> { movingPart },
new List<Part> { obstacle },
workArea,
0,
PushDirection.Up);
PushDirection.Up
);
Assert.True(distance > 0);
Assert.True(movingPart.BoundingBox.Top > 19.9);
@@ -303,15 +349,19 @@ namespace OpenNest.Tests.Fill
{
var workArea = new Box(0, 0, 24, 24);
var leftTriangle = MakeTrianglePart(
2, 2,
2,
2,
new Vector(0, 0),
new Vector(8, 0),
new Vector(4, 10));
new Vector(4, 10)
);
var rightTriangle = MakeTrianglePart(
14, 4,
14,
4,
new Vector(0, 10),
new Vector(8, 10),
new Vector(4, 0));
new Vector(4, 0)
);
var moving = new List<Part> { rightTriangle };
var obstacles = new List<Part> { leftTriangle };
@@ -333,21 +383,20 @@ namespace OpenNest.Tests.Fill
public void Push_Left_BlocksWhenSharedDiagonalEdgeWouldOverlap()
{
var workArea = new Box(0, 0, 20, 20);
var obstacle = MakeTrianglePart(
new Vector(0, 0),
new Vector(10, 0),
new Vector(0, 10));
var obstacle = MakeTrianglePart(new Vector(0, 0), new Vector(10, 0), new Vector(0, 10));
var movingPart = MakeTrianglePart(
new Vector(0, 10),
new Vector(10, 0),
new Vector(10, 10));
new Vector(10, 10)
);
var distance = Compactor.Push(
new List<Part> { movingPart },
new List<Part> { obstacle },
workArea,
0,
PushDirection.Left);
PushDirection.Left
);
Assert.Equal(0, distance);
Assert.Equal(0, movingPart.BoundingBox.Left);
@@ -362,7 +411,10 @@ namespace OpenNest.Tests.Fill
var obstacles = new List<Part>();
// direction = left
var direction = new Vector(System.Math.Cos(System.Math.PI), System.Math.Sin(System.Math.PI));
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);
@@ -394,7 +446,13 @@ namespace OpenNest.Tests.Fill
var moving = new List<Part> { part };
var obstacles = new List<Part>();
var distance = Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left);
var distance = Compactor.PushBoundingBox(
moving,
obstacles,
workArea,
0,
PushDirection.Left
);
Assert.True(distance > 0);
Assert.True(part.BoundingBox.Left < 1);
+14 -14
View File
@@ -37,12 +37,12 @@ public class DefaultFillComparerTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
TestHelpers.MakePartAt(40, 0, 10)
TestHelpers.MakePartAt(40, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10)
TestHelpers.MakePartAt(20, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -53,12 +53,12 @@ public class DefaultFillComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10)
TestHelpers.MakePartAt(12, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(50, 0, 10)
TestHelpers.MakePartAt(50, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -76,12 +76,12 @@ public class VerticalRemnantComparerTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 0, 10),
TestHelpers.MakePartAt(80, 0, 10)
TestHelpers.MakePartAt(80, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10)
TestHelpers.MakePartAt(12, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -92,12 +92,12 @@ public class VerticalRemnantComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10)
TestHelpers.MakePartAt(12, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(50, 0, 10)
TestHelpers.MakePartAt(50, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -108,12 +108,12 @@ public class VerticalRemnantComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 0, 10)
TestHelpers.MakePartAt(40, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 40, 10)
TestHelpers.MakePartAt(40, 40, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -144,12 +144,12 @@ public class HorizontalRemnantComparerTests
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 12, 10)
TestHelpers.MakePartAt(0, 12, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 50, 10)
TestHelpers.MakePartAt(0, 50, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
@@ -161,12 +161,12 @@ public class HorizontalRemnantComparerTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 40, 10),
TestHelpers.MakePartAt(0, 80, 10)
TestHelpers.MakePartAt(0, 80, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 12, 10)
TestHelpers.MakePartAt(0, 12, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
+24 -12
View File
@@ -41,10 +41,14 @@ public class FillExtentsTests
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}");
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}"
);
}
}
@@ -82,8 +86,10 @@ public class FillExtentsTests
// 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");
Assert.True(
gap < 1.0,
$"Gap of {gap:F2} is too large — adjustment should fill close to the top"
);
}
[Fact]
@@ -96,8 +102,10 @@ public class FillExtentsTests
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");
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)
@@ -136,10 +144,14 @@ public class FillExtentsTests
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.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);
}
+38 -18
View File
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest;
using OpenNest.CNC;
using OpenNest.Converters;
@@ -6,8 +8,6 @@ using OpenNest.Geometry;
using OpenNest.Math;
using Xunit;
using Xunit.Abstractions;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest.Tests.Fill
{
@@ -32,19 +32,23 @@ namespace OpenNest.Tests.Fill
// Outer circle (CCW)
var outerStart = new Vector(outerRadius * 2, outerRadius);
pgm.Codes.Add(new RapidMove(outerStart));
pgm.Codes.Add(new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW));
pgm.Codes.Add(
new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW)
);
// Inner circle (CW = hole)
var innerStart = new Vector(outerRadius + innerRadius, outerRadius);
pgm.Codes.Add(new RapidMove(innerStart));
pgm.Codes.Add(new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW));
pgm.Codes.Add(
new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW)
);
return new Drawing("ring", pgm);
}
[Theory]
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
[InlineData(1.0, 0.125)] // 2" diameter circle
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
[InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing
[InlineData(1.0, 0.125)] // 2" diameter circle
[InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing
[InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing
public void CircleFill_OffsetBoundaries_DoNotOverlap(double radius, double spacing)
{
var drawing = MakeCircleDrawing(radius);
@@ -58,21 +62,31 @@ namespace OpenNest.Tests.Fill
}
[Theory]
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
public void RingFill_OffsetBoundaries_DoNotOverlap(double outerR, double innerR, double spacing)
[InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5
[InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0
public void RingFill_OffsetBoundaries_DoNotOverlap(
double outerR,
double innerR,
double spacing
)
{
var drawing = MakeRingDrawing(outerR, innerR);
var workArea = new Box(0, 0, 48, 48);
var engine = new FillLinear(workArea, spacing);
var parts = engine.Fill(drawing, 0, NestDirection.Horizontal);
_output.WriteLine($"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts");
_output.WriteLine(
$"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts"
);
AssertNoOffsetOverlap(parts, spacing, outerR * 2);
}
private void AssertNoOffsetOverlap(List<Part> parts, double spacing, double expectedDiameter)
private void AssertNoOffsetOverlap(
List<Part> parts,
double spacing,
double expectedDiameter
)
{
if (parts.Count < 2)
{
@@ -109,21 +123,27 @@ namespace OpenNest.Tests.Fill
violationCount++;
if (violationCount <= 5)
{
_output.WriteLine($" SPACING VIOLATION parts[{i}] vs parts[{j}]: " +
$"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " +
$"expected>={spacing:F4}");
_output.WriteLine(
$" SPACING VIOLATION parts[{i}] vs parts[{j}]: "
+ $"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, "
+ $"expected>={spacing:F4}"
);
}
}
}
}
_output.WriteLine($" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}");
_output.WriteLine(
$" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}"
);
if (violationCount > 0)
{
var maxDeficit = spacing - minGap;
_output.WriteLine($" Max deficit={maxDeficit:F6}");
Assert.Fail($"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}");
Assert.Fail(
$"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}"
);
}
}
}
+28 -6
View File
@@ -13,12 +13,19 @@ public class FillWithDirectionPreferenceTests
[Fact]
public void NullPreference_TriesBothDirections_ReturnsBetter()
{
var hParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(12, 0, 10) };
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);
null,
comparer,
workArea
);
Assert.Equal(2, result.Count);
}
@@ -26,12 +33,24 @@ public class FillWithDirectionPreferenceTests
[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 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);
NestDirection.Horizontal,
comparer,
workArea
);
Assert.Equal(2, result.Count); // H has results, so H is returned (preferred)
}
@@ -43,7 +62,10 @@ public class FillWithDirectionPreferenceTests
var result = FillHelpers.FillWithDirectionPreference(
dir => dir == NestDirection.Horizontal ? new List<Part>() : vParts,
NestDirection.Horizontal, comparer, workArea);
NestDirection.Horizontal,
comparer,
workArea
);
Assert.Equal(1, result.Count); // Falls back to V
}
+6 -3
View File
@@ -1,5 +1,5 @@
using OpenNest.Geometry;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
namespace OpenNest.Tests.Fill;
@@ -57,7 +57,10 @@ public class FillScoreTests
[Fact]
public void Compute_EmptyParts_ReturnsDefault()
{
var score = FillScore.Compute(new System.Collections.Generic.List<Part>(), new Box(0, 0, 100, 100));
var score = FillScore.Compute(
new System.Collections.Generic.List<Part>(),
new Box(0, 0, 100, 100)
);
Assert.Equal(0, score.Count);
}
@@ -69,7 +72,7 @@ public class FillScoreTests
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
TestHelpers.MakePartAt(40, 0, 10)
TestHelpers.MakePartAt(40, 0, 10),
};
var score = FillScore.Compute(parts, new Box(0, 0, 100, 100));
@@ -19,7 +19,12 @@ public class IterativeShrinkFillerTests
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);
var result = IterativeShrinkFiller.Fill(
new List<NestItem>(),
new Box(0, 0, 100, 100),
fillFunc,
1.0
);
Assert.Empty(result.Parts);
Assert.Empty(result.Leftovers);
@@ -42,7 +47,7 @@ public class IterativeShrinkFillerTests
var drawing = MakeRectDrawing(20, 10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
new NestItem { Drawing = drawing, Quantity = 5 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -110,7 +115,7 @@ public class IterativeShrinkFillerTests
{
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 0 }
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 0 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -134,13 +139,19 @@ public class IterativeShrinkFillerTests
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 10 }
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);
var result = IterativeShrinkFiller.Fill(
items,
new Box(0, 0, 100, 100),
fillFunc,
1.0,
cts.Token
);
Assert.NotNull(result);
}
@@ -47,10 +47,10 @@ public class PairOverlapDiagnosticTests
}
[Theory]
[InlineData(0)] // 0 degrees
[InlineData(90)] // 90 degrees
[InlineData(180)] // 180 degrees
[InlineData(270)] // 270 degrees
[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();
@@ -109,8 +109,8 @@ public class PairOverlapDiagnosticTests
}
[Theory]
[InlineData(false)] // simple rect
[InlineData(true)] // rounded rect
[InlineData(false)] // simple rect
[InlineData(true)] // rounded rect
public void FillExtents_NoPairOverlap_At90Degrees(bool rounded)
{
var drawing = rounded ? MakeRoundedRect() : MakeSimpleRect();
@@ -126,8 +126,10 @@ public class PairOverlapDiagnosticTests
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})");
_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
@@ -137,15 +139,21 @@ public class PairOverlapDiagnosticTests
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);
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})");
_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})");
Assert.False(
overlapX > 0.01 && overlapY > 0.01,
$"Parts [{i}] and [{j}] have overlapping bounding boxes "
+ $"({overlapX:F3} x {overlapY:F3})"
);
}
}
}
@@ -172,8 +180,12 @@ public class PairOverlapDiagnosticTests
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}");
_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);
@@ -189,7 +201,11 @@ public class PairOverlapDiagnosticTests
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);
var slideDist = SpatialQuery.DirectionalDistance(
movingLines,
stationaryLines,
PushDirection.Left
);
_output.WriteLine($"Slide distance: {slideDist:F4}");
if (slideDist < double.MaxValue && slideDist > 0)
@@ -198,8 +214,12 @@ public class PairOverlapDiagnosticTests
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})");
_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();
@@ -216,8 +236,10 @@ public class PairOverlapDiagnosticTests
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})");
_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
@@ -230,8 +252,10 @@ public class PairOverlapDiagnosticTests
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})");
Assert.False(
ox > 0.01 && oy > 0.01,
$"Parts [{i}] and [{j}] overlap ({ox:F3} x {oy:F3})"
);
}
}
}
+3 -3
View File
@@ -28,7 +28,7 @@ public class RemnantFillerTests2
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
new NestItem { Drawing = drawing, Quantity = 5 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -52,7 +52,7 @@ public class RemnantFillerTests2
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 3 }
new NestItem { Drawing = drawing, Quantity = 3 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
@@ -92,7 +92,7 @@ public class RemnantFillerTests2
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
new NestItem { Drawing = drawing, Quantity = 5 },
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
+69 -44
View File
@@ -108,16 +108,15 @@ public class RemnantFinderTests
var remnants = finder.FindRemnants();
var gap = remnants.FirstOrDefault(r =>
r.Length >= 19.9 && r.Length <= 20.1 &&
r.Width >= 99.9);
r.Length >= 19.9 && r.Length <= 20.1 && r.Width >= 99.9
);
Assert.NotNull(gap);
}
[Fact]
public void FromPlate_CreatesFinderWithPartsAsObstacles()
{
var plate = TestHelpers.MakePlate(60, 120,
TestHelpers.MakePartAt(0, 0, 20));
var plate = TestHelpers.MakePlate(60, 120, TestHelpers.MakePartAt(0, 0, 20));
var finder = RemnantFinder.FromPlate(plate);
var remnants = finder.FindRemnants();
@@ -146,8 +145,8 @@ public class RemnantFinderTests
// Should find the 80x100 strip on the left
var left = remnants.FirstOrDefault(r =>
r.Length >= 79.9 && r.Length <= 80.1 &&
r.Width >= 99.9);
r.Length >= 79.9 && r.Length <= 80.1 && r.Width >= 99.9
);
Assert.NotNull(left);
}
@@ -163,9 +162,16 @@ public class RemnantFinderTests
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");
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
@@ -177,16 +183,11 @@ public class RemnantFinderTests
[Fact]
public void ConstructorWithObstaclesList()
{
var obstacles = new List<Box>
{
new Box(0, 0, 40, 100),
new Box(60, 0, 40, 100)
};
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.Length >= 19.9 && r.Length <= 20.1);
var gap = remnants.FirstOrDefault(r => r.Length >= 19.9 && r.Length <= 20.1);
Assert.NotNull(gap);
}
@@ -194,15 +195,10 @@ public class RemnantFinderTests
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)
});
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.Length >= 19.9 && r.Length <= 20.1);
var gap = remnants.FirstOrDefault(r => r.Length >= 19.9 && r.Length <= 20.1);
Assert.NotNull(gap);
}
@@ -239,11 +235,17 @@ public class RemnantFinderTests
{
// 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");
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");
Assert.False(
overlaps2,
$"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2"
);
}
}
@@ -254,8 +256,8 @@ public class RemnantFinderTests
// 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));
for (var col = 0; col < 5; col++)
finder.AddObstacle(new Box(col * 20, row * 20, 10, 10));
var remnants = finder.FindRemnants();
@@ -304,16 +306,19 @@ public class RemnantFinderTests
// 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));
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.Length > 100 &&
t.Box.Width > 5);
t.Box.Bottom > 50
&& t.Box.Bottom < 55
&& t.Box.Left < 1
&& t.Box.Length > 100
&& t.Box.Width > 5
);
Assert.True(topGap.Box.Length > 0, "Expected remnant above main grid");
}
@@ -337,24 +342,44 @@ public class RemnantFinderTests
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 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));
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));
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));
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));
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));
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);
+16 -10
View File
@@ -72,8 +72,14 @@ public class ShrinkFillerTests
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);
var result = ShrinkFiller.Shrink(
fillFunc,
item,
box,
1.0,
ShrinkAxis.Length,
token: cts.Token
);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
@@ -84,10 +90,10 @@ public class ShrinkFillerTests
{
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
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);
@@ -101,10 +107,10 @@ public class ShrinkFillerTests
{
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
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);
+3 -3
View File
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Tests.Geometry;
@@ -141,8 +141,8 @@ public class CollisionTests
public void CheckAll_MultiplePolygons_FindsAllOverlaps()
{
var a = MakeSquare(0, 0, 1, 1);
var b = MakeSquare(0.5, 0, 1.5, 1); // overlaps A
var c = MakeSquare(5, 5, 6, 6); // overlaps nobody
var b = MakeSquare(0.5, 0, 1.5, 1); // overlaps A
var c = MakeSquare(5, 5, 6, 6); // overlaps nobody
var results = Collision.CheckAll(new List<Polygon> { a, b, c });
@@ -67,11 +67,7 @@ public class ContourClassificationTests
[Fact]
public void Classify_identifies_etch_layer_shapes()
{
var shapes = new List<Shape>
{
MakeRectShape(0, 0, 100, 50),
MakeEtchShape(),
};
var shapes = new List<Shape> { MakeRectShape(0, 0, 100, 50), MakeEtchShape() };
var contours = ContourInfo.Classify(shapes);
@@ -86,11 +82,7 @@ public class ContourClassificationTests
openShape.Entities.Add(new Line(new Vector(10, 0), new Vector(10, 5)));
// Not closed — doesn't return to (0,0)
var shapes = new List<Shape>
{
MakeRectShape(0, 0, 100, 50),
openShape,
};
var shapes = new List<Shape> { MakeRectShape(0, 0, 100, 50), openShape };
var contours = ContourInfo.Classify(shapes);
@@ -100,11 +92,7 @@ public class ContourClassificationTests
[Fact]
public void Classify_orders_holes_first_perimeter_last()
{
var shapes = new List<Shape>
{
MakeRectShape(0, 0, 100, 50),
MakeCircleShape(25, 25, 5),
};
var shapes = new List<Shape> { MakeRectShape(0, 0, 100, 50), MakeCircleShape(25, 25, 5) };
var contours = ContourInfo.Classify(shapes);
+153 -46
View File
@@ -1,9 +1,9 @@
using System.Linq;
using OpenNest.Geometry;
using OpenNest.IO;
using OpenNest.Math;
using Xunit;
using Xunit.Abstractions;
using System.Linq;
namespace OpenNest.Tests.Geometry;
@@ -89,8 +89,14 @@ public class EllipseConverterTests
public void Convert_Circle_ProducesOneOrTwoArcs()
{
var result = EllipseConverter.Convert(
new Vector(0, 0), semiMajor: 10, semiMinor: 10, rotation: 0,
startParam: 0, endParam: Angle.TwoPI, tolerance: 0.001);
new Vector(0, 0),
semiMajor: 10,
semiMinor: 10,
rotation: 0,
startParam: 0,
endParam: Angle.TwoPI,
tolerance: 0.001
);
Assert.All(result, e => Assert.IsType<Arc>(e));
Assert.InRange(result.Count, 1, 4);
@@ -103,8 +109,14 @@ public class EllipseConverterTests
var b = 7.0;
var tolerance = 0.001;
var result = EllipseConverter.Convert(
new Vector(0, 0), a, b, rotation: 0,
startParam: 0, endParam: Angle.TwoPI, tolerance: tolerance);
new Vector(0, 0),
a,
b,
rotation: 0,
startParam: 0,
endParam: Angle.TwoPI,
tolerance: tolerance
);
Assert.True(result.Count >= 4, $"Expected at least 4 arcs, got {result.Count}");
Assert.All(result, e => Assert.IsType<Arc>(e));
@@ -113,9 +125,11 @@ public class EllipseConverterTests
{
var arc = (Arc)entity;
var maxDev = MaxDeviationFromEllipse(arc, new Vector(0, 0), a, b, 0, 50);
Assert.True(maxDev <= tolerance,
$"Arc at center ({arc.Center.X:F4},{arc.Center.Y:F4}) r={arc.Radius:F4} " +
$"deviates {maxDev:F6} from ellipse (tolerance={tolerance})");
Assert.True(
maxDev <= tolerance,
$"Arc at center ({arc.Center.X:F4},{arc.Center.Y:F4}) r={arc.Radius:F4} "
+ $"deviates {maxDev:F6} from ellipse (tolerance={tolerance})"
);
}
}
@@ -126,18 +140,29 @@ public class EllipseConverterTests
var b = 3.0;
var tolerance = 0.001;
var result = EllipseConverter.Convert(
new Vector(0, 0), a, b, rotation: 0,
startParam: 0, endParam: Angle.TwoPI, tolerance: tolerance);
new Vector(0, 0),
a,
b,
rotation: 0,
startParam: 0,
endParam: Angle.TwoPI,
tolerance: tolerance
);
Assert.True(result.Count >= 8, $"Expected at least 8 arcs for eccentric ellipse, got {result.Count}");
Assert.True(
result.Count >= 8,
$"Expected at least 8 arcs for eccentric ellipse, got {result.Count}"
);
Assert.All(result, e => Assert.IsType<Arc>(e));
foreach (var entity in result)
{
var arc = (Arc)entity;
var maxDev = MaxDeviationFromEllipse(arc, new Vector(0, 0), a, b, 0, 50);
Assert.True(maxDev <= tolerance,
$"Deviation {maxDev:F6} exceeds tolerance {tolerance}");
Assert.True(
maxDev <= tolerance,
$"Deviation {maxDev:F6} exceeds tolerance {tolerance}"
);
}
}
@@ -148,8 +173,14 @@ public class EllipseConverterTests
var b = 5.0;
var tolerance = 0.001;
var result = EllipseConverter.Convert(
new Vector(0, 0), a, b, rotation: 0,
startParam: 0, endParam: System.Math.PI / 2, tolerance: tolerance);
new Vector(0, 0),
a,
b,
rotation: 0,
startParam: 0,
endParam: System.Math.PI / 2,
tolerance: tolerance
);
Assert.NotEmpty(result);
Assert.All(result, e => Assert.IsType<Arc>(e));
@@ -169,23 +200,27 @@ public class EllipseConverterTests
public void Convert_EndpointContinuity_ArcsConnect()
{
var result = EllipseConverter.Convert(
new Vector(5, 10), semiMajor: 15, semiMinor: 8, rotation: 0.5,
startParam: 0, endParam: Angle.TwoPI, tolerance: 0.001);
new Vector(5, 10),
semiMajor: 15,
semiMinor: 8,
rotation: 0.5,
startParam: 0,
endParam: Angle.TwoPI,
tolerance: 0.001
);
for (var i = 0; i < result.Count - 1; i++)
{
var current = (Arc)result[i];
var next = (Arc)result[i + 1];
var gap = current.EndPoint().DistanceTo(next.StartPoint());
Assert.True(gap < 1e-6,
$"Gap of {gap:E4} between arc {i} and arc {i + 1}");
Assert.True(gap < 1e-6, $"Gap of {gap:E4} between arc {i} and arc {i + 1}");
}
var lastArc = (Arc)result[^1];
var firstArc = (Arc)result[0];
var closingGap = lastArc.EndPoint().DistanceTo(firstArc.StartPoint());
Assert.True(closingGap < 1e-6,
$"Closing gap of {closingGap:E4}");
Assert.True(closingGap < 1e-6, $"Closing gap of {closingGap:E4}");
}
[Fact]
@@ -197,16 +232,25 @@ public class EllipseConverterTests
var b = 6.0;
var tolerance = 0.001;
var result = EllipseConverter.Convert(center, a, b, rotation,
startParam: 0, endParam: Angle.TwoPI, tolerance: tolerance);
var result = EllipseConverter.Convert(
center,
a,
b,
rotation,
startParam: 0,
endParam: Angle.TwoPI,
tolerance: tolerance
);
Assert.NotEmpty(result);
foreach (var entity in result)
{
var arc = (Arc)entity;
var maxDev = MaxDeviationFromEllipse(arc, center, a, b, rotation, 50);
Assert.True(maxDev <= tolerance,
$"Deviation {maxDev:F6} exceeds tolerance {tolerance}");
Assert.True(
maxDev <= tolerance,
$"Deviation {maxDev:F6} exceeds tolerance {tolerance}"
);
}
}
@@ -221,7 +265,7 @@ public class EllipseConverterTests
MajorAxisEndPoint = new CSMath.XYZ(10, 0, 0),
RadiusRatio = 0.6,
StartParameter = 0,
EndParameter = System.Math.PI * 2
EndParameter = System.Math.PI * 2,
};
doc.Entities.Add(ellipse);
@@ -253,19 +297,24 @@ public class EllipseConverterTests
public void DxfImport_ArcBoundingBoxes_Diagnostic()
{
var path = @"C:\Users\aisaacs\Desktop\11ga tab.dxf";
if (!System.IO.File.Exists(path)) return;
if (!System.IO.File.Exists(path))
return;
var result = Dxf.Import(path);
var all = (System.Collections.Generic.IEnumerable<IBoundable>)result.Entities;
var bbox = all.GetBoundingBox();
_output.WriteLine($"Overall: X={bbox.X:F4} Y={bbox.Y:F4} W={bbox.Length:F4} H={bbox.Width:F4}");
_output.WriteLine(
$"Overall: X={bbox.X:F4} Y={bbox.Y:F4} W={bbox.Length:F4} H={bbox.Width:F4}"
);
for (var i = 0; i < result.Entities.Count; i++)
{
var e = result.Entities[i];
var b = e.BoundingBox;
var flag = (b.Length > 1 || b.Width > 1) ? " ***" : "";
_output.WriteLine($"{i + 1,3}. {e.GetType().Name,-8} X={b.X:F4} Y={b.Y:F4} W={b.Length:F4} H={b.Width:F4}{flag}");
_output.WriteLine(
$"{i + 1, 3}. {e.GetType().Name, -8} X={b.X:F4} Y={b.Y:F4} W={b.Length:F4} H={b.Width:F4}{flag}"
);
}
}
@@ -279,7 +328,7 @@ public class EllipseConverterTests
RadiusRatio = 0.28,
StartParameter = 0.017,
EndParameter = 1.571,
Normal = new CSMath.XYZ(0, 0, 1)
Normal = new CSMath.XYZ(0, 0, 1),
};
var flipped = new ACadSharp.Entities.Ellipse
@@ -289,7 +338,7 @@ public class EllipseConverterTests
RadiusRatio = 0.28,
StartParameter = 0.017,
EndParameter = 1.571,
Normal = new CSMath.XYZ(0, 0, -1)
Normal = new CSMath.XYZ(0, 0, -1),
};
var normalArcs = normal.ToOpenNest();
@@ -305,13 +354,25 @@ public class EllipseConverterTests
var normalStart = GetArcStart(normalFirst);
var flippedStart = GetArcStart(flippedFirst);
Assert.True(normalStart.X < 0, $"Normal ellipse start X should be negative, got {normalStart.X}");
Assert.True(flippedStart.X > 0, $"Flipped ellipse should bulge right, got {flippedStart.X}");
Assert.True(
normalStart.X < 0,
$"Normal ellipse start X should be negative, got {normalStart.X}"
);
Assert.True(
flippedStart.X > 0,
$"Flipped ellipse should bulge right, got {flippedStart.X}"
);
var normalBbox = GetBoundingBox(normalArcs.Cast<Arc>());
var flippedBbox = GetBoundingBox(flippedArcs.Cast<Arc>());
Assert.True(flippedBbox.minX > 0, $"Flipped ellipse should stay on positive X side, minX={flippedBbox.minX}");
Assert.True(normalBbox.maxX < 0, $"Normal ellipse should stay on negative X side, maxX={normalBbox.maxX}");
Assert.True(
flippedBbox.minX > 0,
$"Flipped ellipse should stay on positive X side, minX={flippedBbox.minX}"
);
Assert.True(
normalBbox.maxX < 0,
$"Normal ellipse should stay on negative X side, maxX={normalBbox.maxX}"
);
}
private static (double minX, double maxX) GetBoundingBox(IEnumerable<Arc> arcs)
@@ -333,7 +394,8 @@ public class EllipseConverterTests
var angle = arc.IsReversed ? arc.EndAngle : arc.StartAngle;
return new Vector(
arc.Center.X + arc.Radius * System.Math.Cos(angle),
arc.Center.Y + arc.Radius * System.Math.Sin(angle));
arc.Center.Y + arc.Radius * System.Math.Sin(angle)
);
}
private static Vector GetArcEnd(Arc arc)
@@ -341,11 +403,18 @@ public class EllipseConverterTests
var angle = arc.IsReversed ? arc.StartAngle : arc.EndAngle;
return new Vector(
arc.Center.X + arc.Radius * System.Math.Cos(angle),
arc.Center.Y + arc.Radius * System.Math.Sin(angle));
arc.Center.Y + arc.Radius * System.Math.Sin(angle)
);
}
private static double MaxDeviationFromEllipse(Arc arc, Vector ellipseCenter,
double semiMajor, double semiMinor, double rotation, int samples)
private static double MaxDeviationFromEllipse(
Arc arc,
Vector ellipseCenter,
double semiMajor,
double semiMinor,
double rotation,
int samples
)
{
var maxDev = 0.0;
var sweep = arc.SweepAngle();
@@ -367,9 +436,19 @@ public class EllipseConverterTests
for (var j = 0; j <= 1000; j++)
{
var t = (double)j / 1000 * Angle.TwoPI;
var ep2 = EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t);
var ep2 = EllipseConverter.EvaluatePoint(
semiMajor,
semiMinor,
rotation,
ellipseCenter,
t
);
var dist = arcPoint.DistanceTo(ep2);
if (dist < minDist) { minDist = dist; bestT = t; }
if (dist < minDist)
{
minDist = dist;
bestT = t;
}
}
// Refine with local bisection around bestT
@@ -379,12 +458,40 @@ public class EllipseConverterTests
{
var t1 = lo + (hi - lo) / 3;
var t2 = lo + 2 * (hi - lo) / 3;
var d1 = arcPoint.DistanceTo(EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1));
var d2 = arcPoint.DistanceTo(EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t2));
if (d1 < d2) hi = t2; else lo = t1;
var d1 = arcPoint.DistanceTo(
EllipseConverter.EvaluatePoint(
semiMajor,
semiMinor,
rotation,
ellipseCenter,
t1
)
);
var d2 = arcPoint.DistanceTo(
EllipseConverter.EvaluatePoint(
semiMajor,
semiMinor,
rotation,
ellipseCenter,
t2
)
);
if (d1 < d2)
hi = t2;
else
lo = t1;
}
var bestDist = arcPoint.DistanceTo(EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, (lo + hi) / 2));
if (bestDist > maxDev) maxDev = bestDist;
var bestDist = arcPoint.DistanceTo(
EllipseConverter.EvaluatePoint(
semiMajor,
semiMinor,
rotation,
ellipseCenter,
(lo + hi) / 2
)
);
if (bestDist > maxDev)
maxDev = bestDist;
}
return maxDev;
@@ -1,7 +1,7 @@
using OpenNest.Geometry;
using OpenNest.IO;
using System.IO;
using System.Linq;
using OpenNest.Geometry;
using OpenNest.IO;
using Xunit;
namespace OpenNest.Tests.Geometry;
@@ -146,7 +146,8 @@ public class GeometrySimplifierTests
foreach (var shape in shapes)
{
var candidates = simplifier.Analyze(shape);
if (candidates.Count == 0) continue;
if (candidates.Count == 0)
continue;
var simplified = simplifier.Apply(shape, candidates);
@@ -160,20 +161,23 @@ public class GeometrySimplifierTests
{
Line l => l.EndPoint,
Arc a => a.EndPoint(),
_ => Vector.Invalid
_ => Vector.Invalid,
};
var nextStart = next switch
{
Line l => l.StartPoint,
Arc a => a.StartPoint(),
_ => Vector.Invalid
_ => Vector.Invalid,
};
if (!currentEnd.IsValid() || !nextStart.IsValid()) continue;
if (!currentEnd.IsValid() || !nextStart.IsValid())
continue;
var gap = currentEnd.DistanceTo(nextStart);
Assert.True(gap < 0.005,
$"Gap of {gap:F4} between entities {i} ({current.GetType().Name}) and {i + 1} ({next.GetType().Name})");
Assert.True(
gap < 0.005,
$"Gap of {gap:F4} between entities {i} ({current.GetType().Name}) and {i + 1} ({next.GetType().Name})"
);
}
}
}
+23 -12
View File
@@ -30,10 +30,13 @@ public class PolygonHelperTests
// OffsetSide.Left offsets outward or inward depending on winding,
// but either way the result must be a different size.
Assert.True(
System.Math.Abs(withSpacing.Polygon.BoundingBox.Width - noSpacing.Polygon.BoundingBox.Width) > 0.5,
$"Expected polygon width to differ by >0.5 with 1mm spacing. " +
$"No-spacing width: {noSpacing.Polygon.BoundingBox.Width:F3}, " +
$"With-spacing width: {withSpacing.Polygon.BoundingBox.Width:F3}");
System.Math.Abs(
withSpacing.Polygon.BoundingBox.Width - noSpacing.Polygon.BoundingBox.Width
) > 0.5,
$"Expected polygon width to differ by >0.5 with 1mm spacing. "
+ $"No-spacing width: {noSpacing.Polygon.BoundingBox.Width:F3}, "
+ $"With-spacing width: {withSpacing.Polygon.BoundingBox.Width:F3}"
);
}
[Fact]
@@ -47,10 +50,14 @@ public class PolygonHelperTests
noSpacing.Polygon.UpdateBounds();
withSpacing.Polygon.UpdateBounds();
Assert.True(withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width,
$"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}");
Assert.True(withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length,
$"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}");
Assert.True(
withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width,
$"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}"
);
Assert.True(
withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length,
$"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}"
);
}
[Fact]
@@ -71,10 +78,14 @@ public class PolygonHelperTests
noSpacing.Polygon.UpdateBounds();
withSpacing.Polygon.UpdateBounds();
Assert.True(withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width,
$"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}");
Assert.True(withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length,
$"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}");
Assert.True(
withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width,
$"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}"
);
Assert.True(
withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length,
$"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}"
);
}
[Fact]
+13 -3
View File
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.Tests.Geometry;
@@ -45,9 +45,19 @@ public class SpatialQueryTests
foreach (var e in entities)
{
if (e is Line line)
result.Add(new Line(line.pt1.X + dx, line.pt1.Y + dy, line.pt2.X + dx, line.pt2.Y + dy));
result.Add(
new Line(line.pt1.X + dx, line.pt1.Y + dy, line.pt2.X + dx, line.pt2.Y + dy)
);
else if (e is Arc arc)
result.Add(new Arc(arc.Center.X + dx, arc.Center.Y + dy, arc.Radius, arc.StartAngle, arc.EndAngle));
result.Add(
new Arc(
arc.Center.X + dx,
arc.Center.Y + dy,
arc.Radius,
arc.StartAngle,
arc.EndAngle
)
);
else if (e is Circle circle)
result.Add(new Circle(circle.Center.X + dx, circle.Center.Y + dy, circle.Radius));
}
@@ -63,7 +63,7 @@ public class SplineConverterTests
var points = new System.Collections.Generic.List<Vector>
{
new Vector(0, 0),
new Vector(10, 5)
new Vector(10, 5),
};
var result = SplineConverter.Convert(points, isClosed: false, tolerance: 0.001);
@@ -89,16 +89,18 @@ public class SplineConverterTests
var endPt = GetEndPoint(result[i]);
var startPt = GetStartPoint(result[i + 1]);
var gap = endPt.DistanceTo(startPt);
Assert.True(gap < 0.001,
$"Gap of {gap:F6} between entity {i} and {i + 1}");
Assert.True(gap < 0.001, $"Gap of {gap:F6} between entity {i} and {i + 1}");
}
}
[Fact]
public void Convert_EmptyPoints_ReturnsEmpty()
{
var result = SplineConverter.Convert(new System.Collections.Generic.List<Vector>(),
isClosed: false, tolerance: 0.001);
var result = SplineConverter.Convert(
new System.Collections.Generic.List<Vector>(),
isClosed: false,
tolerance: 0.001
);
Assert.Empty(result);
}
@@ -116,7 +118,7 @@ public class SplineConverterTests
{
Arc a => a.StartPoint(),
Line l => l.StartPoint,
_ => throw new System.Exception("Unexpected entity type")
_ => throw new System.Exception("Unexpected entity type"),
};
}
@@ -126,7 +128,7 @@ public class SplineConverterTests
{
Arc a => a.EndPoint(),
Line l => l.EndPoint,
_ => throw new System.Exception("Unexpected entity type")
_ => throw new System.Exception("Unexpected entity type"),
};
}
}
@@ -150,11 +150,7 @@ public class EnvelopeGuardTests
new[] { new Vector(0, 0), new Vector(0, -2) },
};
var opts = new GravographISWriterOptions
{
WorkEnvelopeXMm = 25.4,
WorkEnvelopeYMm = 25.4,
};
var opts = new GravographISWriterOptions { WorkEnvelopeXMm = 25.4, WorkEnvelopeYMm = 25.4 };
Assert.Throws<System.InvalidOperationException>(() =>
{
@@ -14,16 +14,16 @@ public class GravographISWriterTests
// those frozen deltas send the head to a fixed point regardless of the job. The
// writer now emits a job-specific leading DR travel from operator zero instead.
private const string PreambleHex =
"21 41 53 20 33 38 3b 01 90 01 f4 01 90 01 f4 01 90 01 f4 00 00 00 00 00 00 00 00 00 00 " +
"00 00 00 09 00 00 03 e8 05 06 00 00 00 00 00 00 ff fd 32 44 00 00 ff fd 4d 43 00 01 ff fd " +
"4f 55 ff fb ff fd 4f 55 ff fa ff fd 50 5a 00 00 ff fd 56 53 00 23 ff fd 56 5a 00 23 ff fd " +
"44 5a 01 fc";
"21 41 53 20 33 38 3b 01 90 01 f4 01 90 01 f4 01 90 01 f4 00 00 00 00 00 00 00 00 00 00 "
+ "00 00 00 09 00 00 03 e8 05 06 00 00 00 00 00 00 ff fd 32 44 00 00 ff fd 4d 43 00 01 ff fd "
+ "4f 55 ff fb ff fd 4f 55 ff fa ff fd 50 5a 00 00 ff fd 56 53 00 23 ff fd 56 5a 00 23 ff fd "
+ "44 5a 01 fc";
// Legacy 36-byte tail with lift, aux off, motor off, operator beep, job finish.
// Byte-exact capture tests disable dynamic return-to-origin to preserve this form.
private const string PostambleHex =
"ff fd 50 55 00 01 ff fd 4f 55 ff fa ff fd 4f 55 ff fb ff fd 4d 43 00 00 " +
"ff fd 4f 50 00 00 ff fd 4a 46 00 00";
"ff fd 50 55 00 01 ff fd 4f 55 ff fa ff fd 4f 55 ff fb ff fd 4d 43 00 00 "
+ "ff fd 4f 50 00 00 ff fd 4a 46 00 00";
[Fact]
public void TestA_SingleTwoInchVerticalLine_IsByteExact()
@@ -33,20 +33,22 @@ public class GravographISWriterTests
new[] { new Vector(1, 1), new Vector(1, 3) },
};
var writer = new GravographISWriter(new GravographISWriterOptions
{
DepthInches = 0.25,
FeedMmPerSec = 35,
EnvelopeGuardEnabled = false,
ReturnToOriginAtEnd = false,
});
var writer = new GravographISWriter(
new GravographISWriterOptions
{
DepthInches = 0.25,
FeedMmPerSec = 35,
EnvelopeGuardEnabled = false,
ReturnToOriginAtEnd = false,
}
);
using var ms = new MemoryStream();
writer.Write(polylines, ms);
const string GeomHex =
"ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " +
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20";
"ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 "
+ "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20";
var expected = HexToBytes(PreambleHex + " " + GeomHex + " " + PostambleHex);
Assert.Equal(expected, ms.ToArray());
@@ -63,26 +65,28 @@ public class GravographISWriterTests
new[] { new Vector(1, 5), new Vector(1, 7) },
};
var writer = new GravographISWriter(new GravographISWriterOptions
{
DepthInches = 0.25,
FeedMmPerSec = 35,
EnvelopeGuardEnabled = false,
ReturnToOriginAtEnd = false,
});
var writer = new GravographISWriter(
new GravographISWriterOptions
{
DepthInches = 0.25,
FeedMmPerSec = 35,
EnvelopeGuardEnabled = false,
ReturnToOriginAtEnd = false,
}
);
using var ms = new MemoryStream();
writer.Write(polylines, ms);
const string GeomHex =
"ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " +
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " +
"ff fd 50 55 00 00 35 40 00 b4 17 d0 0f e0 " +
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " +
"ff fd 50 55 00 00 40 00 00 b4 00 00 f0 20 " +
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " +
"ff fd 50 55 00 00 35 40 00 b4 e8 30 0f e0 " +
"ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20";
"ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 "
+ "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 "
+ "ff fd 50 55 00 00 35 40 00 b4 17 d0 0f e0 "
+ "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 "
+ "ff fd 50 55 00 00 40 00 00 b4 00 00 f0 20 "
+ "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 "
+ "ff fd 50 55 00 00 35 40 00 b4 e8 30 0f e0 "
+ "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20";
var expected = HexToBytes(PreambleHex + " " + GeomHex + " " + PostambleHex);
Assert.Equal(expected, ms.ToArray());
@@ -97,7 +101,9 @@ public class GravographISWriterTests
};
using var ms = new MemoryStream();
new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false }).Write(polylines, ms);
new GravographISWriter(
new GravographISWriterOptions { EnvelopeGuardEnabled = false }
).Write(polylines, ms);
var bytes = ms.ToArray();
// First command after the 93-byte preamble must be DR to the first point,
@@ -122,7 +128,9 @@ public class GravographISWriterTests
};
using var ms = new MemoryStream();
new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false }).Write(polylines, ms);
new GravographISWriter(
new GravographISWriterOptions { EnvelopeGuardEnabled = false }
).Write(polylines, ms);
var bytes = ms.ToArray();
Assert.Equal((byte)'D', bytes[95]);
@@ -140,11 +148,13 @@ public class GravographISWriterTests
};
using var ms = new MemoryStream();
new GravographISWriter(new GravographISWriterOptions
{
DepthInches = 0.125, // 254 steps = 0x00FE
FeedMmPerSec = 50, // 0x0032
}).Write(polylines, ms);
new GravographISWriter(
new GravographISWriterOptions
{
DepthInches = 0.125, // 254 steps = 0x00FE
FeedMmPerSec = 50, // 0x0032
}
).Write(polylines, ms);
var bytes = ms.ToArray();
AssertOperand(bytes, (byte)'V', (byte)'S', 0x00, 0x32);
@@ -182,7 +192,12 @@ public class GravographISWriterTests
{
for (var i = 0; i < bytes.Length - 5; i++)
{
if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD && bytes[i + 2] == c0 && bytes[i + 3] == c1)
if (
bytes[i] == 0xFF
&& bytes[i + 1] == 0xFD
&& bytes[i + 2] == c0
&& bytes[i + 3] == c1
)
{
Assert.Equal(hi, bytes[i + 4]);
Assert.Equal(lo, bytes[i + 5]);
@@ -196,9 +211,14 @@ public class GravographISWriterTests
{
for (var i = bytes.Length - 6; i >= 0; i--)
{
if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD &&
bytes[i + 2] == c0 && bytes[i + 3] == c1 &&
bytes[i + 4] == hi && bytes[i + 5] == lo)
if (
bytes[i] == 0xFF
&& bytes[i + 1] == 0xFD
&& bytes[i + 2] == c0
&& bytes[i + 3] == c1
&& bytes[i + 4] == hi
&& bytes[i + 5] == lo
)
{
return i;
}
@@ -214,7 +234,9 @@ public class GravographISWriterTests
internal static byte[] HexToBytes(string hex)
{
var clean = hex.Replace(" ", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty);
var clean = hex.Replace(" ", string.Empty)
.Replace("\n", string.Empty)
.Replace("\r", string.Empty);
var bytes = new byte[clean.Length / 2];
for (var i = 0; i < bytes.Length; i++)
bytes[i] = System.Convert.ToByte(clean.Substring(i * 2, 2), 16);
@@ -108,8 +108,10 @@ public class PolylinePrePassTests
Assert.Equal(3, reordered.Count);
var travelBefore = TotalPenUpTravel(inputs);
var travelAfter = TotalPenUpTravel(reordered);
Assert.True(travelAfter < travelBefore,
$"Expected reorder to reduce pen-up travel; before={travelBefore}, after={travelAfter}");
Assert.True(
travelAfter < travelBefore,
$"Expected reorder to reduce pen-up travel; before={travelBefore}, after={travelAfter}"
);
}
[Fact]
@@ -150,7 +152,8 @@ public class PolylinePrePassTests
Vector? last = null;
foreach (var p in polylines)
{
if (p == null || p.Count < 2) continue;
if (p == null || p.Count < 2)
continue;
if (last.HasValue)
{
var dx = p[0].X - last.Value.X;
+37 -15
View File
@@ -7,8 +7,7 @@ namespace OpenNest.Tests.IO
{
public class CadImporterTests
{
private static string TestDxf =>
Path.Combine("Bending", "TestData", "4526 A14 PT11.dxf");
private static string TestDxf => Path.Combine("Bending", "TestData", "4526 A14 PT11.dxf");
[Fact]
public void Import_LoadsEntitiesAndDetectsBends()
@@ -45,8 +44,10 @@ namespace OpenNest.Tests.IO
// Exercises the named-detector branch: when BendDetectorName doesn't
// match any registered detector, bends should be an empty list
// (not a crash, and no fall-through to auto-detect).
var result = CadImporter.Import(TestDxf,
new CadImportOptions { BendDetectorName = "__nonexistent__" });
var result = CadImporter.Import(
TestDxf,
new CadImportOptions { BendDetectorName = "__nonexistent__" }
);
Assert.Empty(result.Bends);
}
@@ -62,7 +63,8 @@ namespace OpenNest.Tests.IO
result.Bends,
quantity: 5,
customer: "ACME",
editedProgram: null);
editedProgram: null
);
Assert.NotNull(drawing);
Assert.Equal("4526 A14 PT11", drawing.Name);
@@ -80,8 +82,14 @@ namespace OpenNest.Tests.IO
{
var result = CadImporter.Import(TestDxf);
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
quantity: 1, customer: null, editedProgram: null);
var drawing = CadImporter.BuildDrawing(
result,
result.Entities,
result.Bends,
quantity: 1,
customer: null,
editedProgram: null
);
Assert.NotNull(drawing.Source.Offset);
// After offset extraction, the program's first rapid must start at origin.
@@ -95,15 +103,21 @@ namespace OpenNest.Tests.IO
{
var result = CadImporter.Import(TestDxf);
// Suppress the first non-bend-source entity
var bendSources = result.Bends
.Where(b => b.SourceEntity != null)
var bendSources = result
.Bends.Where(b => b.SourceEntity != null)
.Select(b => b.SourceEntity)
.ToHashSet();
var hidden = result.Entities.First(e => !bendSources.Contains(e));
hidden.IsVisible = false;
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
quantity: 1, customer: null, editedProgram: null);
var drawing = CadImporter.BuildDrawing(
result,
result.Entities,
result.Bends,
quantity: 1,
customer: null,
editedProgram: null
);
Assert.Contains(hidden.Id, drawing.SuppressedEntityIds);
}
@@ -115,8 +129,14 @@ namespace OpenNest.Tests.IO
var edited = new OpenNest.CNC.Program();
edited.MoveTo(new OpenNest.Geometry.Vector(0, 0));
var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends,
quantity: 1, customer: null, editedProgram: edited);
var drawing = CadImporter.BuildDrawing(
result,
result.Entities,
result.Bends,
quantity: 1,
customer: null,
editedProgram: edited
);
Assert.Same(edited, drawing.Program);
}
@@ -124,8 +144,10 @@ namespace OpenNest.Tests.IO
[Fact]
public void ImportDrawing_ComposesImportAndBuild()
{
var drawing = CadImporter.ImportDrawing(TestDxf,
new CadImportOptions { Quantity = 3, Customer = "ACME" });
var drawing = CadImporter.ImportDrawing(
TestDxf,
new CadImportOptions { Quantity = 3, Customer = "ACME" }
);
Assert.NotNull(drawing);
Assert.Equal("4526 A14 PT11", drawing.Name);
+44 -20
View File
@@ -51,7 +51,10 @@ public class ChrFontTests
Assert.NotNull(glyph);
var entities = glyph.ToEntities(1.0, 0, 0);
Assert.True(entities.Count >= 2, $"Expected at least 2 entities for 'L', got {entities.Count}");
Assert.True(
entities.Count >= 2,
$"Expected at least 2 entities for 'L', got {entities.Count}"
);
Assert.All(entities, e => Assert.Equal(EntityType.Line, e.Type));
}
@@ -99,8 +102,10 @@ public class ChrFontTests
var abBox = abEntities.GetBoundingBox();
var aBox = aEntities.GetBoundingBox();
Assert.True(abBox.Length > aBox.Length * 1.5,
$"AB width ({abBox.Length:F1}) should be significantly wider than A width ({aBox.Length:F1})");
Assert.True(
abBox.Length > aBox.Length * 1.5,
$"AB width ({abBox.Length:F1}) should be significantly wider than A width ({aBox.Length:F1})"
);
}
[SkippableFact]
@@ -129,18 +134,28 @@ public class ChrFontTests
var tolerance = 0.5;
Assert.True(System.Math.Abs(box.Left - refLeft) < tolerance,
$"Left: ours={box.Left:F2}, ref={refLeft:F2}, diff={System.Math.Abs(box.Left - refLeft):F2}");
Assert.True(System.Math.Abs(box.Right - refRight) < tolerance,
$"Right: ours={box.Right:F2}, ref={refRight:F2}, diff={System.Math.Abs(box.Right - refRight):F2}");
Assert.True(System.Math.Abs(box.Bottom - refBottom) < tolerance,
$"Bottom: ours={box.Bottom:F2}, ref={refBottom:F2}, diff={System.Math.Abs(box.Bottom - refBottom):F2}");
Assert.True(System.Math.Abs(box.Top - refTop) < tolerance,
$"Top: ours={box.Top:F2}, ref={refTop:F2}, diff={System.Math.Abs(box.Top - refTop):F2}");
Assert.True(
System.Math.Abs(box.Left - refLeft) < tolerance,
$"Left: ours={box.Left:F2}, ref={refLeft:F2}, diff={System.Math.Abs(box.Left - refLeft):F2}"
);
Assert.True(
System.Math.Abs(box.Right - refRight) < tolerance,
$"Right: ours={box.Right:F2}, ref={refRight:F2}, diff={System.Math.Abs(box.Right - refRight):F2}"
);
Assert.True(
System.Math.Abs(box.Bottom - refBottom) < tolerance,
$"Bottom: ours={box.Bottom:F2}, ref={refBottom:F2}, diff={System.Math.Abs(box.Bottom - refBottom):F2}"
);
Assert.True(
System.Math.Abs(box.Top - refTop) < tolerance,
$"Top: ours={box.Top:F2}, ref={refTop:F2}, diff={System.Math.Abs(box.Top - refTop):F2}"
);
var actualCapHeight = box.Top - box.Bottom;
Assert.True(System.Math.Abs(actualCapHeight - height) < 0.5,
$"Cap height: ours={actualCapHeight:F2}, expected={height:F2}");
Assert.True(
System.Math.Abs(actualCapHeight - height) < 0.5,
$"Cap height: ours={actualCapHeight:F2}, expected={height:F2}"
);
}
[SkippableFact]
@@ -152,10 +167,14 @@ public class ChrFontTests
var entities = font.RenderText("Text", height, Vector.Zero);
var box = entities.GetBoundingBox();
Assert.True(measuredWidth >= box.Length,
$"Measured={measuredWidth:F2} should be >= rendered={box.Length:F2}");
Assert.True(measuredWidth - box.Length < 2.0,
$"Measured={measuredWidth:F2}, rendered={box.Length:F2}, diff={measuredWidth - box.Length:F2}");
Assert.True(
measuredWidth >= box.Length,
$"Measured={measuredWidth:F2} should be >= rendered={box.Length:F2}"
);
Assert.True(
measuredWidth - box.Length < 2.0,
$"Measured={measuredWidth:F2}, rendered={box.Length:F2}, diff={measuredWidth - box.Length:F2}"
);
}
[SkippableFact]
@@ -171,10 +190,15 @@ public class ChrFontTests
Assert.True(lines.Count >= 10, $"Expected at least 10 entities for 't', got {lines.Count}");
var curveLines = lines.Skip(1).Take(lines.Count - 3).ToList();
Assert.True(curveLines.Count >= 14, $"Expected at least 14 curve segments, got {curveLines.Count}");
Assert.True(
curveLines.Count >= 14,
$"Expected at least 14 curve segments, got {curveLines.Count}"
);
var lastCurve = curveLines[^1];
Assert.True(lastCurve.EndPoint.X > curveLines[0].StartPoint.X,
$"Curve should end to the right of where it starts: start X={curveLines[0].StartPoint.X:F1}, end X={lastCurve.EndPoint.X:F1}");
Assert.True(
lastCurve.EndPoint.X > curveLines[0].StartPoint.X,
$"Curve should end to the right of where it starts: start X={curveLines[0].StartPoint.X:F1}, end X={lastCurve.EndPoint.X:F1}"
);
}
}
+18 -12
View File
@@ -22,12 +22,10 @@ public class DxfRoundtripTests
return reimported;
}
private static List<T> FilterByLayer<T>(List<Entity> entities, string layerName) where T : Entity
private static List<T> FilterByLayer<T>(List<Entity> entities, string layerName)
where T : Entity
{
return entities
.Where(e => e is T && e.Layer?.Name == layerName)
.Cast<T>()
.ToList();
return entities.Where(e => e is T && e.Layer?.Name == layerName).Cast<T>().ToList();
}
[Fact]
@@ -38,7 +36,7 @@ public class DxfRoundtripTests
{
new Line(0, 0, 10, 0),
new Line(10, 0, 5, 8),
new Line(5, 8, 0, 0)
new Line(5, 8, 0, 0),
};
var reimported = ExportAndReimport(original);
@@ -97,7 +95,7 @@ public class DxfRoundtripTests
new Line(0, 0, 10, 0),
new Line(10, 0, 10, 5),
new Circle(20, 20, 3),
new Arc(15, 15, 5, 0.0, System.Math.PI)
new Arc(15, 15, 5, 0.0, System.Math.PI),
};
var reimported = ExportAndReimport(original);
@@ -120,17 +118,25 @@ public class DxfRoundtripTests
new Line(0, 0, 20, 0),
new Line(20, 0, 20, 10),
new Line(20, 10, 0, 10),
new Line(0, 10, 0, 0)
new Line(0, 10, 0, 0),
};
var reimported = ExportAndReimport(original);
var cutLines = FilterByLayer<Line>(reimported, "Cut");
// Verify bounding box is preserved regardless of line order
var origMinX = original.Cast<Line>().Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X));
var origMaxX = original.Cast<Line>().Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X));
var origMinY = original.Cast<Line>().Min(l => System.Math.Min(l.StartPoint.Y, l.EndPoint.Y));
var origMaxY = original.Cast<Line>().Max(l => System.Math.Max(l.StartPoint.Y, l.EndPoint.Y));
var origMinX = original
.Cast<Line>()
.Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X));
var origMaxX = original
.Cast<Line>()
.Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X));
var origMinY = original
.Cast<Line>()
.Min(l => System.Math.Min(l.StartPoint.Y, l.EndPoint.Y));
var origMaxY = original
.Cast<Line>()
.Max(l => System.Math.Max(l.StartPoint.Y, l.EndPoint.Y));
var rtMinX = cutLines.Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X));
var rtMaxX = cutLines.Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X));
+23 -19
View File
@@ -1,7 +1,7 @@
using System.Linq;
using OpenNest.Bending;
using OpenNest.Geometry;
using OpenNest.IO;
using System.Linq;
namespace OpenNest.Tests.IO;
@@ -11,24 +11,28 @@ public class NestBendSerializationTests
public void Bends_SurviveNestRoundtrip()
{
var drawing = TestHelpers.MakeSquareDrawing();
drawing.Bends.Add(new Bend
{
StartPoint = new Vector(0, 5),
EndPoint = new Vector(10, 5),
Direction = BendDirection.Up,
Angle = 90,
Radius = 0.06,
NoteText = "UP 90° R0.06"
});
drawing.Bends.Add(new Bend
{
StartPoint = new Vector(0, 3),
EndPoint = new Vector(10, 3),
Direction = BendDirection.Down,
Angle = 45.5,
Radius = 0.125,
NoteText = "DOWN 45.5° R0.125"
});
drawing.Bends.Add(
new Bend
{
StartPoint = new Vector(0, 5),
EndPoint = new Vector(10, 5),
Direction = BendDirection.Up,
Angle = 90,
Radius = 0.06,
NoteText = "UP 90° R0.06",
}
);
drawing.Bends.Add(
new Bend
{
StartPoint = new Vector(0, 3),
EndPoint = new Vector(10, 3),
Direction = BendDirection.Down,
Angle = 45.5,
Radius = 0.125,
NoteText = "DOWN 45.5° R0.125",
}
);
var nest = new Nest();
nest.Drawings.Add(drawing);
+4 -5
View File
@@ -13,7 +13,8 @@ public class NestWriterVariableTests
public void RoundTrip_VariableDefinitions_Preserved()
{
var nest = CreateNestWithVariableProgram(
"width = 48.0 global\ndiameter = 0.3\nG90\nG01X$widthY$diameter");
"width = 48.0 global\ndiameter = 0.3\nG90\nG01X$widthY$diameter"
);
var loaded = RoundTrip(nest);
var pgm = loaded.Drawings.First().Program;
@@ -28,8 +29,7 @@ public class NestWriterVariableTests
[Fact]
public void RoundTrip_VariableRefs_Preserved()
{
var nest = CreateNestWithVariableProgram(
"width = 48.0\nG90\nG01X$widthY0");
var nest = CreateNestWithVariableProgram("width = 48.0\nG90\nG01X$widthY0");
var loaded = RoundTrip(nest);
var pgm = loaded.Drawings.First().Program;
@@ -43,8 +43,7 @@ public class NestWriterVariableTests
[Fact]
public void RoundTrip_InlineFlag_Preserved()
{
var nest = CreateNestWithVariableProgram(
"kerf = 0.06 inline\nG90\nG01X1Y0");
var nest = CreateNestWithVariableProgram("kerf = 0.06 inline\nG90\nG01X1Y0");
var loaded = RoundTrip(nest);
var pgm = loaded.Drawings.First().Program;
@@ -54,7 +54,14 @@ public class SubProgramSerializationTests
var pgm = new Program(Mode.Absolute);
pgm.SubPrograms[42] = sub;
pgm.Codes.Add(new SubProgramCall { Id = 42, Program = sub, Offset = new Vector(5, 5) });
pgm.Codes.Add(
new SubProgramCall
{
Id = 42,
Program = sub,
Offset = new Vector(5, 5),
}
);
// Add perimeter so the drawing has non-zero geometry
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(10, 0));
@@ -87,7 +87,7 @@ public class ExpressionEvaluatorTests
{
var vars = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
{ "Diameter", 0.3 }
{ "Diameter", 0.3 },
};
Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter", vars));
}
@@ -95,8 +95,7 @@ public class ExpressionEvaluatorTests
[Fact]
public void Evaluate_UndefinedVariable_Throws()
{
Assert.Throws<KeyNotFoundException>(() =>
ExpressionEvaluator.Evaluate("$missing", Empty));
Assert.Throws<KeyNotFoundException>(() => ExpressionEvaluator.Evaluate("$missing", Empty));
}
[Fact]
@@ -31,7 +31,7 @@ public class PlateSnapToStandardSizeTests
// 10x20 is well below 48x48 MinSheet -> snap to integer increment.
Assert.Null(result.MatchedLabel);
Assert.Equal(10, plate.Size.Length); // X axis
Assert.Equal(20, plate.Size.Width); // Y axis
Assert.Equal(20, plate.Size.Width); // Y axis
}
[Fact]
@@ -58,7 +58,7 @@ public class PlateSnapToStandardSizeTests
Assert.Equal("48x96", result.MatchedLabel);
Assert.Equal(96, plate.Size.Length); // X axis = long
Assert.Equal(48, plate.Size.Width); // Y axis = short
Assert.Equal(48, plate.Size.Width); // Y axis = short
}
[Fact]
@@ -72,7 +72,7 @@ public class PlateSnapToStandardSizeTests
Assert.Equal("48x96", result.MatchedLabel);
Assert.Equal(48, plate.Size.Length); // X axis = short
Assert.Equal(96, plate.Size.Width); // Y axis = long
Assert.Equal(96, plate.Size.Width); // Y axis = long
}
[Fact]
@@ -105,8 +105,8 @@ public class PlateSnapToStandardSizeTests
{
var plate = new Plate(200, 200);
plate.Parts.Add(MakeRectPart(0, 0, 30, 40));
plate.Parts.Add(MakeRectPart(30, 0, 30, 40)); // combined X-extent = 60
plate.Parts.Add(MakeRectPart(0, 40, 60, 60)); // combined extent = 60 x 100
plate.Parts.Add(MakeRectPart(30, 0, 30, 40)); // combined X-extent = 60
plate.Parts.Add(MakeRectPart(0, 40, 60, 60)); // combined extent = 60 x 100
var result = plate.SnapToStandardSize();
@@ -27,8 +27,10 @@ public class DirectRapidPlannerTests
cutArea.Entities.Add(new Line(new Vector(60, 0), new Vector(50, 0)));
var result = planner.Plan(
new Vector(0, 0), new Vector(10, 10),
new List<Shape> { cutArea });
new Vector(0, 0),
new Vector(10, 10),
new List<Shape> { cutArea }
);
Assert.False(result.HeadUp);
}
@@ -45,8 +47,10 @@ public class DirectRapidPlannerTests
cutArea.Entities.Add(new Line(new Vector(6, 0), new Vector(5, 0)));
var result = planner.Plan(
new Vector(0, 10), new Vector(10, 10),
new List<Shape> { cutArea });
new Vector(0, 10),
new Vector(10, 10),
new List<Shape> { cutArea }
);
Assert.True(result.HeadUp);
Assert.Empty(result.Waypoints);
@@ -24,7 +24,7 @@ public class AdvancedSequencerTests
{
Method = SequenceMethod.Advanced,
MinDistanceBetweenRowsColumns = 5.0,
AlternateRowsColumns = false
AlternateRowsColumns = false,
};
var sequencer = new AdvancedSequencer(parameters);
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
@@ -52,7 +52,7 @@ public class AdvancedSequencerTests
{
Method = SequenceMethod.Advanced,
MinDistanceBetweenRowsColumns = 5.0,
AlternateRowsColumns = true
AlternateRowsColumns = true,
};
var sequencer = new AdvancedSequencer(parameters);
var result = sequencer.Sequence(plate.Parts.ToList(), plate);
@@ -5,6 +5,7 @@ namespace OpenNest.Tests.Sequencing;
public class DirectionalSequencerTests
{
private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y);
private static Plate MakePlate(params Part[] parts) => TestHelpers.MakePlate(60, 120, parts);
[Fact]
+7 -1
View File
@@ -30,7 +30,13 @@ public class LShapeTests
[Fact]
public void GetDrawing_CustomLegDimensions()
{
var shape = new LShape { Width = 10, Height = 20, LegWidth = 3, LegHeight = 5 };
var shape = new LShape
{
Width = 10,
Height = 20,
LegWidth = 3,
LegHeight = 5,
};
var drawing = shape.GetDrawing();
// Area = Width*Height - (Width - LegWidth) * (Height - LegHeight)
+2 -6
View File
@@ -31,9 +31,7 @@ public class NgonShapeTests
var shape = new NgonShape { Sides = sides, Width = 20 };
var drawing = shape.GetDrawing();
var moves = drawing.Program.Codes
.OfType<OpenNest.CNC.LinearMove>()
.Count();
var moves = drawing.Program.Codes.OfType<OpenNest.CNC.LinearMove>().Count();
Assert.Equal(sides, moves);
}
@@ -43,9 +41,7 @@ public class NgonShapeTests
var shape = new NgonShape { Sides = 2, Width = 20 };
var drawing = shape.GetDrawing();
var moves = drawing.Program.Codes
.OfType<OpenNest.CNC.LinearMove>()
.Count();
var moves = drawing.Program.Codes.OfType<OpenNest.CNC.LinearMove>().Count();
Assert.Equal(3, moves);
}
}
+33 -31
View File
@@ -14,7 +14,7 @@ public class PipeFlangeShapeTests
OD = 10,
HoleDiameter = 1,
HolePatternDiameter = 7,
HoleCount = 4
HoleCount = 4,
};
var drawing = shape.GetDrawing();
@@ -32,7 +32,7 @@ public class PipeFlangeShapeTests
HoleDiameter = 1,
HolePatternDiameter = 7,
HoleCount = 4,
Blind = true
Blind = true,
};
var drawing = shape.GetDrawing();
@@ -48,7 +48,7 @@ public class PipeFlangeShapeTests
OD = 10,
HoleDiameter = 1,
HolePatternDiameter = 7,
HoleCount = 4
HoleCount = 4,
};
var drawing = shape.GetDrawing();
@@ -64,9 +64,9 @@ public class PipeFlangeShapeTests
HoleDiameter = 1,
HolePatternDiameter = 7,
HoleCount = 4,
PipeSize = "2", // OD = 2.375
PipeSize = "2", // OD = 2.375
PipeClearance = 0.125,
Blind = false
Blind = false,
};
var drawing = shape.GetDrawing();
@@ -87,7 +87,7 @@ public class PipeFlangeShapeTests
HoleCount = 4,
PipeSize = "2",
PipeClearance = 0.125,
Blind = true
Blind = true,
};
var drawing = shape.GetDrawing();
@@ -107,7 +107,7 @@ public class PipeFlangeShapeTests
HoleCount = 4,
PipeSize = "not-a-real-pipe",
PipeClearance = 0.125,
Blind = false
Blind = false,
};
var drawing = shape.GetDrawing();
@@ -128,7 +128,7 @@ public class PipeFlangeShapeTests
HolePatternDiameter = 7,
HoleCount = 4,
PipeSize = pipeSize,
PipeClearance = 0.125
PipeClearance = 0.125,
};
var drawing = shape.GetDrawing();
@@ -140,27 +140,27 @@ public class PipeFlangeShapeTests
public void LoadFromJson_ProducesCorrectDrawing()
{
var json = """
[
{
"Name": "2in-150#",
"PipeSize": "2",
"PipeClearance": 0.0625,
"OD": 6.0,
"HoleDiameter": 0.75,
"HolePatternDiameter": 4.75,
"HoleCount": 4
},
{
"Name": "2in-300#",
"PipeSize": "2",
"PipeClearance": 0.0625,
"OD": 6.5,
"HoleDiameter": 0.75,
"HolePatternDiameter": 5.0,
"HoleCount": 8
}
]
""";
[
{
"Name": "2in-150#",
"PipeSize": "2",
"PipeClearance": 0.0625,
"OD": 6.0,
"HoleDiameter": 0.75,
"HolePatternDiameter": 4.75,
"HoleCount": 4
},
{
"Name": "2in-300#",
"PipeSize": "2",
"PipeClearance": 0.0625,
"OD": 6.5,
"HoleDiameter": 0.75,
"HolePatternDiameter": 5.0,
"HoleCount": 8
}
]
""";
var tempFile = Path.GetTempFileName();
try
@@ -208,8 +208,10 @@ public class PipeFlangeShapeTests
foreach (var f in flanges)
{
Assert.False(string.IsNullOrWhiteSpace(f.PipeSize));
Assert.True(PipeSizes.TryGetOD(f.PipeSize, out _),
$"Unknown PipeSize '{f.PipeSize}' in entry '{f.Name}'");
Assert.True(
PipeSizes.TryGetOD(f.PipeSize, out _),
$"Unknown PipeSize '{f.PipeSize}' in entry '{f.Name}'"
);
Assert.Equal(0.0625, f.PipeClearance, 0.0001);
}
}
+10 -13
View File
@@ -43,12 +43,12 @@ public class PlateSizesTests
}
[Theory]
[InlineData(40, 40, true)] // small - fits trivially
[InlineData(48, 96, true)] // exact
[InlineData(96, 48, true)] // rotated exact
[InlineData(90, 40, true)] // rotated
[InlineData(49, 97, false)] // just over in both dims
[InlineData(50, 50, false)] // too wide in both orientations
[InlineData(40, 40, true)] // small - fits trivially
[InlineData(48, 96, true)] // exact
[InlineData(96, 48, true)] // rotated exact
[InlineData(90, 40, true)] // rotated
[InlineData(49, 97, false)] // just over in both dims
[InlineData(50, 50, false)] // too wide in both orientations
public void Entry_Fits_RespectsRotation(double w, double h, bool expected)
{
var entry = new PlateSizes.Entry("48x96", 48, 96);
@@ -233,11 +233,7 @@ public class PlateSizesTests
public void Recommend_BoxEnumerable_CombinesIntoEnvelope()
{
// Two boxes that together span 0..40 x 0..90 -> fits 48x96
var boxes = new[]
{
new Box(0, 0, 40, 50),
new Box(0, 40, 30, 50),
};
var boxes = new[] { new Box(0, 0, 40, 50), new Box(0, 40, 30, 50) };
var result = PlateSizes.Recommend(boxes);
@@ -247,8 +243,9 @@ public class PlateSizesTests
[Fact]
public void Recommend_BoxEnumerable_Empty_Throws()
{
Assert.Throws<System.ArgumentException>(
() => PlateSizes.Recommend(System.Array.Empty<Box>()));
Assert.Throws<System.ArgumentException>(() =>
PlateSizes.Recommend(System.Array.Empty<Box>())
);
}
[Fact]
+6 -1
View File
@@ -27,7 +27,12 @@ public class RectangleShapeTests
[Fact]
public void GetDrawing_CustomName_IsUsed()
{
var shape = new RectangleShape { Name = "Plate1", Length = 10, Width = 5 };
var shape = new RectangleShape
{
Name = "Plate1",
Length = 10,
Width = 5,
};
var drawing = shape.GetDrawing();
Assert.Equal("Plate1", drawing.Name);
@@ -7,7 +7,12 @@ public class RoundedRectangleShapeTests
[Fact]
public void GetDrawing_BoundingBoxMatchesDimensions()
{
var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 2 };
var shape = new RoundedRectangleShape
{
Length = 20,
Width = 10,
Radius = 2,
};
var drawing = shape.GetDrawing();
var bbox = drawing.Program.BoundingBox();
@@ -18,7 +23,12 @@ public class RoundedRectangleShapeTests
[Fact]
public void GetDrawing_AreaIsLessThanFullRectangle()
{
var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 2 };
var shape = new RoundedRectangleShape
{
Length = 20,
Width = 10,
Radius = 2,
};
var drawing = shape.GetDrawing();
// Area should be less than 20*10=200 because corners are rounded
@@ -30,7 +40,12 @@ public class RoundedRectangleShapeTests
[Fact]
public void GetDrawing_ZeroRadius_MatchesRectangleArea()
{
var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 0 };
var shape = new RoundedRectangleShape
{
Length = 20,
Width = 10,
Radius = 0,
};
var drawing = shape.GetDrawing();
Assert.Equal(200, drawing.Area, 0.5);
+7 -1
View File
@@ -30,7 +30,13 @@ public class TShapeTests
[Fact]
public void GetDrawing_CustomStemAndBarDimensions()
{
var shape = new TShape { Width = 12, Height = 18, StemWidth = 6, BarHeight = 4 };
var shape = new TShape
{
Width = 12,
Height = 18,
StemWidth = 6,
BarHeight = 4,
};
var drawing = shape.GetDrawing();
// Area = Width * BarHeight + StemWidth * (Height - BarHeight)
+12 -2
View File
@@ -7,7 +7,12 @@ public class TrapezoidShapeTests
[Fact]
public void GetDrawing_BoundingBoxMatchesDimensions()
{
var shape = new TrapezoidShape { BottomWidth = 20, TopWidth = 10, Height = 8 };
var shape = new TrapezoidShape
{
BottomWidth = 20,
TopWidth = 10,
Height = 8,
};
var drawing = shape.GetDrawing();
var bbox = drawing.Program.BoundingBox();
@@ -18,7 +23,12 @@ public class TrapezoidShapeTests
[Fact]
public void GetDrawing_AreaIsCorrect()
{
var shape = new TrapezoidShape { BottomWidth = 20, TopWidth = 10, Height = 8 };
var shape = new TrapezoidShape
{
BottomWidth = 20,
TopWidth = 10,
Height = 8,
};
var drawing = shape.GetDrawing();
// Area = (top + bottom) / 2 * height = (10 + 20) / 2 * 8 = 120
+210 -80
View File
@@ -9,7 +9,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Rectangle_Vertical_ProducesTwoPieces()
{
var drawing = new RectangleShape { Name = "RECT", Length = 100, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "RECT",
Length = 100,
Width = 50,
}.GetDrawing();
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -27,7 +32,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Rectangle_Horizontal_ProducesTwoPieces()
{
var drawing = new RectangleShape { Name = "RECT", Length = 100, Width = 60 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "RECT",
Length = 100,
Width = 60,
}.GetDrawing();
var splitLines = new List<SplitLine> { new SplitLine(30.0, CutOffAxis.Horizontal) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -41,11 +51,16 @@ public class DrawingSplitterTests
[Fact]
public void Split_ThreePieces_NamesSequentially()
{
var drawing = new RectangleShape { Name = "PART", Length = 150, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "PART",
Length = 150,
Width = 50,
}.GetDrawing();
var splitLines = new List<SplitLine>
{
new SplitLine(50.0, CutOffAxis.Vertical),
new SplitLine(100.0, CutOffAxis.Vertical)
new SplitLine(100.0, CutOffAxis.Vertical),
};
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -60,28 +75,45 @@ public class DrawingSplitterTests
[Fact]
public void Split_CopiesDrawingProperties()
{
var drawing = new RectangleShape { Name = "PART", Length = 100, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "PART",
Length = 100,
Width = 50,
}.GetDrawing();
drawing.Color = System.Drawing.Color.Red;
drawing.Priority = 5;
var results = DrawingSplitter.Split(drawing,
var results = DrawingSplitter.Split(
drawing,
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
new SplitParameters());
new SplitParameters()
);
Assert.All(results, d =>
{
Assert.Equal(System.Drawing.Color.Red, d.Color);
Assert.Equal(5, d.Priority);
});
Assert.All(
results,
d =>
{
Assert.Equal(System.Drawing.Color.Red, d.Color);
Assert.Equal(5, d.Priority);
}
);
}
[Fact]
public void Split_PiecesNormalizedToOrigin()
{
var drawing = new RectangleShape { Name = "PART", Length = 100, Width = 50 }.GetDrawing();
var results = DrawingSplitter.Split(drawing,
var drawing = new RectangleShape
{
Name = "PART",
Length = 100,
Width = 50,
}.GetDrawing();
var results = DrawingSplitter.Split(
drawing,
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
new SplitParameters());
new SplitParameters()
);
// Each piece's program bounding box should start near (0,0)
foreach (var d in results)
@@ -101,14 +133,14 @@ public class DrawingSplitterTests
new Line(new Vector(0, 0), new Vector(100, 0)),
new Line(new Vector(100, 0), new Vector(100, 50)),
new Line(new Vector(100, 50), new Vector(0, 50)),
new Line(new Vector(0, 50), new Vector(0, 0))
new Line(new Vector(0, 50), new Vector(0, 0)),
};
var cutoutEntities = new List<Entity>
{
new Line(new Vector(20, 20), new Vector(30, 20)),
new Line(new Vector(30, 20), new Vector(30, 30)),
new Line(new Vector(30, 30), new Vector(20, 30)),
new Line(new Vector(20, 30), new Vector(20, 20))
new Line(new Vector(20, 30), new Vector(20, 20)),
};
var allEntities = new List<Entity>();
allEntities.AddRange(perimeterEntities);
@@ -118,24 +150,33 @@ public class DrawingSplitterTests
var drawing = new Drawing("HOLE", pgm);
// Split at X=50 — cutout is in the left half
var results = DrawingSplitter.Split(drawing,
var results = DrawingSplitter.Split(
drawing,
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
new SplitParameters());
new SplitParameters()
);
Assert.Equal(2, results.Count);
// Left piece should have smaller area (has the cutout)
Assert.True(results[0].Area < results[1].Area,
"Left piece should have less area due to cutout");
Assert.True(
results[0].Area < results[1].Area,
"Left piece should have less area due to cutout"
);
}
[Fact]
public void Split_GridSplit_ProducesFourPieces()
{
var drawing = new RectangleShape { Name = "GRID", Length = 100, Width = 100 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "GRID",
Length = 100,
Width = 100,
}.GetDrawing();
var splitLines = new List<SplitLine>
{
new SplitLine(50.0, CutOffAxis.Vertical),
new SplitLine(50.0, CutOffAxis.Horizontal)
new SplitLine(50.0, CutOffAxis.Horizontal),
};
var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters());
@@ -149,7 +190,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Square_Vertical_PieceWidthsSumToOriginal()
{
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "SQ",
Length = 100,
Width = 100,
}.GetDrawing();
var splitLines = new List<SplitLine> { new SplitLine(40.0, CutOffAxis.Vertical) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -171,7 +217,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Square_Horizontal_PieceHeightsSumToOriginal()
{
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "SQ",
Length = 100,
Width = 100,
}.GetDrawing();
var splitLines = new List<SplitLine> { new SplitLine(60.0, CutOffAxis.Horizontal) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -193,7 +244,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Square_Vertical_AreaPreserved()
{
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "SQ",
Length = 100,
Width = 100,
}.GetDrawing();
var originalArea = drawing.Area;
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -207,7 +263,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Square_Vertical_PiecesAreClosedPerimeters()
{
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "SQ",
Length = 100,
Width = 100,
}.GetDrawing();
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -215,17 +276,24 @@ public class DrawingSplitterTests
foreach (var piece in results)
{
var entities = ConvertProgram.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var entities = ConvertProgram
.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
Assert.True(entities.Count >= 4, $"{piece.Name} should have at least 4 entities for a rectangle");
Assert.True(
entities.Count >= 4,
$"{piece.Name} should have at least 4 entities for a rectangle"
);
// First entity start should connect to last entity end (closed shape)
var firstStart = GetStartPoint(entities[0]);
var lastEnd = GetEndPoint(entities[^1]);
var closingGap = firstStart.DistanceTo(lastEnd);
Assert.True(closingGap < 0.01,
$"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start");
Assert.True(
closingGap < 0.01,
$"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start"
);
// Consecutive entities should connect
for (var i = 0; i < entities.Count - 1; i++)
@@ -233,8 +301,10 @@ public class DrawingSplitterTests
var end = GetEndPoint(entities[i]);
var start = GetStartPoint(entities[i + 1]);
var gap = end.DistanceTo(start);
Assert.True(gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
Assert.True(
gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"
);
}
}
}
@@ -242,7 +312,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Square_Horizontal_PiecesAreClosedPerimeters()
{
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "SQ",
Length = 100,
Width = 100,
}.GetDrawing();
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Horizontal) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -250,24 +325,33 @@ public class DrawingSplitterTests
foreach (var piece in results)
{
var entities = ConvertProgram.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var entities = ConvertProgram
.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
Assert.True(entities.Count >= 4, $"{piece.Name} should have at least 4 entities for a rectangle");
Assert.True(
entities.Count >= 4,
$"{piece.Name} should have at least 4 entities for a rectangle"
);
var firstStart = GetStartPoint(entities[0]);
var lastEnd = GetEndPoint(entities[^1]);
var closingGap = firstStart.DistanceTo(lastEnd);
Assert.True(closingGap < 0.01,
$"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start");
Assert.True(
closingGap < 0.01,
$"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start"
);
for (var i = 0; i < entities.Count - 1; i++)
{
var end = GetEndPoint(entities[i]);
var start = GetStartPoint(entities[i + 1]);
var gap = end.DistanceTo(start);
Assert.True(gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
Assert.True(
gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"
);
}
}
}
@@ -275,7 +359,12 @@ public class DrawingSplitterTests
[Fact]
public void Split_Square_AsymmetricSplit_PieceDimensionsMatchSplitPosition()
{
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "SQ",
Length = 100,
Width = 100,
}.GetDrawing();
var splitLines = new List<SplitLine> { new SplitLine(30.0, CutOffAxis.Vertical) };
var parameters = new SplitParameters { Type = SplitType.Straight };
@@ -301,7 +390,7 @@ public class DrawingSplitterTests
new Line(new Vector(0, 0), new Vector(100, 0)),
new Line(new Vector(100, 0), new Vector(100, 50)),
new Line(new Vector(100, 50), new Vector(0, 50)),
new Line(new Vector(0, 50), new Vector(0, 0))
new Line(new Vector(0, 50), new Vector(0, 0)),
};
var hole = new Circle(new Vector(20, 25), 3);
var allEntities = new List<Entity>();
@@ -311,24 +400,32 @@ public class DrawingSplitterTests
var pgm = ConvertGeometry.ToProgram(allEntities);
var drawing = new Drawing("CIRC", pgm);
var results = DrawingSplitter.Split(drawing,
var results = DrawingSplitter.Split(
drawing,
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
new SplitParameters());
new SplitParameters()
);
Assert.Equal(2, results.Count);
// Left piece should have the hole — verify by checking it has arc entities
var leftEntities = ConvertProgram.ToGeometry(results[0].Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var leftEntities = ConvertProgram
.ToGeometry(results[0].Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
var leftArcs = leftEntities.OfType<Arc>().ToList();
// Decomposed circle = 2 arcs. Both should be present.
Assert.True(leftArcs.Count >= 2,
$"Left piece should have at least 2 arcs (full circle), but has {leftArcs.Count}");
Assert.True(
leftArcs.Count >= 2,
$"Left piece should have at least 2 arcs (full circle), but has {leftArcs.Count}"
);
// Right piece should have no arcs (hole is on the left)
var rightEntities = ConvertProgram.ToGeometry(results[1].Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var rightEntities = ConvertProgram
.ToGeometry(results[1].Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
var rightArcs = rightEntities.OfType<Arc>().ToList();
Assert.Equal(0, rightArcs.Count);
}
@@ -344,7 +441,7 @@ public class DrawingSplitterTests
new Line(new Vector(0, 0), new Vector(100, 0)),
new Line(new Vector(100, 0), new Vector(100, 50)),
new Line(new Vector(100, 50), new Vector(0, 50)),
new Line(new Vector(0, 50), new Vector(0, 0))
new Line(new Vector(0, 50), new Vector(0, 0)),
};
var hole = new Circle(new Vector(20, 25), 3);
var allEntities = new List<Entity>();
@@ -356,14 +453,19 @@ public class DrawingSplitterTests
drawing.Bends = new List<OpenNest.Bending.Bend>();
// Split — the circle gets decomposed into two arcs
var results = DrawingSplitter.Split(drawing,
var results = DrawingSplitter.Split(
drawing,
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
new SplitParameters());
new SplitParameters()
);
Assert.Equal(2, results.Count);
// Write left piece to DXF and re-import
var tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "split_roundtrip_test.dxf");
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
"split_roundtrip_test.dxf"
);
try
{
var writer = new OpenNest.IO.SplitDxfWriter();
@@ -374,8 +476,10 @@ public class DrawingSplitterTests
var afterArcs = reimportResult.Entities.OfType<Arc>().Count();
var afterCircles = reimportResult.Entities.OfType<Circle>().Count();
Assert.True(afterArcs + afterCircles * 2 >= 2,
$"After DXF round-trip: {afterArcs} arcs, {afterCircles} circles (expected 2+ for full hole)");
Assert.True(
afterArcs + afterCircles * 2 >= 2,
$"After DXF round-trip: {afterArcs} arcs, {afterCircles} circles (expected 2+ for full hole)"
);
}
finally
{
@@ -401,14 +505,14 @@ public class DrawingSplitterTests
new Line(new Vector(0, 0), new Vector(255, 0)),
new Line(new Vector(255, 0), new Vector(255, 55)),
new Line(new Vector(255, 55), new Vector(0, 55)),
new Line(new Vector(0, 55), new Vector(0, 0))
new Line(new Vector(0, 55), new Vector(0, 0)),
};
var slotEntities = new List<Entity>
{
new Line(new Vector(10, 10), new Vector(245, 10)),
new Line(new Vector(245, 10), new Vector(245, 45)),
new Line(new Vector(245, 45), new Vector(10, 45)),
new Line(new Vector(10, 45), new Vector(10, 10))
new Line(new Vector(10, 45), new Vector(10, 10)),
};
var allEntities = new List<Entity>();
allEntities.AddRange(outerEntities);
@@ -422,10 +526,14 @@ public class DrawingSplitterTests
new SplitLine(55.0, CutOffAxis.Vertical),
new SplitLine(110.0, CutOffAxis.Vertical),
new SplitLine(165.0, CutOffAxis.Vertical),
new SplitLine(220.0, CutOffAxis.Vertical)
new SplitLine(220.0, CutOffAxis.Vertical),
};
var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters { Type = SplitType.Straight });
var results = DrawingSplitter.Split(
drawing,
splitLines,
new SplitParameters { Type = SplitType.Straight }
);
// R1 (0..55) → 1 notched piece, height 55
// R2 (55..110) → upper strip + lower strip, each height 10
@@ -454,8 +562,10 @@ public class DrawingSplitterTests
// Each piece should form a closed perimeter (no dangling edges, no gaps).
foreach (var piece in results)
{
var entities = ConvertProgram.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var entities = ConvertProgram
.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
Assert.True(entities.Count >= 3, $"{piece.Name} must have at least 3 edges");
@@ -464,8 +574,10 @@ public class DrawingSplitterTests
var end = GetEndPoint(entities[i]);
var nextStart = GetStartPoint(entities[(i + 1) % entities.Count]);
var gap = end.DistanceTo(nextStart);
Assert.True(gap < 0.01,
$"{piece.Name} gap of {gap:F4} between edge {i} end and edge {(i + 1) % entities.Count} start");
Assert.True(
gap < 0.01,
$"{piece.Name} gap of {gap:F4} between edge {i} end and edge {(i + 1) % entities.Count} start"
);
}
}
}
@@ -477,7 +589,12 @@ public class DrawingSplitterTests
// five columns. Exercises the same path as the synthetic
// Split_RectangleWithSpanningSlot_ProducesDisconnectedStrips test but through
// the full DXF import pipeline.
var path = Path.Combine(AppContext.BaseDirectory, "Splitting", "TestData", "split_test.dxf");
var path = Path.Combine(
AppContext.BaseDirectory,
"Splitting",
"TestData",
"split_test.dxf"
);
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
var imported = OpenNest.IO.Dxf.Import(path);
@@ -487,13 +604,16 @@ public class DrawingSplitterTests
var bb = profile.Perimeter.BoundingBox;
var offsetX = -bb.X;
var offsetY = -bb.Y;
foreach (var e in profile.Perimeter.Entities) e.Offset(offsetX, offsetY);
foreach (var e in profile.Perimeter.Entities)
e.Offset(offsetX, offsetY);
foreach (var cutout in profile.Cutouts)
foreach (var e in cutout.Entities) e.Offset(offsetX, offsetY);
foreach (var e in cutout.Entities)
e.Offset(offsetX, offsetY);
var allEntities = new List<Entity>();
allEntities.AddRange(profile.Perimeter.Entities);
foreach (var cutout in profile.Cutouts) allEntities.AddRange(cutout.Entities);
foreach (var cutout in profile.Cutouts)
allEntities.AddRange(cutout.Entities);
var drawing = new Drawing("SPLITTEST", ConvertGeometry.ToProgram(allEntities));
var originalArea = drawing.Area;
@@ -504,10 +624,14 @@ public class DrawingSplitterTests
new SplitLine(55.0, CutOffAxis.Vertical),
new SplitLine(110.0, CutOffAxis.Vertical),
new SplitLine(165.0, CutOffAxis.Vertical),
new SplitLine(220.0, CutOffAxis.Vertical)
new SplitLine(220.0, CutOffAxis.Vertical),
};
var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters { Type = SplitType.Straight });
var results = DrawingSplitter.Split(
drawing,
splitLines,
new SplitParameters { Type = SplitType.Straight }
);
// Area must be preserved within tolerance (floating-point coords in the DXF).
var totalArea = results.Sum(d => d.Area);
@@ -515,16 +639,20 @@ public class DrawingSplitterTests
// At least one region must yield more than one physical strip — that's the
// whole point of the fix: a cutout that spans a region disconnects it.
Assert.True(results.Count > splitLines.Count + 1,
$"Expected more than {splitLines.Count + 1} pieces (some regions split into strips), got {results.Count}");
Assert.True(
results.Count > splitLines.Count + 1,
$"Expected more than {splitLines.Count + 1} pieces (some regions split into strips), got {results.Count}"
);
// Every output drawing must resolve into fully-closed shapes (outer loop
// and any hole loops), with no dangling geometry. A piece that contains
// a cutout will have its entities span more than one connected loop.
foreach (var piece in results)
{
var entities = ConvertProgram.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var entities = ConvertProgram
.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
Assert.True(entities.Count >= 3, $"{piece.Name} has only {entities.Count} entities");
@@ -533,8 +661,10 @@ public class DrawingSplitterTests
foreach (var shape in shapes)
{
Assert.True(shape.IsClosed(),
$"{piece.Name} contains an open chain of {shape.Entities.Count} entities");
Assert.True(
shape.IsClosed(),
$"{piece.Name} contains an open chain of {shape.Entities.Count} entities"
);
}
}
}
@@ -545,7 +675,7 @@ public class DrawingSplitterTests
{
Line l => l.StartPoint,
Arc a => a.StartPoint(),
_ => new Vector(0, 0)
_ => new Vector(0, 0),
};
}
@@ -555,7 +685,7 @@ public class DrawingSplitterTests
{
Line l => l.EndPoint,
Arc a => a.EndPoint(),
_ => new Vector(0, 0)
_ => new Vector(0, 0),
};
}
}
+2 -1
View File
@@ -98,7 +98,8 @@ public class EntitySplitTests
var expectedHigh = 50.0 + System.Math.Sqrt(300);
Assert.True(
System.Math.Abs(y - expectedLow) < 0.1 || System.Math.Abs(y - expectedHigh) < 0.1,
$"Expected Y near {expectedLow:F2} or {expectedHigh:F2}, got {y:F2}");
$"Expected Y near {expectedLow:F2} or {expectedHigh:F2}, got {y:F2}"
);
}
// --- CrossesSplitLine ---
@@ -12,7 +12,12 @@ public class SplitDxfWriterEtchLayerTests
public void Write_DrawingWithUpBend_EtchLinesHaveEtchLayer()
{
// Create a simple rectangular drawing with an up bend
var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "TEST",
Length = 100,
Width = 50,
}.GetDrawing();
drawing.Bends = new List<Bend>
{
new Bend
@@ -22,8 +27,8 @@ public class SplitDxfWriterEtchLayerTests
Direction = BendDirection.Up,
Angle = 90,
Radius = 0.06,
NoteText = "UP 90° R0.06"
}
NoteText = "UP 90° R0.06",
},
};
var tempPath = Path.Combine(Path.GetTempPath(), $"etch_layer_test_{Guid.NewGuid()}.dxf");
@@ -50,8 +55,9 @@ public class SplitDxfWriterEtchLayerTests
// Check if this line is an etch mark (short, near the bend Y=25)
var midY = (line.StartPoint.Y + line.EndPoint.Y) / 2;
var length = System.Math.Sqrt(
System.Math.Pow(line.EndPoint.X - line.StartPoint.X, 2) +
System.Math.Pow(line.EndPoint.Y - line.StartPoint.Y, 2));
System.Math.Pow(line.EndPoint.X - line.StartPoint.X, 2)
+ System.Math.Pow(line.EndPoint.Y - line.StartPoint.Y, 2)
);
if (System.Math.Abs(midY - 25) < 0.1 && length <= 1.5 && layerName != "BEND")
{
@@ -61,9 +67,11 @@ public class SplitDxfWriterEtchLayerTests
}
// Should have etch lines (up bend with length 100 > 3*EtchLength, so 2 etch dashes)
Assert.True(etchEntities.Count >= 2,
$"Expected at least 2 etch lines, found {etchEntities.Count}. " +
$"All entities: {string.Join(", ", allEntities.Select(e => $"{e.Type}@{e.LayerName}"))}");
Assert.True(
etchEntities.Count >= 2,
$"Expected at least 2 etch lines, found {etchEntities.Count}. "
+ $"All entities: {string.Join(", ", allEntities.Select(e => $"{e.Type}@{e.LayerName}"))}"
);
// ALL etch lines should be on the ETCH layer, not layer 0
foreach (var etch in etchEntities)
@@ -83,7 +91,12 @@ public class SplitDxfWriterEtchLayerTests
public void Write_SplitDrawingWithUpBend_EtchLinesHaveEtchLayer()
{
// Create a drawing, split it, then verify etch layers in the split DXFs
var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "TEST",
Length = 100,
Width = 50,
}.GetDrawing();
drawing.Bends = new List<Bend>
{
new Bend
@@ -93,8 +106,8 @@ public class SplitDxfWriterEtchLayerTests
Direction = BendDirection.Up,
Angle = 90,
Radius = 0.06,
NoteText = "UP 90° R0.06"
}
NoteText = "UP 90° R0.06",
},
};
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
@@ -109,7 +122,10 @@ public class SplitDxfWriterEtchLayerTests
Assert.NotNull(splitDrawing.Bends);
Assert.True(splitDrawing.Bends.Count > 0, $"{splitDrawing.Name} should have bends");
var tempPath = Path.Combine(Path.GetTempPath(), $"split_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf");
var tempPath = Path.Combine(
Path.GetTempPath(),
$"split_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf"
);
try
{
var writer = new SplitDxfWriter();
@@ -135,15 +151,19 @@ public class SplitDxfWriterEtchLayerTests
}
// Should have etch entities
Assert.True(etchLayerEntities.Count > 0,
$"{splitDrawing.Name}: No entities on ETCH layer. " +
$"All: {string.Join(", ", entitySummary)}");
Assert.True(
etchLayerEntities.Count > 0,
$"{splitDrawing.Name}: No entities on ETCH layer. "
+ $"All: {string.Join(", ", entitySummary)}"
);
// No entities should be on layer 0
Assert.True(layer0Entities.Count == 0,
$"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 " +
$"(expected all on CUT/BEND/ETCH). " +
$"All: {string.Join(", ", entitySummary)}");
Assert.True(
layer0Entities.Count == 0,
$"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 "
+ $"(expected all on CUT/BEND/ETCH). "
+ $"All: {string.Join(", ", entitySummary)}"
);
}
finally
{
@@ -158,7 +178,12 @@ public class SplitDxfWriterEtchLayerTests
{
// After re-import, ETCH entities should be filtered (like BEND) since
// etch marks are generated from bends, not treated as cut geometry.
var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "TEST",
Length = 100,
Width = 50,
}.GetDrawing();
drawing.Bends = new List<Bend>
{
new Bend
@@ -168,8 +193,8 @@ public class SplitDxfWriterEtchLayerTests
Direction = BendDirection.Up,
Angle = 90,
Radius = 0.06,
NoteText = "UP 90° R0.06"
}
NoteText = "UP 90° R0.06",
},
};
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
@@ -178,7 +203,10 @@ public class SplitDxfWriterEtchLayerTests
foreach (var splitDrawing in results)
{
var tempPath = Path.Combine(Path.GetTempPath(), $"reimport_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf");
var tempPath = Path.Combine(
Path.GetTempPath(),
$"reimport_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf"
);
try
{
var writer = new SplitDxfWriter();
@@ -188,25 +216,40 @@ public class SplitDxfWriterEtchLayerTests
var result = Dxf.Import(tempPath);
// ETCH entities should be filtered during import (like BEND)
var etchEntities = result.Entities
.Where(e => string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase))
var etchEntities = result
.Entities.Where(e =>
string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)
)
.ToList();
var layer0Entities = result.Entities
.Where(e => string.Equals(e.Layer?.Name, "0", StringComparison.OrdinalIgnoreCase))
var layer0Entities = result
.Entities.Where(e =>
string.Equals(e.Layer?.Name, "0", StringComparison.OrdinalIgnoreCase)
)
.ToList();
Assert.True(etchEntities.Count == 0,
$"{splitDrawing.Name}: ETCH entities should be filtered during import, found {etchEntities.Count}");
Assert.True(
etchEntities.Count == 0,
$"{splitDrawing.Name}: ETCH entities should be filtered during import, found {etchEntities.Count}"
);
Assert.True(layer0Entities.Count == 0,
$"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 after re-import");
Assert.True(
layer0Entities.Count == 0,
$"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 after re-import"
);
// All imported entities should be on CUT layer (cut geometry only)
Assert.True(result.Entities.Count > 0, $"{splitDrawing.Name}: Should have cut geometry");
Assert.True(result.Entities.All(e => string.Equals(e.Layer?.Name, "CUT", StringComparison.OrdinalIgnoreCase)),
$"{splitDrawing.Name}: All imported entities should be on CUT layer. " +
$"Found: {string.Join(", ", result.Entities.Select(e => e.Layer?.Name ?? "(null)").Distinct())}");
Assert.True(
result.Entities.Count > 0,
$"{splitDrawing.Name}: Should have cut geometry"
);
Assert.True(
result.Entities.All(e =>
string.Equals(e.Layer?.Name, "CUT", StringComparison.OrdinalIgnoreCase)
),
$"{splitDrawing.Name}: All imported entities should be on CUT layer. "
+ $"Found: {string.Join(", ", result.Entities.Select(e => e.Layer?.Name ?? "(null)").Distinct())}"
);
}
finally
{
@@ -15,7 +15,7 @@ public class SplitFeatureTests
Type = SplitType.WeldGapTabs,
TabWidth = 2.0,
TabHeight = 0.25,
TabCount = 2
TabCount = 2,
};
var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters);
@@ -104,7 +104,7 @@ public class SplitFeatureTests
Type = SplitType.SpikeGroove,
SpikeDepth = 1.0,
SpikeAngle = 60.0,
SpikePairCount = 2
SpikePairCount = 2,
};
var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters);
@@ -9,7 +9,12 @@ public class SplitIntegrationTest
[Fact]
public void Split_SpikeGroove_NoContinuityGaps()
{
var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "TEST",
Length = 100,
Width = 50,
}.GetDrawing();
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
sl.FeaturePositions.Add(12.5);
@@ -22,7 +27,7 @@ public class SplitIntegrationTest
SpikeDepth = 0.75,
SpikeWeldGap = 0.125,
SpikeAngle = 45,
SpikePairCount = 2
SpikePairCount = 2,
};
var results = DrawingSplitter.Split(drawing, new List<SplitLine> { sl }, parameters);
@@ -31,8 +36,10 @@ public class SplitIntegrationTest
foreach (var piece in results)
{
// Get cut entities only (no rapids)
var pieceEntities = ConvertProgram.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var pieceEntities = ConvertProgram
.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
// Check that consecutive entity endpoints connect (no gaps)
for (var i = 0; i < pieceEntities.Count - 1; i++)
@@ -40,8 +47,10 @@ public class SplitIntegrationTest
var end = GetEndPoint(pieceEntities[i]);
var start = GetStartPoint(pieceEntities[i + 1]);
var gap = end.DistanceTo(start);
Assert.True(gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
Assert.True(
gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"
);
}
// Area should be non-zero
@@ -52,7 +61,12 @@ public class SplitIntegrationTest
[Fact]
public void Split_SpikeGroove_Horizontal_NoContinuityGaps()
{
var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
var drawing = new RectangleShape
{
Name = "TEST",
Length = 100,
Width = 50,
}.GetDrawing();
var sl = new SplitLine(25.0, CutOffAxis.Horizontal);
sl.FeaturePositions.Add(25.0);
@@ -65,7 +79,7 @@ public class SplitIntegrationTest
SpikeDepth = 0.75,
SpikeWeldGap = 0.125,
SpikeAngle = 45,
SpikePairCount = 2
SpikePairCount = 2,
};
var results = DrawingSplitter.Split(drawing, new List<SplitLine> { sl }, parameters);
@@ -73,16 +87,20 @@ public class SplitIntegrationTest
foreach (var piece in results)
{
var pieceEntities = ConvertProgram.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var pieceEntities = ConvertProgram
.ToGeometry(piece.Program)
.Where(e => e.Layer != SpecialLayers.Rapid)
.ToList();
for (var i = 0; i < pieceEntities.Count - 1; i++)
{
var end = GetEndPoint(pieceEntities[i]);
var start = GetStartPoint(pieceEntities[i + 1]);
var gap = end.DistanceTo(start);
Assert.True(gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
Assert.True(
gap < 0.01,
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"
);
}
Assert.True(piece.Area > 0, $"{piece.Name} has zero area");
@@ -95,7 +113,7 @@ public class SplitIntegrationTest
{
Line l => l.StartPoint,
Arc a => a.StartPoint(),
_ => new Vector(0, 0)
_ => new Vector(0, 0),
};
}
@@ -105,7 +123,7 @@ public class SplitIntegrationTest
{
Line l => l.EndPoint,
Arc a => a.EndPoint(),
_ => new Vector(0, 0)
_ => new Vector(0, 0),
};
}
}
+5 -1
View File
@@ -69,7 +69,11 @@ public class AutoSplitCalculatorTests
public void SplitByCount_SingleAxis_EvenlySpaced()
{
var partBounds = new Box(0, 0, 100, 50);
var lines = AutoSplitCalculator.SplitByCount(partBounds, horizontalPieces: 1, verticalPieces: 3);
var lines = AutoSplitCalculator.SplitByCount(
partBounds,
horizontalPieces: 1,
verticalPieces: 3
);
Assert.Equal(2, lines.Count);
Assert.All(lines, l => Assert.Equal(CutOffAxis.Vertical, l.Axis));
+17 -8
View File
@@ -25,8 +25,10 @@ public class FillPipelineTests
engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
$"Expected phase results from all active strategies, got {engine.PhaseResults.Count}");
Assert.True(
engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count,
$"Expected phase results from all active strategies, got {engine.PhaseResults.Count}"
);
}
[Fact]
@@ -36,14 +38,21 @@ public class FillPipelineTests
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);
var parts = engine.Fill(
item,
plate.WorkArea(),
null,
System.Threading.CancellationToken.None
);
Assert.True(parts.Count > 0);
Assert.True(engine.WinnerPhase == NestPhase.Pairs ||
engine.WinnerPhase == NestPhase.Linear ||
engine.WinnerPhase == NestPhase.RectBestFit ||
engine.WinnerPhase == NestPhase.Extents ||
engine.WinnerPhase == NestPhase.Custom);
Assert.True(
engine.WinnerPhase == NestPhase.Pairs
|| engine.WinnerPhase == NestPhase.Linear
|| engine.WinnerPhase == NestPhase.RectBestFit
|| engine.WinnerPhase == NestPhase.Extents
|| engine.WinnerPhase == NestPhase.Custom
);
}
[Fact]
@@ -10,7 +10,10 @@ public class FillStrategyRegistryTests
{
var strategies = FillStrategyRegistry.Strategies;
Assert.True(strategies.Count >= 6, $"Expected at least 6 built-in strategies, got {strategies.Count}");
Assert.True(
strategies.Count >= 6,
$"Expected at least 6 built-in strategies, got {strategies.Count}"
);
Assert.Contains(strategies, s => s.Name == "Pairs");
Assert.Contains(strategies, s => s.Name == "RectBestFit");
Assert.Contains(strategies, s => s.Name == "Extents");
@@ -25,8 +28,10 @@ public class FillStrategyRegistryTests
var strategies = FillStrategyRegistry.Strategies;
for (var i = 1; i < strategies.Count; i++)
Assert.True(strategies[i].Order >= strategies[i - 1].Order,
$"Strategy '{strategies[i].Name}' (Order={strategies[i].Order}) should not precede '{strategies[i - 1].Name}' (Order={strategies[i - 1].Order})");
Assert.True(
strategies[i].Order >= strategies[i - 1].Order,
$"Strategy '{strategies[i].Name}' (Order={strategies[i].Order}) should not precede '{strategies[i - 1].Name}' (Order={strategies[i - 1].Order})"
);
}
[Fact]
@@ -35,7 +35,9 @@ public class StrategyOverlapTests
if (drawing is null)
return; // Skip if test DXF not available
_output.WriteLine($"Drawing bbox: {drawing.Program.BoundingBox().Width:F2} x {drawing.Program.BoundingBox().Length:F2}");
_output.WriteLine(
$"Drawing bbox: {drawing.Program.BoundingBox().Width:F2} x {drawing.Program.BoundingBox().Length:F2}"
);
var strategies = FillStrategyRegistry.Strategies.ToList();
var item = new NestItem { Drawing = drawing };
@@ -59,12 +61,17 @@ public class StrategyOverlapTests
context.SharedState["BestRotation"] = classification.PrimaryAngle;
context.SharedState["Classification"] = classification;
context.SharedState["AngleCandidates"] = new AngleCandidateBuilder().Build(
item, classification, context.WorkArea);
item,
classification,
context.WorkArea
);
var parts = strategy.Fill(context);
var count = parts?.Count ?? 0;
_output.WriteLine($"\n{strategy.GetType().Name} (Phase: {strategy.Phase}, Order: {strategy.Order}): {count} parts");
_output.WriteLine(
$"\n{strategy.GetType().Name} (Phase: {strategy.Phase}, Order: {strategy.Order}): {count} parts"
);
if (count == 0)
continue;
@@ -83,7 +90,9 @@ public class StrategyOverlapTests
if (hasOverlaps)
{
failures.Add($"{strategy.GetType().Name} ({strategy.Phase}): {pts.Count} collision pts, {count} parts");
failures.Add(
$"{strategy.GetType().Name} ({strategy.Phase}): {pts.Count} collision pts, {count} parts"
);
// Show overlapping pair details
for (var a = 0; a < parts.Count; a++)
@@ -92,16 +101,27 @@ public class StrategyOverlapTests
{
var ba = parts[a].BoundingBox;
var bb = parts[b].BoundingBox;
var oX = System.Math.Min(ba.Right, bb.Right) - System.Math.Max(ba.Left, bb.Left);
var oY = System.Math.Min(ba.Top, bb.Top) - System.Math.Max(ba.Bottom, bb.Bottom);
if (oX <= OpenNest.Math.Tolerance.Epsilon || oY <= OpenNest.Math.Tolerance.Epsilon)
var oX =
System.Math.Min(ba.Right, bb.Right) - System.Math.Max(ba.Left, bb.Left);
var oY =
System.Math.Min(ba.Top, bb.Top) - System.Math.Max(ba.Bottom, bb.Bottom);
if (
oX <= OpenNest.Math.Tolerance.Epsilon
|| oY <= OpenNest.Math.Tolerance.Epsilon
)
continue;
if (parts[a].Intersects(parts[b], out var pairPts) && pairPts.Count > 0)
{
_output.WriteLine($" [{a}] vs [{b}]: {pairPts.Count} pts, bbox overlap: {oX:F4} x {oY:F4}");
_output.WriteLine($" [{a}]: loc=({parts[a].Location.X:F4},{parts[a].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[a].Rotation):F2}°");
_output.WriteLine($" [{b}]: loc=({parts[b].Location.X:F4},{parts[b].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[b].Rotation):F2}°");
_output.WriteLine(
$" [{a}] vs [{b}]: {pairPts.Count} pts, bbox overlap: {oX:F4} x {oY:F4}"
);
_output.WriteLine(
$" [{a}]: loc=({parts[a].Location.X:F4},{parts[a].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[a].Rotation):F2}°"
);
_output.WriteLine(
$" [{b}]: loc=({parts[b].Location.X:F4},{parts[b].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[b].Rotation):F2}°"
);
}
}
}
@@ -114,5 +134,4 @@ public class StrategyOverlapTests
Assert.Empty(failures);
}
}
+36 -12
View File
@@ -33,8 +33,7 @@ public class StripeFillerTests
/// Builds a simple side-by-side pair BestFitResult for a rectangular drawing.
/// Places two copies next to each other along the X axis with the given spacing.
/// </summary>
private static List<BestFitResult> MakeSideBySideBestFits(
Drawing drawing, double spacing)
private static List<BestFitResult> MakeSideBySideBestFits(Drawing drawing, double spacing)
{
var bb = drawing.Program.BoundingBox();
var w = bb.Length;
@@ -71,10 +70,15 @@ public class StripeFillerTests
{
var pattern = MakeRectPattern(20, 10);
var angle = StripeFiller.FindAngleForTargetSpan(
pattern.Parts, 20.0, NestDirection.Horizontal);
pattern.Parts,
20.0,
NestDirection.Horizontal
);
Assert.True(System.Math.Abs(angle) < 0.05,
$"Expected angle near 0, got {OpenNest.Math.Angle.ToDegrees(angle):F1}°");
Assert.True(
System.Math.Abs(angle) < 0.05,
$"Expected angle near 0, got {OpenNest.Math.Angle.ToDegrees(angle):F1}°"
);
}
[Fact]
@@ -82,12 +86,17 @@ public class StripeFillerTests
{
var pattern = MakeRectPattern(20, 10);
var angle = StripeFiller.FindAngleForTargetSpan(
pattern.Parts, 22.0, NestDirection.Horizontal);
pattern.Parts,
22.0,
NestDirection.Horizontal
);
var rotated = FillHelpers.BuildRotatedPattern(pattern.Parts, angle);
var span = rotated.BoundingBox.Length;
Assert.True(System.Math.Abs(span - 22.0) < 0.5,
$"Expected span ~22, got {span:F2} at {OpenNest.Math.Angle.ToDegrees(angle):F1}°");
Assert.True(
System.Math.Abs(span - 22.0) < 0.5,
$"Expected span ~22, got {span:F2} at {OpenNest.Math.Angle.ToDegrees(angle):F1}°"
);
}
[Fact]
@@ -95,7 +104,10 @@ public class StripeFillerTests
{
var pattern = MakeRectPattern(20, 10);
var angle = StripeFiller.FindAngleForTargetSpan(
pattern.Parts, 30.0, NestDirection.Horizontal);
pattern.Parts,
30.0,
NestDirection.Horizontal
);
Assert.True(angle >= 0 && angle <= System.Math.PI / 2);
}
@@ -105,7 +117,11 @@ public class StripeFillerTests
{
var pattern = MakeRectPattern(20, 10);
var (angle, waste, count) = StripeFiller.ConvergeStripeAngle(
pattern.Parts, 120.0, 0.5, NestDirection.Horizontal);
pattern.Parts,
120.0,
0.5,
NestDirection.Horizontal
);
Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}");
Assert.True(waste < 18.0, $"Expected waste < 18, got {waste:F2}");
@@ -117,7 +133,11 @@ public class StripeFillerTests
// 10x5 pattern: short side (5) oriented along axis, so more pairs fit
var pattern = MakeRectPattern(10, 5);
var (angle, waste, count) = StripeFiller.ConvergeStripeAngle(
pattern.Parts, 100.0, 0.0, NestDirection.Horizontal);
pattern.Parts,
100.0,
0.0,
NestDirection.Horizontal
);
Assert.True(count >= 10, $"Expected at least 10 pairs, got {count}");
Assert.True(waste < 1.0, $"Expected low waste, got {waste:F2}");
@@ -128,7 +148,11 @@ public class StripeFillerTests
{
var pattern = MakeRectPattern(10, 20);
var (angle, waste, count) = StripeFiller.ConvergeStripeAngle(
pattern.Parts, 120.0, 0.5, NestDirection.Vertical);
pattern.Parts,
120.0,
0.5,
NestDirection.Vertical
);
Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}");
}