mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 05:01:05 -04:00
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>
This commit is contained in:
1 parent
abfc1a714a
commit
63233db108
4 files changed
+189
-30
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;
|
||||
|
||||
@@ -12,7 +17,7 @@ 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
|
||||
/// 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()
|
||||
{
|
||||
|
||||
@@ -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 ...
|
||||
@@ -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
|
||||
@@ -86,7 +97,8 @@ 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:
|
||||
|
||||
|
||||
Reference in new issue
Block a user