mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
205d066762 | ||
|
|
391e57a479 | ||
|
|
41ad75d385 | ||
|
|
122f9a9e47 |
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>
|
||||
/// ( <nest name> )
|
||||
/// ( CONFIGURATION - CI FIBER 8K )
|
||||
/// ( August 20, 2026 01:10 PM )
|
||||
/// ( <month d, yyyy hh:mm AM|PM> )
|
||||
/// ( 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<n>: /L "L0" V.E.R3=<n> 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user