4 Commits
Author SHA1 Message Date
aj 205d066762 docs(posts): describe CI Fiber M50 as Table In, the pallet swap
The pallet-change setting and post doc said M50 moves the cut sheet out,
borrowed from the CL-series manual. Cincinnati's CNC Codes Handbook calls
M50 "Table In" and M51 "Table Out". On the CI Fiber shuttle, Table In brings
the waiting pallet into the laser, which sends the cut one out, so M50 is
still the swap. Say that, note that it expects the waiting pallet to be
loaded, and note that newer Cincinnati samples end without a pallet code.
Default (M50) and posted output are unchanged.
2026-10-21 15:32:14 -04:00
aj 391e57a479 chore(posts): stop naming the CI Fiber customer sample part
The Cincinnati sample used by the CI Fiber tests is a customer production
part. Its geometry and NC were never tracked, but comments named its part
number and fixture file names and quoted its date and a pierce coordinate.
Replace them with generic wording and placeholder values; the fixture stays
outside the repository, configured only in the gitignored test-config.json.
Comment-only change.
2026-10-21 15:32:13 -04:00
aj 41ad75d385 test(posts): post the CI Fiber sample's own lead-ins and match it line for line
The sample regression fixture now carries each part's pierce, lead-in and
cut moves from the Cincinnati machine program as its saved cutting program.
A new test posts those saved programs without reassigning lead-ins and
requires the program body to equal the sample exactly: L2/L4 macro, G41/G42
side, lead-ins and every cut move (21,911 lines).

Mutation checks: dropping the largest-contour perimeter rule from the kerf
fix fails it at the first perimeter (L4 posted as L2), and forcing G41 fails
it at the first perimeter's G42. The existing tests still replace the saved
programs with OpenNest's own lead-ins first; their comment now says so
instead of describing the sample's leads wrongly.
2026-10-21 15:32:13 -04:00
ajandClaude Opus 5.5 122f9a9e47 fix(posts): put the CI Fiber kerf on the scrap side for either winding
The CI Fiber post selected G42 for every perimeter. Cincinnati's CNC Codes
Handbook defines G42 as "product remains on the left", and imported
perimeters run clockwise with the part on the right, so the controller
offset every outside edge into the part: parts came out a kerf undersize.
Cincinnati's LaserNST samples only use G42 because they cut perimeters
counter-clockwise.

Winding is normalized only at DXF import, so the post can't assume a
direction either. It now takes the largest contour as the perimeter (as
ShapeProfile does) and picks G41 or G42 from each contour's actual winding.
That also stops a reversed drawing from swapping the L2/L4 lead-in macros.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:32:13 -04:00
7 changed files with 277 additions and 56 deletions

No files matched your search

@@ -1,9 +1,14 @@
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;
@@ -11,8 +16,8 @@ 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
/// structure of a Cincinnati machine sample NC: header block,
/// restart jump, N-labelled contour blocks with /L macro lines, G41
/// selection, trimmed coordinate format and the M50/M30/% tail.
/// </summary>
public class CIFiberPostProcessorTests
@@ -131,7 +136,8 @@ public class CIFiberPostProcessorTests
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]);
// 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]);
@@ -149,6 +155,101 @@ public class CIFiberPostProcessorTests
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 },
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
};
new LeadInAssigner { Sequencer = new LeftSideSequencer() }.Assign(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)
); // centre -> 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()
{
@@ -16,14 +16,20 @@ using OpenNest.Posts.CincinnatiCIFiber;
namespace OpenNest.Tests.CincinnatiCIFiber;
/// <summary>
/// Regression against the real machine program: posts the reconstructed
/// 12992-4SS nest (same part + layout as the sample NC) and compares contour
/// counts, macro counts and perimeter geometry with 12992-4SS_NEST.nc.
/// Regression against a real machine program: posts a nest reconstructed from
/// a Cincinnati sample NC (same part and layout) and compares contour counts,
/// macro counts and perimeter geometry with that NC. The sample is a customer
/// production part, so the fixture stays outside the repository and is not
/// named here.
///
/// The fixture lives outside the repository (/srv/shared). Configure it in
/// OpenNest.Tests/test-config.json:
/// { "CIFiberSampleNcPath": "/srv/shared/.../12992-4SS_NEST.nc",
/// "CIFiberReconstructedNestPath": "/srv/shared/.../12992-4SS_NEST_reconstructed.nest" }
/// Each placed part in the fixture carries the sample's own pierce, lead-in
/// and cut moves as its saved cutting program (not locked). Posting those
/// saved programs must reproduce the sample move for move; the other tests
/// replace them with OpenNest's own lead-ins first.
///
/// Configure the fixture in OpenNest.Tests/test-config.json (gitignored):
/// { "CIFiberSampleNcPath": "<path to the sample>.nc",
/// "CIFiberReconstructedNestPath": "<path to the reconstructed>.nest" }
/// Both must exist for the test to run; otherwise it skips.
/// </summary>
public class CIFiberSampleRegressionTests
@@ -48,10 +54,11 @@ public class CIFiberSampleRegressionTests
var plate = nest.Plates[0];
plate.CuttingParameters = new CuttingParameters
{
// The sample cuts clean holes with a short perpendicular linear
// lead (pierce ~0.079 outside the contour), including the circles
// (ArcCircleLeadIn). TF5200 13.2.4.1 requires a LINEAR first
// motion block after G41/G42, so holes must have linear leads.
// OpenNest's own short perpendicular linear leads, including the
// circles (ArcCircleLeadIn). TF5200 13.2.4.1 requires a LINEAR
// first motion block after G41/G42, so holes must have linear
// leads. (The sample itself pierces holes at their centre and
// uses 0.125 in perimeter leads; see the saved-lead-in test.)
InternalLeadIn = new LineLeadIn { Length = 0.075, ApproachAngle = 90 },
ExternalLeadIn = new LineLeadIn { Length = 0.075, ApproachAngle = 90 },
ArcCircleLeadIn = new LineLeadIn { Length = 0.075, ApproachAngle = 90 },
@@ -96,6 +103,49 @@ public class CIFiberSampleRegressionTests
return result;
}
/// <summary>
/// The program body from the restart label through the last part: every
/// line except the per-part source-path comment, which names the
/// original CAM part file.
/// </summary>
private static List<string> ProgramBody(string nc)
{
var lines = nc.Replace("\r\n", "\n").Split('\n').ToList();
var start = lines.IndexOf("N0:");
var end = lines.LastIndexOf("( PART END )");
Assert.True(start >= 0 && end > start, "No N0: ... ( PART END ) body found");
return lines
.Skip(start)
.Take(end - start + 1)
.Where(line => !line.StartsWith("( PART:", StringComparison.Ordinal))
.ToList();
}
[SkippableFact]
public void Post_ReconstructedNest_SavedSampleLeadIns_ReproduceSampleProgram()
{
var fixture = ResolveFixture();
Skip.If(fixture == null, "CI Fiber fixtures not configured in test-config.json");
// Post the saved programs as they are: no lead-in assignment.
var reader = new NestReader(fixture.Value.Nest);
var nest = reader.Read();
Assert.Empty(reader.Warnings);
Assert.All(nest.Plates.SelectMany(plate => plate.Parts), part => Assert.True(part.HasManualLeadIns));
var expected = ProgramBody(File.ReadAllText(fixture.Value.Nc));
var actual = ProgramBody(PostNest(nest));
// Pierce points, L2/L4 macro, G41/G42 side, lead-ins and every cut
// move line up with the machine program exactly.
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
Assert.True(
expected[i] == actual[i],
$"Line {i} after N0: differs: sample '{expected[i]}', posted '{actual[i]}'"
);
}
[SkippableFact]
public void Post_ReconstructedNest_IsIdenticalAfterSavedLeadInsReload()
{
@@ -6,7 +6,7 @@ namespace OpenNest.Posts.CincinnatiCIFiber
/// <summary>
/// Formats numeric values for the CI Fiber machine program.
/// Coordinates: up to <see cref="PostedAccuracy"/> decimals with trailing
/// zeros trimmed ("3.706", "5.01", "0"), never "-0" (sample NC format).
/// zeros trimmed ("1.234", "5.01", "0"), never "-0" (sample NC format).
/// Header values (V.E.*) use fixed decimals.
/// </summary>
public sealed class CIFiberFormatter
@@ -114,15 +114,17 @@ namespace OpenNest.Posts.CincinnatiCIFiber
/// <summary>
/// Line written to swap pallets: after the last sheet, and between sheets
/// in a single program. M50 is the Cincinnati pallet change (machine sample
/// tail, CL-series manual EM-423 §3.50; confirmed by the machine owner): it
/// moves the cut sheet out for unloading before M30. Blank writes no pallet
/// change.
/// in a single program. Cincinnati's CNC Codes Handbook calls M50 "Table In":
/// the waiting pallet moves into the laser, which sends the cut pallet out
/// for unloading (machine sample tail; reading confirmed by the machine
/// owner). It expects the waiting pallet to be loaded. M51 ("Table Out")
/// only sends the pallet out. Blank writes no pallet change.
/// </summary>
[DisplayName("Pallet change code")]
[Description(
"Written after each sheet to swap pallets, moving the cut sheet out for unloading. "
+ "Cincinnati uses M50. Blank writes no pallet change."
"Written after each sheet to swap pallets. Cincinnati's M50 (Table In) brings the "
+ "waiting pallet into the laser and sends the cut sheet out, so load the waiting "
+ "pallet first. Blank writes no pallet change."
)]
[PostSetting(SheetsSection, 1)]
public string PalletChangeCode { get; set; } = "M50";
@@ -17,7 +17,7 @@ namespace OpenNest.Posts.CincinnatiCIFiber
/// <code>
/// ( &lt;nest name&gt; )
/// ( CONFIGURATION - CI FIBER 8K )
/// ( August 20, 2026 01:10 PM )
/// ( &lt;month d, yyyy hh:mm AM|PM&gt; )
/// ( Material = Mild Steel .060 )
/// V.E.MATERIAL = "MSN" ... V.E.UNIT = 1
/// G90
@@ -29,7 +29,8 @@ namespace OpenNest.Posts.CincinnatiCIFiber
/// ... (single-program mode only: /L "L0", pallet change, next sheet)
/// ( Part #k ) ( PART:... ) V.E.R4=k
/// N&lt;n&gt;: /L "L0" V.E.R3=&lt;n&gt; G0X..Y..
/// /L "L2" + G41 | /L "L4" + G42
/// /L "L2" (hole) | /L "L4" (perimeter), then G41 | G42
/// (whichever puts the kerf in the scrap)
/// G1X..Y.. (linear lead-in)
/// /L "L6" (cut layer on)
/// G1/G2/G3 ...
@@ -42,7 +43,7 @@ namespace OpenNest.Posts.CincinnatiCIFiber
/// %
/// </code>
///
/// Coordinates are spaceless (G1X3.706Y47.488); arcs post I/J incremental
/// Coordinates are spaceless (G1X1.234Y5.678); arcs post I/J incremental
/// from the arc start (TF5200 G162 default). The first motion block after
/// the G41/G42 selection is always LINEAR (TF5200 §13.2.4.1): contours
/// without a lead-in move are rejected with a clear error.
@@ -222,23 +223,22 @@ namespace OpenNest.Posts.CincinnatiCIFiber
contours = contours.Where(c => !IsScribeContour(c)).ToList();
var isCutOff = part.BaseDrawing.IsCutOff;
foreach (var contour in contours)
var perimeter = CIFiberWinding.PerimeterIndex(contours);
for (var i = 0; i < contours.Count; i++)
{
contourNumber++;
if (isCutOff)
WriteCutOffContour(contour, contourNumber, w);
WriteCutOffContour(contours[i], contourNumber, w);
else
WriteContour(contour, contourNumber, w);
WriteContour(contours[i], contours[i].IsExterior || i == perimeter, contourNumber, w);
}
CIFiberFormatter.Line(w, "( PART END )");
return contourNumber;
}
private void WriteContour(CIFiberContour contour, int contourNumber, TextWriter w)
private void WriteContour(CIFiberContour contour, bool isExterior, int contourNumber, TextWriter w)
{
var isExterior = contour.IsExterior || CIFiberWinding.IsExterior(contour);
CIFiberFormatter.Line(w, $"N{contourNumber}:");
CIFiberFormatter.Line(w, SkippableLine(_config.LayerCancel));
CIFiberFormatter.Line(w, $"V.E.R3={contourNumber}");
@@ -264,9 +264,12 @@ namespace OpenNest.Posts.CincinnatiCIFiber
isExterior ? _config.LayerExteriorLeadin : _config.LayerInteriorLeadin
)
);
// G41 leaves the product right of travel, G42 left (Cincinnati CNC
// handbook). Pick the side that puts the kerf in the scrap for the
// way this contour actually runs.
CIFiberFormatter.Line(
w,
isExterior ? "G42" : "G41"
CIFiberWinding.CompensatesLeft(contour, isExterior) ? "G41" : "G42"
);
var prev = contour.LeadIn.EndPoint;
@@ -6,17 +6,16 @@ using OpenNest.Geometry;
namespace OpenNest.Posts.CincinnatiCIFiber
{
/// <summary>
/// Classifies a flattened contour as interior or exterior from the
/// material side of its cut path, so the post picks the G41 (inside, kerf
/// left of travel) vs G42 (outside, kerf right of travel) lead-in macro to
/// keep the kerf on the scrap side, matching the machine sample (holes cut
/// CCW + G41, perimeter cut CCW + G42).
/// Works out, from a part's flattened contours, which one is the perimeter
/// (L4 lead-in macro, the rest are holes on L2) and which compensation
/// side keeps the kerf in the scrap.
///
/// OpenNest contours all keep material on the left of travel (see
/// OffsetSide / ContourCuttingStrategy): the perimeter runs clockwise
/// (negative shoelace area) and holes run counter-clockwise (positive).
/// A contour path that is not closed classifies as interior — a defensive
/// default; open paths should not reach the post.
/// G41 leaves the product right of travel. Imported drawings are wound
/// perimeter CW and holes CCW (ShapeProfile.NormalizeWinding), which puts
/// the part on the right and gives G41 throughout; a drawing that reaches
/// the post wound the other way (built in code or edited by hand) needs G42.
/// Paths that enclose no area (open break cuts) count as holes with G41,
/// a defensive default.
/// </summary>
public static class CIFiberWinding
{
@@ -42,15 +41,59 @@ namespace OpenNest.Posts.CincinnatiCIFiber
}
/// <summary>
/// True when the contour is a closed CW path — an external perimeter
/// under the OpenNest material-left convention.
/// Index of the part's perimeter: the contour enclosing the largest
/// bounding box, as ShapeProfile picks it. -1 when no contour encloses
/// any area.
/// </summary>
public static bool IsExterior(CIFiberContour contour)
public static int PerimeterIndex(IReadOnlyList<CIFiberContour> contours)
{
if (!IsClosed(contour))
return false;
var best = -1;
var bestArea = 0.0;
for (var i = 0; i < contours.Count; i++)
{
if (!EnclosesArea(contours[i]))
continue;
return SignedArea(contour) < 0.0;
var area = BoundingBoxArea(SamplePoints(contours[i]));
if (best < 0 || area > bestArea)
{
best = i;
bestArea = area;
}
}
return best;
}
/// <summary>
/// True when G41 (product right of travel) keeps the kerf in the scrap:
/// a CW perimeter or a CCW hole. False calls for G42.
/// </summary>
public static bool CompensatesLeft(CIFiberContour contour, bool isPerimeter)
{
if (!EnclosesArea(contour))
return true;
var clockwise = SignedArea(contour) < 0.0;
return clockwise == isPerimeter;
}
private static bool EnclosesArea(CIFiberContour contour) =>
System.Math.Abs(SignedArea(contour)) > 1e-9;
private static double BoundingBoxArea(List<Vector> pts)
{
double minX = double.MaxValue, minY = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue;
foreach (var p in pts)
{
minX = System.Math.Min(minX, p.X);
minY = System.Math.Min(minY, p.Y);
maxX = System.Math.Max(maxX, p.X);
maxY = System.Math.Max(maxY, p.Y);
}
return (maxX - minX) * (maxY - minY);
}
/// <summary>True when the cut path returns to its start point.</summary>
+31 -9
View File
@@ -6,7 +6,9 @@ manual or a machine-operation guide. For the separate CL-series post, see
Controller reference used during development: Beckhoff *TF5200 | TwinCAT 3 CNC
Programming manual*, version 1.33, May 19, 2026
(`TF5200_programming_manual_en.pdf`). Obtain the applicable documentation from
(`TF5200_programming_manual_en.pdf`), and Cincinnati's *CNC Codes Handbook*
(Programming Manual, rev 1.0, January 12, 2023) with LaserNST CI FIBER 8K sample
programs, supplied by Cincinnati on October 19, 2026. Obtain the applicable documentation from
Beckhoff and Cincinnati. Vendor PDFs and full-text extracts stay outside source
control; redistribution permission has not been established.
@@ -21,9 +23,18 @@ uses the Cincinnati machine-sample convention, not a generic TF5200 laser API:
- Parts follow plate order. `V.E.R4` identifies the part within its sheet;
numbered contour labels and `V.E.R3` count up through the program for restart
lookup (they restart at 1 in each per-sheet program).
- Each contour: `/L "L0"`, restart number, rapid to pierce, then interior
`/L "L2"` with `G41` or exterior `/L "L4"` with `G42`; linear lead-in,
- Each contour: `/L "L0"`, restart number, rapid to pierce, then `/L "L2"`
for a hole or `/L "L4"` for the perimeter, `G41` or `G42`, linear lead-in,
`/L "L6"`, cutting moves, and `/L "ZHSOFF"`.
- The perimeter is the part's contour with the largest bounding box, as
`ShapeProfile` picks it; the others are holes. Compensation follows the
Cincinnati CNC Codes Handbook (rev 1.0, G-codes): `G41` leaves the product
right of travel and `G42` leaves it left. The post picks whichever puts the
kerf in the scrap for the way each contour runs: imported drawings (perimeter
CW, holes CCW) get `G41` throughout, and a contour wound the other way gets
`G42`. Cincinnati's LaserNST samples cut both perimeter and holes CCW, so they
use `G42` on the perimeter and `G41` on holes, the same kerf sides. Paths that
enclose no area count as holes with `G41`.
- Tail: `/L "L0"`, `L PROGRAMEND.NC`, the pallet-change code (`M50`), `M30`,
and `%`.
- Motion endpoints are sheet-absolute XY. Arc `I`/`J` are offsets from the arc
@@ -70,10 +81,14 @@ validated and saved only when OK is pressed.
change separate sheets, and the header carries the first sheet's size. Sheets
of different sizes are rejected in this mode. Posting to a stream always
produces one program, so it rejects a multi-sheet nest in per-sheet mode.
- `PalletChangeCode` defaults to `M50`, the Cincinnati pallet change: the machine
sample ends `L PROGRAMEND.NC`, `M50`, `M30`, moving the cut sheet out for
unloading before the program ends (CL-series manual EM-423 §3.50; confirmed by
the machine owner).
- `PalletChangeCode` defaults to `M50`, as in the machine sample, which ends
`L PROGRAMEND.NC`, `M50`, `M30`. Cincinnati's CNC Codes Handbook calls M50
"Table In" and M51 "Table Out". On the CI Fiber the head stays at a fixed height
and the pallets shuffle and lift to swap. Table In moves the waiting pallet into
the laser, which sends the cut pallet out for unloading, so M50 is the swap
(reading confirmed by the machine owner). It expects the waiting pallet to be
loaded. Newer Cincinnati samples end with `M30` and no pallet code; clear
the setting to match.
- **Release blocker:** the single-program sequence between sheets (`/L "L0"`, then
`M50`, then the next sheet with no new header or start macro) is inferred, not
observed. Confirm it against a Cincinnati multi-sheet sample before release, or
@@ -86,9 +101,16 @@ validated and saved only when OK is pressed.
## Verification
[Output-contract tests](../OpenNest.Tests/CincinnatiCIFiber/CIFiberPostProcessorTests.cs)
cover a square with a hole, coordinate transforms/formatting, lead-in rejection,
cover a square with a hole, kerf side for either winding, coordinate
transforms/formatting, lead-in rejection,
cut-off sequencing and output, table bounds, suppression/scribe handling, and
multi-sheet output in both modes. Run:
multi-sheet output in both modes.
[Sample regression tests](../OpenNest.Tests/CincinnatiCIFiber/CIFiberSampleRegressionTests.cs)
post a nest rebuilt from a Cincinnati machine sample, whose parts carry the
sample's own pierce, lead-in and cut moves. Posted unchanged, the program body
must match the sample line for line (macro, G41/G42 side, lead-ins and cuts).
The fixture stays outside the repository and the tests skip when
`test-config.json` does not point at it. Run:
```sh
dotnet test OpenNest.Tests/OpenNest.Tests.csproj --filter FullyQualifiedName~CincinnatiCIFiber