refactor: organize test project into subdirectories by feature area

Move 43 root-level test files into feature-specific subdirectories
mirroring the main codebase structure: Geometry, Fill, BestFit, CutOffs,
CuttingStrategy, Engine, IO. Update namespaces to match folder paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:46:43 -04:00
parent 7a6c407edd
commit 3e340e67e0
43 changed files with 62 additions and 61 deletions
@@ -0,0 +1,370 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Tests.CutOffs;
public class CutOffGeometryTests
{
private static readonly CutOffSettings ZeroClearance = new() { PartClearance = 0.0 };
private static double TotalCutLength(Program program, CutOffAxis axis = CutOffAxis.Vertical)
{
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)
{
total += axis == CutOffAxis.Vertical
? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y)
: System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X);
}
}
return total;
}
private static Program MakeSquare(double size)
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new LinearMove(new Vector(size, size)));
pgm.Codes.Add(new LinearMove(new Vector(0, size)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
return pgm;
}
private static Program MakeCircle(double radius)
{
// Rapid to (radius, 0) relative to center at (0, 0),
// then full-circle arc back to same point.
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(radius, 0)));
pgm.Codes.Add(new ArcMove(new Vector(radius, 0), new Vector(0, 0)));
return pgm;
}
private static Program MakeDiamond(double halfSize)
{
// Diamond: points at (half,0), (2*half,half), (half,2*half), (0,half)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(halfSize, 0)));
pgm.Codes.Add(new LinearMove(new Vector(halfSize * 2, halfSize)));
pgm.Codes.Add(new LinearMove(new Vector(halfSize, halfSize * 2)));
pgm.Codes.Add(new LinearMove(new Vector(0, halfSize)));
pgm.Codes.Add(new LinearMove(new Vector(halfSize, 0)));
return pgm;
}
private static Program MakeTriangle(double width, double height)
{
// Right triangle: (0,0) -> (width,0) -> (0,height) -> (0,0)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(width, 0)));
pgm.Codes.Add(new LinearMove(new Vector(0, height)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
return pgm;
}
[Fact]
public void Square_GeometryExclusionMatchesBoundingBox()
{
// For a square, geometry and BB should produce the same exclusion.
var drawing = new Drawing("sq", MakeSquare(20));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
// Vertical cut at X=20 (through the middle of the square).
// BB exclusion Y = [10, 30]. Geometry should give the same.
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance);
var codes = cutoff.Drawing.Program.Codes;
// Two segments: before and after the square → 4 codes
Assert.Equal(4, codes.Count);
}
[Fact]
public void Circle_GeometryExclusionNarrowerThanBoundingBox()
{
// Circle radius=10, center at (10,10) after placement.
// BB = (0,0,20,20). Vertical cut at X=2 clips the circle edge.
// BB would exclude full Y=[0,20].
// Geometry: at X=2, the chord is much narrower.
var drawing = new Drawing("circ", MakeCircle(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(0, 0);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
// Cut at X=2: inside the BB but near the edge of the circle.
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, cache);
// The circle chord at X=2 from center (10,0) is much shorter than 20.
// With geometry, we get a tighter exclusion, so the segments should
// 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}");
}
[Fact]
public void Diamond_GeometryExclusionNarrowerThanBoundingBox()
{
// Diamond half=10 → points at (10,0), (20,10), (10,20), (0,10).
// BB = (0,0,20,20).
// Vertical cut at X=5: BB excludes Y=[0,20].
// Diamond edge at X=5: intersects at Y=5 and Y=15 → exclusion [5,15].
var drawing = new Drawing("dia", MakeDiamond(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(0, 0);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, cache);
// 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}");
}
[Fact]
public void Triangle_AsymmetricExclusion()
{
// Right triangle: (0,0)→(30,0)→(0,30)→(0,0) placed at (10,10).
// Vertical cut at X=20 (10 into the triangle from left).
// The hypotenuse from (40,10) to (10,40): at X=20, Y = 30.
// So geometry exclusion should be roughly [10, 30], not [10, 40] like BB.
var drawing = new Drawing("tri", MakeTriangle(30, 30));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, cache);
// 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}");
}
[Fact]
public void CutLineMissesPart_NoExclusion()
{
var drawing = new Drawing("sq", MakeSquare(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(50, 50);
plate.Parts.Add(part);
// Vertical cut at X=5: well outside the part at X=[50,60].
var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance);
// Single full-length segment → 2 codes
Assert.Equal(2, cutoff.Drawing.Program.Codes.Count);
}
[Fact]
public void HorizontalCut_Circle_UsesGeometry()
{
var drawing = new Drawing("circ", MakeCircle(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(0, 0);
plate.Parts.Add(part);
var cache = Plate.BuildPerimeterCache(plate);
// Horizontal cut at Y=2: near the edge of the circle.
var cutoff = new CutOff(new Vector(0, 2), CutOffAxis.Horizontal);
cutoff.Regenerate(plate, ZeroClearance, cache);
// 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}");
}
[Fact]
public void Clearance_ExpandsGeometryExclusion()
{
var drawing = new Drawing("sq", MakeSquare(20));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
var settings = new CutOffSettings { PartClearance = 5.0 };
var cache = Plate.BuildPerimeterCache(plate);
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings, cache);
// Square at Y=[10,30]. With 5 clearance → exclusion [5,35].
// Segments: [0,5] and [35,100] → 4 codes.
Assert.Equal(4, cutoff.Drawing.Program.Codes.Count);
}
[Fact]
public void BuildPerimeterCache_OpenContourGetsConvexHull()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(new Drawing("open", pgm)));
var cache = Plate.BuildPerimeterCache(plate);
Assert.Single(cache);
var perimeter = cache[plate.Parts[0]];
Assert.NotNull(perimeter);
Assert.IsType<Polygon>(perimeter);
}
[Fact]
public void NullCache_FallsBackToBoundingBox()
{
// Without a cache, should still work (using BB fallback).
var drawing = new Drawing("sq", MakeSquare(20));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(10, 10);
plate.Parts.Add(part);
var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance, null);
Assert.True(cutoff.Drawing.Program.Codes.Count > 0);
}
[Fact]
public void MultipleParts_IndependentExclusions()
{
var plate = new Plate(100, 100);
var sq1 = new Drawing("sq1", MakeSquare(10));
var p1 = Part.CreateAtOrigin(sq1);
p1.Location = new Vector(10, 10);
plate.Parts.Add(p1);
var sq2 = new Drawing("sq2", MakeSquare(10));
var p2 = Part.CreateAtOrigin(sq2);
p2.Location = new Vector(10, 50);
plate.Parts.Add(p2);
// Vertical cut at X=15 crosses both parts.
var cutoff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
cutoff.Regenerate(plate, ZeroClearance);
// 3 segments: before p1, between p1 and p2, after p2 → 6 codes
Assert.Equal(6, cutoff.Drawing.Program.Codes.Count);
}
[Fact]
public void CollectPoints_LinesAndArcs_ReturnsAllPoints()
{
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)
};
var points = entities.CollectPoints();
// Line: 2 points. Arc: 2 endpoints + 4 cardinals = 6. Total = 8.
Assert.Equal(8, points.Count);
}
[Fact]
public void PlatePerimeterCache_ReturnsOneEntryPerPart()
{
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(new Drawing("a", MakeSquare(10))));
plate.Parts.Add(new Part(new Drawing("b", MakeCircle(5))));
plate.Parts.Add(new Part(new Drawing("c", MakeDiamond(8))));
var cache = Plate.BuildPerimeterCache(plate);
Assert.Equal(3, cache.Count);
}
[Fact]
public void PlatePerimeterCache_SkipsCutOffParts()
{
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(new Drawing("real", MakeSquare(10))));
plate.Parts.Add(new Part(new Drawing("cutoff", new Program()) { IsCutOff = true }));
var cache = Plate.BuildPerimeterCache(plate);
Assert.Single(cache);
}
[Fact]
public void RegenerateCutOffs_UsesGeometryExclusions()
{
// Circle radius=10 at origin. Vertical cut at X=2.
// With geometry: tighter exclusion than BB.
var drawing = new Drawing("circ", MakeCircle(10));
var plate = new Plate(100, 100);
var part = Part.CreateAtOrigin(drawing);
plate.Parts.Add(part);
var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings { PartClearance = 0 });
// Find the materialized cut-off part
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}");
}
[Fact]
public void ShapeProfile_SelectsLargestShapeAsPerimeter()
{
// Outer square: (5,0)→(25,0)→(25,20)→(5,20)→(5,0)
// Inner cutout: (0,5)→(10,5)→(10,15)→(0,15)→(0,5)
// The cutout has Left=0, perimeter has Left=5.
// Old heuristic would pick the cutout as perimeter.
var outer = new Shape();
outer.Entities.Add(new Line(new Vector(5, 0), new Vector(25, 0)));
outer.Entities.Add(new Line(new Vector(25, 0), new Vector(25, 20)));
outer.Entities.Add(new Line(new Vector(25, 20), new Vector(5, 20)));
outer.Entities.Add(new Line(new Vector(5, 20), new Vector(5, 0)));
var inner = new Shape();
inner.Entities.Add(new Line(new Vector(0, 5), new Vector(10, 5)));
inner.Entities.Add(new Line(new Vector(10, 5), new Vector(10, 15)));
inner.Entities.Add(new Line(new Vector(10, 15), new Vector(0, 15)));
inner.Entities.Add(new Line(new Vector(0, 15), new Vector(0, 5)));
// 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(outer.Entities);
var profile = new ShapeProfile(entities);
// Perimeter should be the outer (larger) shape
var bb = profile.Perimeter.BoundingBox;
Assert.Equal(20.0, bb.Width, 1);
Assert.Equal(20.0, bb.Length, 1);
}
}
@@ -0,0 +1,119 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests.CutOffs;
public class CutOffSerializationTests
{
[Fact]
public void RoundTrip_CutOffsPreserved()
{
var nest = new Nest();
nest.Name = "test";
nest.DateCreated = DateTime.Now;
nest.DateLastModified = DateTime.Now;
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var drawing = new Drawing("part1", pgm);
nest.Drawings.Add(drawing);
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
plate.CutOffs.Add(new CutOff(new Vector(62.0, 24.0), CutOffAxis.Vertical));
plate.CutOffs.Add(new CutOff(new Vector(48.0, 30.0), CutOffAxis.Horizontal));
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
using var stream = new MemoryStream();
var writer = new NestWriter(nest);
writer.Write(stream);
stream.Position = 0;
var reader = new NestReader(stream);
var loaded = reader.Read();
Assert.Single(loaded.Plates);
var loadedPlate = loaded.Plates[0];
Assert.Equal(2, loadedPlate.CutOffs.Count);
Assert.Equal(CutOffAxis.Vertical, loadedPlate.CutOffs[0].Axis);
Assert.Equal(62.0, loadedPlate.CutOffs[0].Position.X, 5);
Assert.Equal(24.0, loadedPlate.CutOffs[0].Position.Y, 5);
Assert.Equal(CutOffAxis.Horizontal, loadedPlate.CutOffs[1].Axis);
Assert.Single(loadedPlate.Parts.Where(p => !p.BaseDrawing.IsCutOff));
Assert.Single(loaded.Drawings);
}
[Fact]
public void NestWriter_SkipsCutOffPartsInPartsList()
{
var nest = new Nest();
nest.Name = "test";
nest.DateCreated = DateTime.Now;
nest.DateLastModified = DateTime.Now;
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var drawing = new Drawing("part1", pgm);
nest.Drawings.Add(drawing);
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
plate.CutOffs.Add(new CutOff(new Vector(50, 25), CutOffAxis.Vertical));
plate.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
Assert.Equal(2, plate.Parts.Count);
using var stream = new MemoryStream();
var writer = new NestWriter(nest);
writer.Write(stream);
stream.Position = 0;
var reader = new NestReader(stream);
var loaded = reader.Read();
Assert.Single(loaded.Plates[0].Parts.Where(p => !p.BaseDrawing.IsCutOff));
}
[Fact]
public void RoundTrip_LimitsPreserved()
{
var nest = new Nest();
nest.Name = "test";
nest.DateCreated = DateTime.Now;
nest.DateLastModified = DateTime.Now;
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var drawing = new Drawing("part1", pgm);
nest.Drawings.Add(drawing);
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.RegenerateCutOffs(new CutOffSettings());
nest.Plates.Add(plate);
using var stream = new MemoryStream();
var writer = new NestWriter(nest);
writer.Write(stream);
stream.Position = 0;
var reader = new NestReader(stream);
var loaded = reader.Read();
var loadedPlate = loaded.Plates[0];
Assert.Equal(85.0, loadedPlate.CutOffs[0].EndLimit);
Assert.Null(loadedPlate.CutOffs[0].StartLimit);
Assert.Equal(30.0, loadedPlate.CutOffs[1].StartLimit);
Assert.Null(loadedPlate.CutOffs[1].EndLimit);
}
}
+266
View File
@@ -0,0 +1,266 @@
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Tests.CutOffs;
public class CutOffTests
{
[Fact]
public void Drawing_IsCutOff_DefaultsFalse()
{
var drawing = new Drawing("test", new Program());
Assert.False(drawing.IsCutOff);
}
[Fact]
public void Plate_CutOffPart_DoesNotIncrementQuantity()
{
var drawing = new Drawing("cutoff", new Program()) { IsCutOff = true };
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(drawing));
Assert.Equal(0, drawing.Quantity.Nested);
}
[Fact]
public void Plate_Utilization_ExcludesCutOffParts()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
var realDrawing = new Drawing("real", pgm);
var cutoffDrawing = new Drawing("cutoff", new Program()) { IsCutOff = true };
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(realDrawing));
plate.Parts.Add(new Part(cutoffDrawing));
var utilization = plate.Utilization();
var expected = realDrawing.Area / plate.Area();
Assert.Equal(expected, utilization, 5);
}
[Fact]
public void Plate_HasOverlappingParts_SkipsCutOffParts()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
var realDrawing = new Drawing("real", pgm);
var cutoffDrawing = new Drawing("cutoff", pgm) { IsCutOff = true };
var plate = new Plate(100, 100);
plate.Parts.Add(new Part(realDrawing));
plate.Parts.Add(new Part(cutoffDrawing));
var hasOverlap = plate.HasOverlappingParts(out var pts);
Assert.False(hasOverlap);
}
[Fact]
public void CutOff_VerticalCut_GeneratesFullLineOnEmptyPlate()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 20), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
Assert.NotNull(cutoff.Drawing);
Assert.True(cutoff.Drawing.IsCutOff);
Assert.True(cutoff.Drawing.Program.Codes.Count > 0);
}
[Fact]
public void CutOff_HorizontalCut_GeneratesFullLineOnEmptyPlate()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
var cutoff = new CutOff(new Vector(25, 20), CutOffAxis.Horizontal);
cutoff.Regenerate(plate, settings);
var codes = cutoff.Drawing.Program.Codes;
Assert.Equal(2, codes.Count);
}
[Fact]
public void CutOff_VerticalCut_TrimsAroundPart()
{
// Create a 10x10 part at the origin, then move it to (20,20)
// so the bounding box is Box(20,20,10,10) and doesn't span the origin.
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
var drawing = new Drawing("sq", pgm);
var plate = new Plate(50, 50);
var part = Part.CreateAtOrigin(drawing);
part.Location = new Vector(20, 20);
plate.Parts.Add(part);
// Vertical cut at X=25 runs along Y from 0 to 50.
// Part BB at (20,20,10,10) with clearance 1 → exclusion X=[19,31], Y=[19,31].
// X=25 is within [19,31] so exclusion applies: skip Y=[19,31].
// Segments: (0, 19) and (31, 50) → 2 segments → 4 codes.
var settings = new CutOffSettings { PartClearance = 1.0 };
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
var codes = cutoff.Drawing.Program.Codes;
Assert.Equal(4, codes.Count);
}
[Fact]
public void CutOff_ShortSegment_FilteredByMinLength()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(20, 0.02)));
pgm.Codes.Add(new LinearMove(new Vector(30, 0.02)));
pgm.Codes.Add(new LinearMove(new Vector(30, 10)));
pgm.Codes.Add(new LinearMove(new Vector(20, 10)));
pgm.Codes.Add(new LinearMove(new Vector(20, 0.02)));
var drawing = new Drawing("sq", pgm);
var plate = new Plate(50, 50);
plate.Parts.Add(new Part(drawing));
var settings = new CutOffSettings { PartClearance = 0.0, MinSegmentLength = 0.05 };
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
var rapidCount = cutoff.Drawing.Program.Codes.Count(c => c is RapidMove);
var lineCount = cutoff.Drawing.Program.Codes.Count(c => c is LinearMove);
Assert.Equal(rapidCount, lineCount);
}
[Fact]
public void CutOff_Overtravel_ExtendsFarEnd()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings { Overtravel = 2.0 };
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
cutoff.Regenerate(plate, settings);
// Plate(100, 50) = Width=100, Length=50. Vertical cut runs along Y (Width axis).
// BoundingBox Y extent = Size.Width = 100. With 2" overtravel = 102.
// Default AwayFromOrigin: RapidMove to near end (0), LinearMove to far end (102).
var linearMoves = cutoff.Drawing.Program.Codes.OfType<LinearMove>().ToList();
Assert.Single(linearMoves);
Assert.Equal(102.0, linearMoves[0].EndPoint.Y, 5);
}
[Fact]
public void CutOff_StartLimit_TruncatesNearEnd()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
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).
var rapidMoves = cutoff.Drawing.Program.Codes.OfType<RapidMove>().ToList();
Assert.Single(rapidMoves);
Assert.Equal(20.0, rapidMoves[0].EndPoint.Y, 5);
}
[Fact]
public void CutOff_EndLimit_TruncatesFarEnd()
{
var plate = new Plate(100, 50);
var settings = new CutOffSettings();
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).
var linearMoves = cutoff.Drawing.Program.Codes.OfType<LinearMove>().ToList();
Assert.Single(linearMoves);
Assert.Equal(80.0, linearMoves[0].EndPoint.Y, 5);
}
[Fact]
public void CutOff_BothLimits_LShapedCornerCut()
{
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
};
hCut.Regenerate(plate, settings);
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);
Assert.True(vCut.Drawing.Program.Codes.Count > 0);
}
[Fact]
public void Plate_RegenerateCutOffs_MaterializesParts()
{
var plate = new Plate(100, 50);
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings());
Assert.Single(plate.Parts);
Assert.True(plate.Parts[0].BaseDrawing.IsCutOff);
}
[Fact]
public void Plate_RegenerateCutOffs_ReplacesOldParts()
{
var plate = new Plate(100, 50);
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
var settings = new CutOffSettings();
plate.RegenerateCutOffs(settings);
plate.RegenerateCutOffs(settings);
Assert.Single(plate.Parts);
}
[Fact]
public void Plate_RegenerateCutOffs_DoesNotAffectRegularParts()
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(5, 5)));
var drawing = new Drawing("real", pgm);
var plate = new Plate(100, 50);
plate.Parts.Add(new Part(drawing));
var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical);
plate.CutOffs.Add(cutoff);
plate.RegenerateCutOffs(new CutOffSettings());
Assert.Equal(2, plate.Parts.Count);
Assert.False(plate.Parts[0].BaseDrawing.IsCutOff);
Assert.True(plate.Parts[1].BaseDrawing.IsCutOff);
}
}