Multi-sheet nests previously went into one program with a single header size and one M50 at the end, so sheet 2 would cut into sheet 1's skeleton. New Sheets settings: - One program per sheet (default on): JOB.cnc -> JOB-1.cnc, JOB-2.cnc, each a full program with its own size and pallet change (CL-series batch rule, EM-423 7.4). Single-sheet nests keep the chosen name. - Off: one program, with /L "L0" + pallet change between sheets; mixed sheet sizes are rejected. - Pallet change code (default M50, unconfirmed for multi-sheet CI Fiber runs; documented as a release blocker). All sheets are validated and rendered before any file is written. IMultiFilePostProcessor lets the desktop app confirm overwrites of every target file and list what was saved, and the console print each file.
549 lines
21 KiB
C#
549 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Posts.CincinnatiCIFiber;
|
|
|
|
namespace OpenNest.Tests.CincinnatiCIFiber;
|
|
|
|
/// <summary>
|
|
/// Output-contract tests for the Cincinnati CI Fiber post, checked against the
|
|
/// structure of the machine sample NC (12992-4SS_NEST.nc): header block,
|
|
/// restart jump, N-labelled contour blocks with /L macro lines, G41/G42
|
|
/// selection, trimmed coordinate format and the M50/M30/% tail.
|
|
/// </summary>
|
|
public class CIFiberPostProcessorTests
|
|
{
|
|
private static CIFiberPostConfig MakeConfig() =>
|
|
new() { ConfigurationName = "CI FIBER 8K" };
|
|
|
|
private static string Post(Nest nest, CIFiberPostConfig? config = null)
|
|
{
|
|
var post = new CIFiberPostProcessor(config ?? MakeConfig());
|
|
using var ms = new MemoryStream();
|
|
post.Post(nest, ms);
|
|
return Encoding.UTF8.GetString(ms.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 10x10 CW square perimeter (closed loop, no leading rapid) at the plate
|
|
/// location, with one CCW full-circle hole posted as an incremental
|
|
/// SubProgramCall at (5,5) — the exact shape OpenNest nests produce.
|
|
/// </summary>
|
|
private static Nest MakeSquareWithHoleNest(Vector? partLocation = null)
|
|
{
|
|
var location = partLocation ?? new Vector(1.0, 2.0);
|
|
|
|
var pgm = new Program(Mode.Absolute);
|
|
|
|
// Hole sub-program: CCW full circle r=0.5 centred on the call offset,
|
|
// linear lead-in from below, left absolute then flipped to
|
|
// incremental — exactly what ContourCuttingStrategy emits for a hole.
|
|
var hole = new Program(Mode.Absolute);
|
|
hole.Codes.Add(new RapidMove(new Vector(0, -0.6)));
|
|
hole.Codes.Add(new LinearMove(new Vector(0, -0.5)) { Layer = LayerType.Leadin });
|
|
hole.Codes.Add(
|
|
new ArcMove(new Vector(0, -0.5), new Vector(0, 0), RotationType.CCW)
|
|
{
|
|
Layer = LayerType.Cut,
|
|
}
|
|
);
|
|
hole.Mode = Mode.Incremental;
|
|
pgm.Codes.Add(new SubProgramCall { Id = 1, Program = hole, Offset = new Vector(5, 5) });
|
|
|
|
// Perimeter: linear lead-in into a CW loop starting at (0,0).
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, -0.5)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)) { Layer = LayerType.Leadin });
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
|
|
|
var drawing = new Drawing("square-hole", pgm);
|
|
var part = new Part(drawing, location);
|
|
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(part);
|
|
|
|
var nest = new Nest("Test Nest") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
return nest;
|
|
}
|
|
|
|
private static List<string> Lines(string output) =>
|
|
output.Replace("\r\n", "\n").Split('\n').ToList();
|
|
|
|
[Fact]
|
|
public void Post_Structure_GoldenSquareWithHole()
|
|
{
|
|
var output = Post(MakeSquareWithHoleNest());
|
|
var lines = Lines(output);
|
|
|
|
// File ends with % and uses CRLF throughout, no BOM.
|
|
Assert.StartsWith("(", output);
|
|
Assert.EndsWith("%\r\n", output);
|
|
foreach (var raw in output.Split('\n').SkipLast(1))
|
|
Assert.EndsWith("\r", raw);
|
|
|
|
// Header block
|
|
Assert.Equal("( Test Nest )", lines[0]);
|
|
Assert.Equal("( CONFIGURATION - CI FIBER 8K )", lines[1]);
|
|
Assert.Matches(@"^\( \w+ \d+, \d{4} \d\d:\d\d (AM|PM) \)$", lines[2]);
|
|
Assert.Equal("( Material = Mild Steel .060 )", lines[3]);
|
|
Assert.Equal("V.E.MATERIAL = \"MSN\"", lines[4]);
|
|
Assert.Equal("V.E.THICKNESS = 0.060", lines[5]);
|
|
Assert.Equal("V.E.X_SIZE = 120.000", lines[6]);
|
|
Assert.Equal("V.E.Y_SIZE = 60.000", lines[7]);
|
|
Assert.Equal("V.E.UNIT = 1", lines[8]);
|
|
Assert.DoesNotContain("V.E.SHEET_WEIGHT", output);
|
|
|
|
// Restart preamble
|
|
Assert.Equal("G90", lines[9]);
|
|
Assert.Equal("L PROGRAMSTART.NC", lines[10]);
|
|
Assert.Equal("P3=V.E.R3", lines[11]);
|
|
Assert.Equal("$GOTO NP3:", lines[12]);
|
|
Assert.Equal("N0:", lines[13]);
|
|
Assert.Equal("( Sheet number - 1 )", lines[14]);
|
|
|
|
// Part block
|
|
Assert.Equal("( Part #1 )", lines[15]);
|
|
Assert.Equal("( PART:square-hole )", lines[16]);
|
|
Assert.Equal("V.E.R4=1", lines[17]);
|
|
|
|
// Contour 1 = the flattened hole (part program order).
|
|
Assert.Equal("N1:", lines[18]);
|
|
Assert.Equal("/L \"L0\"", lines[19]);
|
|
Assert.Equal("V.E.R3=1", lines[20]);
|
|
Assert.Equal("G0X6Y6.4", lines[21]); // pierce = part(1,2) + hole(5,5) + (0,-0.6)
|
|
Assert.Equal("/L \"L2\"", lines[22]);
|
|
Assert.Equal("G41", lines[23]);
|
|
Assert.Equal("G1X6Y6.5", lines[24]); // contour start = hole centre + (0,-0.5)
|
|
Assert.Equal("/L \"L6\"", lines[25]);
|
|
Assert.Equal("G3X6Y6.5I0J0.5", lines[26]); // I/J incremental from arc start
|
|
Assert.Equal("/L \"ZHSOFF\"", lines[27]);
|
|
|
|
// Contour 2 = perimeter.
|
|
Assert.Equal("N2:", lines[28]);
|
|
Assert.Equal("/L \"L0\"", lines[29]);
|
|
Assert.Equal("V.E.R3=2", lines[30]);
|
|
Assert.Equal("G0X1Y1.5", lines[31]);
|
|
Assert.Equal("/L \"L4\"", lines[32]);
|
|
Assert.Equal("G42", lines[33]);
|
|
Assert.Equal("G1X1Y2", lines[34]);
|
|
Assert.Equal("/L \"L6\"", lines[35]);
|
|
Assert.Equal("G1X1Y12", lines[36]);
|
|
Assert.Equal("G1X11Y12", lines[37]);
|
|
Assert.Equal("G1X11Y2", lines[38]);
|
|
Assert.Equal("G1X1Y2", lines[39]);
|
|
Assert.Equal("/L \"ZHSOFF\"", lines[40]);
|
|
|
|
// Part end and tail
|
|
Assert.Equal("( PART END )", lines[41]);
|
|
Assert.Equal("/L \"L0\"", lines[42]);
|
|
Assert.Equal("L PROGRAMEND.NC", lines[43]);
|
|
Assert.Equal("M50", lines[44]);
|
|
Assert.Equal("M30", lines[45]);
|
|
Assert.Equal("%", lines[46]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_FlattensRotatedPartHole_ToSheetCoords()
|
|
{
|
|
// Same nest, but the part is rotated 90 degrees CCW about the origin:
|
|
// (x,y) -> (-y,x). Location (1,2) -> (-2,1); hole offset (5,5) -> (-5,5).
|
|
var nest = MakeSquareWithHoleNest();
|
|
var part = nest.Plates[0].Parts[0];
|
|
part.Rotate(OpenNest.Math.Angle.ToRadians(90));
|
|
|
|
var output = Post(nest);
|
|
var lines = Lines(output);
|
|
|
|
// Hole pierce local (0,-0.6) -> (0.6,0): (-2,1)+(-5,5)+(0.6,0) = (-6.4,6).
|
|
Assert.Equal("G0X-6.4Y6", lines[21]);
|
|
// Contour start local (0,-0.5) -> (0.5,0) = (-6.5,6).
|
|
Assert.Equal("G1X-6.5Y6", lines[24]);
|
|
// Full CCW circle, centre local (0,0.5) from the start -> (-0.5,0):
|
|
// centre at (-7,6), so I=-0.5 J=0.
|
|
Assert.Equal("G3X-6.5Y6I-0.5J0", lines[26]);
|
|
// Perimeter corner local (0,10) -> (-10,0): pierce (-2,1)+(0.5,0)...
|
|
// pierce local (0,-0.5)->(0.5,0) = (-1.5,1); corner (-10,1)... check
|
|
// first cut G1 target = lead-in end = (-2,1).
|
|
Assert.Equal("G0X-1.5Y1", lines[31]);
|
|
Assert.Equal("G1X-2Y1", lines[34]);
|
|
Assert.Equal("G1X-12Y1", lines[36]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_CoordinateFormat_TrimsZerosAndNeverNegativeZero()
|
|
{
|
|
// Part placed so a coordinate lands on -0.0004 (rounds to -0) and on
|
|
// values whose 3-decimal representation keeps trailing digits.
|
|
var pgm = new Program(Mode.Absolute);
|
|
pgm.Codes.Add(new RapidMove(new Vector(-0.0004, 3.5009)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(5, 3.5009)) { Layer = LayerType.Leadin });
|
|
pgm.Codes.Add(new LinearMove(new Vector(5, 4)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(-0.0004, 4)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(-0.0004, 3.5009)) { Layer = LayerType.Cut });
|
|
|
|
var drawing = new Drawing("fmt", pgm);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(drawing, Vector.Zero));
|
|
var nest = new Nest("fmt") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
|
|
var output = Post(nest);
|
|
|
|
Assert.Contains("G0X0Y3.501", output); // -0.0004 -> "0" not "-0"; 3.5009 -> 3.501
|
|
Assert.Contains("G1X5Y3.501", output);
|
|
Assert.DoesNotContain("-0Y", output);
|
|
Assert.DoesNotContain("-0\r", output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_RejectsArcLeadIn_PerTf5200LinearFirstRule()
|
|
{
|
|
// A contour whose first move after the comp selection is an arc must
|
|
// be rejected (TF5200 13.2.4.1: first motion block after selection
|
|
// must be LINEAR).
|
|
var pgm = new Program(Mode.Absolute);
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
|
pgm.Codes.Add(new ArcMove(0, 0, 0, 0.5, RotationType.CCW) { Layer = LayerType.Cut });
|
|
|
|
var drawing = new Drawing("arcdirect", pgm);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(drawing, Vector.Zero));
|
|
var nest = new Nest("arc") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
|
|
var ex = Assert.Throws<InvalidOperationException>(() => Post(nest));
|
|
Assert.Contains("13.2.4.1", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_CutOff_PostsLastWithoutCompensationOrLeadIn()
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
var plate = nest.Plates[0];
|
|
|
|
// Vertical cut-off at X=20: a bare rapid + line, no lead-in.
|
|
var cutOff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
|
cutOff.Regenerate(plate, new CutOffSettings());
|
|
plate.Parts.Insert(0, new Part(cutOff.Drawing, Vector.Zero));
|
|
|
|
var lines = Lines(Post(nest));
|
|
|
|
// Cut-off follows the real part despite being first in plate order.
|
|
var partEnd = lines.IndexOf("( PART END )");
|
|
var cutOffPart = lines.IndexOf("( Part #2 )");
|
|
Assert.True(partEnd >= 0 && cutOffPart > partEnd);
|
|
|
|
var n3 = lines.IndexOf("N3:");
|
|
Assert.True(n3 > cutOffPart);
|
|
Assert.Equal("/L \"L0\"", lines[n3 + 1]);
|
|
Assert.Equal("V.E.R3=3", lines[n3 + 2]);
|
|
Assert.StartsWith("G0X20Y", lines[n3 + 3]);
|
|
Assert.Equal("/L \"L4\"", lines[n3 + 4]);
|
|
Assert.Equal("/L \"L6\"", lines[n3 + 5]);
|
|
Assert.StartsWith("G1X20Y", lines[n3 + 6]);
|
|
Assert.Equal("/L \"ZHSOFF\"", lines[n3 + 7]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_ValidatesTableSize()
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
|
|
var tight = MakeConfig();
|
|
tight.MaxTableX = 100; // plate length is 120
|
|
Assert.Contains("exceeds maximum table X", Assert.Throws<InvalidOperationException>(() => Post(nest, tight)).Message);
|
|
|
|
var wide = MakeConfig();
|
|
wide.MaxTableX = 160.25;
|
|
wide.MaxTableY = 81.25;
|
|
Post(nest, wide); // does not throw
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_SkipsSuppressedAndScribeMoves()
|
|
{
|
|
var pgm = new Program(Mode.Absolute);
|
|
|
|
// Suppressed contour (every move suppressed, as suppression marks a
|
|
// whole feature) must not appear.
|
|
pgm.Codes.Add(new RapidMove(new Vector(30, 30)) { Suppressed = true });
|
|
pgm.Codes.Add(
|
|
new LinearMove(new Vector(31, 30)) { Layer = LayerType.Leadin, Suppressed = true }
|
|
);
|
|
pgm.Codes.Add(
|
|
new LinearMove(new Vector(31, 31)) { Layer = LayerType.Cut, Suppressed = true }
|
|
);
|
|
|
|
// Scribe contour skipped by default.
|
|
pgm.Codes.Add(new RapidMove(new Vector(40, 40)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(41, 40)) { Layer = LayerType.Leadin });
|
|
pgm.Codes.Add(new LinearMove(new Vector(41, 41)) { Layer = LayerType.Scribe });
|
|
|
|
// Cutting contour that must appear.
|
|
pgm.Codes.Add(new RapidMove(new Vector(50, 50)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(51, 50)) { Layer = LayerType.Leadin });
|
|
pgm.Codes.Add(new LinearMove(new Vector(51, 51)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(50, 51)) { Layer = LayerType.Cut });
|
|
pgm.Codes.Add(new LinearMove(new Vector(50, 50)) { Layer = LayerType.Cut });
|
|
|
|
var drawing = new Drawing("layers", pgm);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(drawing, Vector.Zero));
|
|
var nest = new Nest("layers") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
|
|
var output = Post(nest);
|
|
var lines = Lines(output);
|
|
|
|
// Exactly one contour survived (N0: is the restart-jump label).
|
|
Assert.Single(lines.Where(l => l.StartsWith("N") && l != "N0:" && l.EndsWith(":")));
|
|
Assert.DoesNotContain("X30Y30", output);
|
|
Assert.DoesNotContain("X40Y40", output);
|
|
Assert.Contains("G0X50Y50", output);
|
|
Assert.Contains("V.E.R3=1", output);
|
|
}
|
|
|
|
[Fact]
|
|
public void MaterialCodes_MatchIgnoringCase_AfterJsonReload()
|
|
{
|
|
var options = new System.Text.Json.JsonSerializerOptions();
|
|
var json = System.Text.Json.JsonSerializer.Serialize(
|
|
new CIFiberPostConfig
|
|
{
|
|
MaterialCodes = new() { ["Stainless"] = "SSN" },
|
|
DefaultMaterialCode = "MSN",
|
|
},
|
|
options
|
|
);
|
|
|
|
var config = System.Text.Json.JsonSerializer.Deserialize<CIFiberPostConfig>(json, options)!;
|
|
|
|
Assert.Equal("SSN", config.ResolveMaterialCode("stainless"));
|
|
Assert.Equal("SSN", config.ResolveMaterialCode(" STAINLESS "));
|
|
Assert.Equal("MSN", config.ResolveMaterialCode("Aluminum"));
|
|
}
|
|
|
|
[Fact]
|
|
public void SavedConfigWithRetiredPartComment_StillLoads()
|
|
{
|
|
const string json = """
|
|
{ "PartComment": "OLD", "MaxTableX": 120.0, "DefaultMaterialCode": "SSN" }
|
|
""";
|
|
|
|
var config = System.Text.Json.JsonSerializer.Deserialize<CIFiberPostConfig>(json)!;
|
|
|
|
Assert.Equal(120.0, config.MaxTableX);
|
|
Assert.Equal("SSN", config.DefaultMaterialCode);
|
|
}
|
|
|
|
private static Nest MakeMultiSheetNest(params (double Width, double Length)[] extraSheets)
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
var drawing = nest.Plates[0].Parts[0].BaseDrawing;
|
|
foreach (var (width, length) in extraSheets)
|
|
{
|
|
var plate = new Plate(width, length);
|
|
plate.Parts.Add(new Part(drawing, new Vector(3, 4)));
|
|
nest.Plates.Add(plate);
|
|
}
|
|
return nest;
|
|
}
|
|
|
|
private static string NewTempDir()
|
|
{
|
|
var dir = Path.Combine(Path.GetTempPath(), $"cifiber-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(dir);
|
|
return dir;
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_OneProgramPerSheet_WritesNumberedFiles_EachAFullProgram()
|
|
{
|
|
var nest = MakeMultiSheetNest((48, 96));
|
|
nest.Plates.Add(new Plate(60, 120)); // empty sheet: not posted
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var chosen = Path.Combine(dir, "JOB.cnc");
|
|
var post = new CIFiberPostProcessor(MakeConfig());
|
|
|
|
var expected = post.GetOutputFiles(nest, chosen);
|
|
post.Post(nest, chosen);
|
|
|
|
Assert.Equal(
|
|
new[] { Path.Combine(dir, "JOB-1.cnc"), Path.Combine(dir, "JOB-2.cnc") },
|
|
expected
|
|
);
|
|
Assert.Equal(
|
|
expected.OrderBy(f => f),
|
|
Directory.GetFiles(dir).OrderBy(f => f)
|
|
);
|
|
|
|
var first = Lines(File.ReadAllText(expected[0]));
|
|
var second = Lines(File.ReadAllText(expected[1]));
|
|
|
|
Assert.Contains("V.E.X_SIZE = 120.000", first);
|
|
Assert.Contains("V.E.X_SIZE = 96.000", second);
|
|
Assert.Contains("V.E.Y_SIZE = 48.000", second);
|
|
Assert.Contains("( Sheet number - 1 )", first);
|
|
Assert.Contains("( Sheet number - 2 )", second);
|
|
|
|
foreach (var program in new[] { first, second })
|
|
{
|
|
// Restart labels start over in every program.
|
|
Assert.Contains("N1:", program);
|
|
Assert.DoesNotContain("N3:", program);
|
|
Assert.Single(program, "M50");
|
|
Assert.Equal(
|
|
new[] { "L PROGRAMEND.NC", "M50", "M30", "%", "" },
|
|
program.TakeLast(5)
|
|
);
|
|
}
|
|
Assert.Contains("G0X3Y3.5", second); // hole pierce at the sheet-2 part location
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_SingleProgram_ChangesPalletBetweenSheets()
|
|
{
|
|
var nest = MakeMultiSheetNest((60, 120), (60, 120));
|
|
var config = MakeConfig();
|
|
config.OneProgramPerSheet = false;
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var chosen = Path.Combine(dir, "JOB.cnc");
|
|
var post = new CIFiberPostProcessor(config);
|
|
|
|
Assert.Equal(new[] { chosen }, post.GetOutputFiles(nest, chosen));
|
|
post.Post(nest, chosen);
|
|
Assert.Equal(new[] { chosen }, Directory.GetFiles(dir));
|
|
|
|
var lines = Lines(File.ReadAllText(chosen));
|
|
Assert.Equal(3, lines.Count(l => l == "M50"));
|
|
Assert.Single(lines, "M30");
|
|
|
|
var sheet2 = lines.IndexOf("( Sheet number - 2 )");
|
|
Assert.Equal(
|
|
new[] { "( PART END )", "/L \"L0\"", "M50", "( Sheet number - 2 )" },
|
|
lines.Skip(sheet2 - 3).Take(4)
|
|
);
|
|
Assert.True(lines.IndexOf("( Sheet number - 3 )") > sheet2);
|
|
|
|
// Restart labels continue across sheets within one program.
|
|
Assert.Contains("N6:", lines);
|
|
Assert.Contains("V.E.R3=6", lines);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_SingleProgram_RejectsMixedSheetSizes_WritingNothing()
|
|
{
|
|
var nest = MakeMultiSheetNest((48, 96));
|
|
var config = MakeConfig();
|
|
config.OneProgramPerSheet = false;
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
new CIFiberPostProcessor(config).Post(nest, Path.Combine(dir, "JOB.cnc"))
|
|
);
|
|
Assert.Contains("One program per sheet", ex.Message);
|
|
Assert.Empty(Directory.GetFiles(dir));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_OneProgramPerSheet_OversizeSheet_WritesNoFiles()
|
|
{
|
|
var nest = MakeMultiSheetNest((60, 200)); // second sheet 200 long > 160.25 table
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
new CIFiberPostProcessor(MakeConfig()).Post(nest, Path.Combine(dir, "JOB.cnc"))
|
|
);
|
|
Assert.Contains("exceeds maximum table X", ex.Message);
|
|
Assert.Empty(Directory.GetFiles(dir));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_OneProgramPerSheet_SingleSheet_UsesChosenName()
|
|
{
|
|
var nest = MakeSquareWithHoleNest();
|
|
var dir = NewTempDir();
|
|
try
|
|
{
|
|
var chosen = Path.Combine(dir, "JOB.cnc");
|
|
var post = new CIFiberPostProcessor(MakeConfig());
|
|
|
|
Assert.Equal(new[] { chosen }, post.GetOutputFiles(nest, chosen));
|
|
post.Post(nest, chosen);
|
|
|
|
Assert.Equal(Post(nest), File.ReadAllText(chosen));
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Post_PalletChangeCode_IsConfigurable()
|
|
{
|
|
var custom = MakeConfig();
|
|
custom.PalletChangeCode = " M51 ";
|
|
var lines = Lines(Post(MakeSquareWithHoleNest(), custom));
|
|
Assert.Equal(new[] { "L PROGRAMEND.NC", "M51", "M30", "%", "" }, lines.TakeLast(5));
|
|
Assert.DoesNotContain("M50", lines);
|
|
|
|
var none = MakeConfig();
|
|
none.PalletChangeCode = "";
|
|
lines = Lines(Post(MakeSquareWithHoleNest(), none));
|
|
Assert.Equal(new[] { "L PROGRAMEND.NC", "M30", "%", "" }, lines.TakeLast(4));
|
|
}
|
|
|
|
[Fact]
|
|
public void PostToStream_MultiSheetPerSheetMode_Throws()
|
|
{
|
|
var nest = MakeMultiSheetNest((60, 120));
|
|
var ex = Assert.Throws<InvalidOperationException>(() => Post(nest));
|
|
Assert.Contains("file path", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void SavedConfigWithoutSheetSettings_DefaultsToPerSheetAndM50()
|
|
{
|
|
var config = System.Text.Json.JsonSerializer.Deserialize<CIFiberPostConfig>(
|
|
"""{ "MaxTableX": 120.0 }"""
|
|
)!;
|
|
|
|
Assert.True(config.OneProgramPerSheet);
|
|
Assert.Equal("M50", config.PalletChangeCode);
|
|
}
|
|
}
|