mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-11 00:51:09 -05:00
The repo mixed variants such as center/centre and color/colour, so a name could be spelled either way and searches missed half the hits. Standardize on the US spelling, matching .NET's own APIs (Color, Center). Wire values owned by other systems keep their spelling: the material ledger's "Cancelled" hold status and GitHub's "cancelled" job status. No behavior change beyond the spelling of a few status messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
672 lines
26 KiB
C#
672 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Engine.Sequencing;
|
|
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 a Cincinnati machine sample NC: header block,
|
|
/// restart jump, N-labeled contour blocks with /L macro lines, G41
|
|
/// 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 centerd 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 center + (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]);
|
|
// CW perimeter: the part is right of travel, so the kerf goes left.
|
|
Assert.Equal("G41", 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]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End to end through the real cutting strategy, with a drawing wound
|
|
/// against OpenNest's convention (perimeter CCW, hole CW, as a drawing built
|
|
/// in code may be): the compensation must still keep the product on the side
|
|
/// the controller leaves it (G41 right of travel, G42 left), for the
|
|
/// perimeter and the hole alike, and each must get its own lead-in macro.
|
|
/// The wrong side under- or oversizes the cut by a kerf width.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Post_Compensation_KeepsPartOnProductSide_ForPerimeterAndHole()
|
|
{
|
|
var pgm = new Program(Mode.Absolute);
|
|
// Square drawn CCW, the opposite of OpenNest's CW perimeter convention.
|
|
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
|
|
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
|
|
// Hole r=1 at (5,5), drawn CW, the opposite of OpenNest's CCW hole convention.
|
|
pgm.Codes.Add(new RapidMove(new Vector(6, 5)));
|
|
pgm.Codes.Add(new ArcMove(new Vector(6, 5), new Vector(5, 5), RotationType.CW));
|
|
|
|
var location = new Vector(3, 4);
|
|
var plate = new Plate(60, 120);
|
|
plate.Parts.Add(new Part(new Drawing("rect-hole", pgm), location));
|
|
plate.CuttingParameters = new CuttingParameters
|
|
{
|
|
InternalLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
|
ExternalLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
|
};
|
|
TestLeadIns.Apply(plate);
|
|
|
|
var nest = new Nest("kerf") { Thickness = 0.06, Material = new Material("Mild Steel") };
|
|
nest.Plates.Add(plate);
|
|
var lines = Lines(Post(nest));
|
|
|
|
var holeCenter = location + new Vector(5, 5);
|
|
bool InMaterial(Vector p) =>
|
|
p.X > location.X && p.X < location.X + 10
|
|
&& p.Y > location.Y && p.Y < location.Y + 10
|
|
&& p.DistanceTo(holeCenter) > 1.0;
|
|
|
|
var checkedContours = 0;
|
|
for (var i = 0; i < lines.Count; i++)
|
|
{
|
|
if (lines[i] != "G41" && lines[i] != "G42")
|
|
continue;
|
|
|
|
// The lead-in ends on the contour; the first cut after /L "L6" sets
|
|
// the direction of travel there.
|
|
var start = ParseXY(lines[i + 1]);
|
|
var onPerimeter = start.DistanceTo(holeCenter) > 1.5;
|
|
Assert.Equal(onPerimeter ? "/L \"L4\"" : "/L \"L2\"", lines[i - 1]);
|
|
Assert.Equal("/L \"L6\"", lines[i + 2]);
|
|
var cut = lines[i + 3];
|
|
Vector travel;
|
|
if (cut.StartsWith("G1"))
|
|
{
|
|
travel = ParseXY(cut) - start;
|
|
}
|
|
else
|
|
{
|
|
var m = Regex.Match(cut, @"I(-?[\d.]+)J(-?[\d.]+)");
|
|
var radial = new Vector(
|
|
-double.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
-double.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture)
|
|
); // center -> start
|
|
travel = cut.StartsWith("G3")
|
|
? new Vector(-radial.Y, radial.X)
|
|
: new Vector(radial.Y, -radial.X);
|
|
}
|
|
|
|
var length = System.Math.Sqrt(travel.X * travel.X + travel.Y * travel.Y);
|
|
var right = new Vector(travel.Y / length, -travel.X / length);
|
|
if (lines[i] == "G42")
|
|
right = new Vector(-right.X, -right.Y); // G42 keeps the product on the left
|
|
Assert.True(InMaterial(start + right * 0.01), $"Part is not right of travel at {lines[i + 1]}");
|
|
Assert.False(InMaterial(start - right * 0.01), $"Kerf side is not scrap at {lines[i + 1]}");
|
|
checkedContours++;
|
|
}
|
|
|
|
Assert.Equal(2, checkedContours);
|
|
}
|
|
|
|
private static Vector ParseXY(string line)
|
|
{
|
|
var m = Regex.Match(line, @"X(-?[\d.]+)Y(-?[\d.]+)");
|
|
Assert.True(m.Success, line);
|
|
return new Vector(
|
|
double.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture),
|
|
double.Parse(m.Groups[2].Value, CultureInfo.InvariantCulture)
|
|
);
|
|
}
|
|
|
|
[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, center local (0,0.5) from the start -> (-0.5,0):
|
|
// center 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_PostsWithoutCompensationOrLeadIn()
|
|
{
|
|
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.Add(new Part(cutOff.Drawing, Vector.Zero));
|
|
|
|
var lines = Lines(Post(nest));
|
|
|
|
var n3 = lines.IndexOf("N3:");
|
|
Assert.True(n3 > lines.IndexOf("( PART:CutOff-V-20.00 )"));
|
|
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_CutOff_FollowsPlateSequence()
|
|
{
|
|
// Plate order is the cut sequence: part, cut-off, part. The cut-off is
|
|
// sequence 2, so it posts second, not after every part.
|
|
var nest = MakeSquareWithHoleNest();
|
|
var plate = nest.Plates[0];
|
|
plate.Parts.Add(new Part(plate.Parts[0].BaseDrawing, new Vector(40, 2)));
|
|
|
|
var cutOff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
|
|
cutOff.Regenerate(plate, new CutOffSettings());
|
|
plate.Parts.Insert(1, new Part(cutOff.Drawing, Vector.Zero));
|
|
|
|
var lines = Lines(Post(nest));
|
|
|
|
Assert.Equal(
|
|
new[] { "( PART:square-hole )", "( PART:CutOff-V-20.00 )", "( PART:square-hole )" },
|
|
lines.Where(l => l.StartsWith("( PART:")).ToArray()
|
|
);
|
|
|
|
// Part number and restart label run in the same order: the first
|
|
// part's hole and perimeter are N1/N2, so the cut-off is N3.
|
|
var cutOffBlock = lines.IndexOf("( PART:CutOff-V-20.00 )");
|
|
Assert.Equal("( Part #2 )", lines[cutOffBlock - 1]);
|
|
Assert.Equal("V.E.R4=2", lines[cutOffBlock + 1]);
|
|
Assert.Equal("N3:", lines[cutOffBlock + 2]);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|