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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)}");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user