mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-12 11:21:11 -05:00
fix(posts): pick Cincinnati CL kerf side from the shared contour winding
The CL post wrote the configured default side (G41) on every contour, so a contour wound against OpenNest's convention was compensated into the part. It now takes each feature's side from Core's shared PostContours winding rule, as the CI Fiber post does: the perimeter is picked across the part's cut features and hole sub-program contours are always holes. Correctly wound drawings post unchanged. Remove the "Default kerf side" post setting; saved configs that still carry it load as before. Expose PostContours.Flatten and Classify so a post can classify one part's features itself. Add CL tests for a square with a hole: the exact inline program, cuts that match the shared contours on the sheet, G41/G42 keeping the part out of the kerf inline and through part sub-programs, and identical output from absolute and incremental part and hole programs. The replay now records the compensation in force on each cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd623ff1aa
commit
fa6f664296
12 files changed
+318
-42
No files matched your search
@@ -41,14 +41,25 @@ public static class PostContours
|
||||
/// </summary>
|
||||
public static List<PostContour> Extract(Program program, Vector origin)
|
||||
{
|
||||
var contours = new List<PostContour>();
|
||||
Flatten(program, origin, contours);
|
||||
var contours = Flatten(program, origin);
|
||||
Classify(contours);
|
||||
return contours;
|
||||
}
|
||||
|
||||
/// <summary>Picks the perimeter (marks excluded) and each contour's kerf side.</summary>
|
||||
private static void Classify(List<PostContour> contours)
|
||||
/// <summary>
|
||||
/// The program's contours in the <paramref name="origin"/> frame, with no perimeter or kerf
|
||||
/// side chosen yet. A post that splits one part across several calls gathers them here and
|
||||
/// classifies the whole part with <see cref="Classify"/>.
|
||||
/// </summary>
|
||||
public static List<PostContour> Flatten(Program program, Vector origin)
|
||||
{
|
||||
var contours = new List<PostContour>();
|
||||
FlattenInto(program, origin, contours);
|
||||
return contours;
|
||||
}
|
||||
|
||||
/// <summary>Picks one part's perimeter (marks excluded) and each contour's kerf side.</summary>
|
||||
public static void Classify(IReadOnlyList<PostContour> contours)
|
||||
{
|
||||
var perimeter = ContourWinding.PerimeterIndex(contours);
|
||||
for (var i = 0; i < contours.Count; i++)
|
||||
@@ -68,7 +79,7 @@ public static class PostContours
|
||||
/// suppressed moves starts a new contour pierced where the run ends, so the gap is crossed
|
||||
/// with the beam off rather than cut as a chord.
|
||||
/// </summary>
|
||||
private static void Flatten(Program program, Vector origin, List<PostContour> contours)
|
||||
private static void FlattenInto(Program program, Vector origin, List<PostContour> contours)
|
||||
{
|
||||
if (program == null)
|
||||
return;
|
||||
@@ -118,7 +129,7 @@ public static class PostContours
|
||||
// A hole call is its own contour.
|
||||
Flush();
|
||||
resumeAt = null;
|
||||
Flatten(call.Program, origin + call.Offset, contours);
|
||||
FlattenInto(call.Program, origin + call.Offset, contours);
|
||||
break;
|
||||
|
||||
case Motion motion:
|
||||
|
||||
@@ -13,7 +13,6 @@ public class CincinnatiFeatureWriterTests
|
||||
FeatureLineNumberStart = 1,
|
||||
UseAntiDive = true,
|
||||
KerfCompensation = KerfMode.ControllerSide,
|
||||
DefaultKerfSide = KerfSide.Left,
|
||||
InteriorM47 = M47Mode.Always,
|
||||
ExteriorM47 = M47Mode.Always,
|
||||
UseSpeedGas = false,
|
||||
@@ -112,8 +111,8 @@ public class CincinnatiFeatureWriterTests
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.ControllerSide;
|
||||
config.DefaultKerfSide = KerfSide.Left;
|
||||
var ctx = SimpleContext();
|
||||
ctx.Kerf = KerfType.Left;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G41", output);
|
||||
@@ -125,8 +124,8 @@ public class CincinnatiFeatureWriterTests
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.KerfCompensation = KerfMode.ControllerSide;
|
||||
config.DefaultKerfSide = KerfSide.Right;
|
||||
var ctx = SimpleContext();
|
||||
ctx.Kerf = KerfType.Right;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("G42", output);
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.Posting;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// A square with a hole through the CL post: the exact program, where the replayed G-code cuts
|
||||
/// against Core's shared contours, a kerf side that keeps the part out of the kerf in inline
|
||||
/// posting, part sub-programs and the hole sub-program, and the same program whichever mode
|
||||
/// the part and hole programs are stored in.
|
||||
/// </summary>
|
||||
public class CincinnatiKerfAndCoordinateTests
|
||||
{
|
||||
private static readonly Vector Location = new(1, 2);
|
||||
|
||||
/// <summary>
|
||||
/// A 10x10 square cut CW from (0,0) after a linear lead-in, and a radius-0.5 hole at (5,5)
|
||||
/// cut CCW as a hole call; <paramref name="reversed"/> winds both the other way.
|
||||
/// </summary>
|
||||
private static Nest SquareWithHole(Mode partMode = Mode.Absolute, Mode holeMode = Mode.Incremental,
|
||||
bool reversed = false)
|
||||
{
|
||||
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), Vector.Zero, reversed ? RotationType.CW : RotationType.CCW)
|
||||
{
|
||||
Layer = LayerType.Cut,
|
||||
});
|
||||
hole.Mode = holeMode;
|
||||
|
||||
var pgm = new Program(Mode.Absolute);
|
||||
pgm.Codes.Add(new SubProgramCall { Id = 1, Program = hole, Offset = new Vector(5, 5) });
|
||||
pgm.Codes.Add(new RapidMove(new Vector(0, -0.5)));
|
||||
pgm.Codes.Add(new LinearMove(new Vector(0, 0)) { Layer = LayerType.Leadin });
|
||||
var corners = reversed
|
||||
? new[] { new Vector(10, 0), new Vector(10, 10), new Vector(0, 10), Vector.Zero }
|
||||
: new[] { new Vector(0, 10), new Vector(10, 10), new Vector(10, 0), Vector.Zero };
|
||||
foreach (var corner in corners)
|
||||
pgm.Codes.Add(new LinearMove(corner) { Layer = LayerType.Cut });
|
||||
pgm.Mode = partMode;
|
||||
|
||||
var plate = new Plate(60, 120);
|
||||
plate.Parts.Add(new Part(new Drawing("square-hole", pgm), Location));
|
||||
var nest = new Nest("N");
|
||||
nest.Plates.Add(plate);
|
||||
return nest;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareWithHole_Inline_PostsTheExactProgram()
|
||||
{
|
||||
var output = CincinnatiReplay.Post(SquareWithHole(), partSubprograms: false);
|
||||
|
||||
var body = output[output.IndexOf("GOTO1( Goto Feature )", StringComparison.Ordinal)..];
|
||||
Assert.Equal(
|
||||
"""
|
||||
GOTO1( Goto Feature )
|
||||
N1 G52 X6 Y7
|
||||
( PART: square-hole )
|
||||
M98 P200
|
||||
G52 X0 Y0
|
||||
M47
|
||||
N1002 G0 X1 Y1.5
|
||||
(WARNING: No library found)
|
||||
G84
|
||||
G41 G1 X1 Y2 F#126
|
||||
G1 X1 Y12 F#148
|
||||
G1 X11 Y12
|
||||
G1 X11 Y2
|
||||
G1 X1 Y2
|
||||
G40
|
||||
M35
|
||||
M42
|
||||
M99 (END OF N.001)
|
||||
(*****************************************************)
|
||||
:200
|
||||
( PART: HOLE )
|
||||
N1 G0 X0 Y-0.6
|
||||
(WARNING: No library found)
|
||||
G84
|
||||
G41 G1 X0 Y-0.5 F#126
|
||||
G3 X0 Y-0.5 I0 J0.5 F[#148*#128]
|
||||
G40
|
||||
M35
|
||||
M99 (END OF HOLE)
|
||||
|
||||
""",
|
||||
body);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SquareWithHole_CutsTheSharedContoursOnTheSheet(bool partSubprograms)
|
||||
{
|
||||
var nest = SquareWithHole();
|
||||
var contours = PostContours.Extract(nest.Plates[0].Parts[0]);
|
||||
|
||||
var run = CincinnatiReplay.Run(CincinnatiReplay.Post(nest, partSubprograms));
|
||||
|
||||
AssertPoints(contours.Select(c => c.Pierce), run.PierceLog.Select(p => p.Point));
|
||||
var expected = contours.SelectMany(c => new[] { c.LeadIn }.Concat(c.Cuts)).ToList();
|
||||
Assert.Equal(expected.Count, run.Moves.Count);
|
||||
for (var i = 0; i < expected.Count; i++)
|
||||
{
|
||||
Assert.True(CincinnatiReplay.Near(expected[i].EndPoint, run.Moves[i].To),
|
||||
$"Move {i} ends at {run.Moves[i].To}, expected {expected[i].EndPoint}");
|
||||
if (expected[i] is ArcMove arc)
|
||||
{
|
||||
Assert.True(CincinnatiReplay.Near(arc.CenterPoint, run.Moves[i].Center!.Value));
|
||||
Assert.Equal(arc.Rotation == RotationType.CW, run.Moves[i].Clockwise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, "G41")]
|
||||
[InlineData(true, false, "G41")]
|
||||
[InlineData(false, true, "G42")]
|
||||
[InlineData(true, true, "G42")]
|
||||
public void KerfSide_KeepsThePartOutOfTheKerf(bool partSubprograms, bool reversed, string expected)
|
||||
{
|
||||
var run = CincinnatiReplay.Run(CincinnatiReplay.Post(SquareWithHole(reversed: reversed), partSubprograms));
|
||||
|
||||
Assert.Equal(2, run.Pierces);
|
||||
Assert.All(run.Moves, move => Assert.Equal(expected, move.Compensation));
|
||||
|
||||
// The first move after each pierce is the lead-in, which runs through scrap.
|
||||
var checkedCuts = 0;
|
||||
foreach (var move in run.Moves.Where((m, i) => i > 0 && run.Moves[i - 1].Pierce == m.Pierce))
|
||||
{
|
||||
AssertPartOnCompensatedSide(move);
|
||||
checkedCuts++;
|
||||
}
|
||||
Assert.Equal(5, checkedCuts);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, Mode.Absolute, Mode.Absolute)]
|
||||
[InlineData(false, Mode.Incremental, Mode.Absolute)]
|
||||
[InlineData(false, Mode.Incremental, Mode.Incremental)]
|
||||
[InlineData(true, Mode.Absolute, Mode.Absolute)]
|
||||
[InlineData(true, Mode.Incremental, Mode.Absolute)]
|
||||
[InlineData(true, Mode.Incremental, Mode.Incremental)]
|
||||
public void AbsoluteAndIncrementalPrograms_PostTheSameProgram(bool partSubprograms, Mode partMode, Mode holeMode)
|
||||
{
|
||||
var expected = WithoutDate(CincinnatiReplay.Post(SquareWithHole(Mode.Absolute, Mode.Incremental), partSubprograms));
|
||||
|
||||
var actual = WithoutDate(CincinnatiReplay.Post(SquareWithHole(partMode, holeMode), partSubprograms));
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
// The header's third line is the posting time.
|
||||
private static string WithoutDate(string output) =>
|
||||
string.Join('\n', output.Split('\n').Where((_, i) => i != 2));
|
||||
|
||||
/// <summary>
|
||||
/// Mid-line (or at an arc's start), the point just right of travel is in the part under G41
|
||||
/// and the point just left of it under G42; the other side is the kerf's.
|
||||
/// </summary>
|
||||
private static void AssertPartOnCompensatedSide(CincinnatiReplay.CutMove move)
|
||||
{
|
||||
Vector at, travel;
|
||||
if (move.Center is { } center)
|
||||
{
|
||||
at = move.From;
|
||||
var radial = move.From - center;
|
||||
travel = move.Clockwise ? new Vector(radial.Y, -radial.X) : new Vector(-radial.Y, radial.X);
|
||||
}
|
||||
else
|
||||
{
|
||||
at = (move.From + move.To) * 0.5;
|
||||
travel = move.To - move.From;
|
||||
}
|
||||
|
||||
var length = System.Math.Sqrt(travel.X * travel.X + travel.Y * travel.Y);
|
||||
var right = new Vector(travel.Y / length, -travel.X / length);
|
||||
var partSide = move.Compensation == "G41" ? right : new Vector(-right.X, -right.Y);
|
||||
|
||||
Assert.True(InPart(at + partSide * 0.01), $"Part is not on the compensated side at {at}");
|
||||
Assert.False(InPart(at - partSide * 0.01), $"Kerf side is in the part at {at}");
|
||||
}
|
||||
|
||||
private static bool InPart(Vector p) =>
|
||||
p.X > Location.X && p.X < Location.X + 10
|
||||
&& p.Y > Location.Y && p.Y < Location.Y + 10
|
||||
&& p.DistanceTo(Location + new Vector(5, 5)) > 0.5;
|
||||
|
||||
private static void AssertPoints(IEnumerable<Vector> expected, IEnumerable<Vector> actual)
|
||||
{
|
||||
var want = expected.ToList();
|
||||
var got = actual.ToList();
|
||||
Assert.Equal(want.Count, got.Count);
|
||||
for (var i = 0; i < want.Count; i++)
|
||||
Assert.True(CincinnatiReplay.Near(want[i], got[i]), $"Point {i} is {got[i]}, expected {want[i]}");
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,12 @@ internal static class CincinnatiReplay
|
||||
/// <summary>Every cutting move, start to end, in machine coordinates.</summary>
|
||||
public List<(Vector From, Vector To)> Cuts { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Every cutting move with the pierce it follows (1-based), its arc centre (null for
|
||||
/// a line), direction and the compensation in force: "G41", "G42" or null after G40.
|
||||
/// </summary>
|
||||
public List<CutMove> Moves { get; } = new();
|
||||
|
||||
/// <summary>Head raises posted while the head was already raised.</summary>
|
||||
public int DoubleRaises { get; set; }
|
||||
|
||||
@@ -50,6 +56,8 @@ internal static class CincinnatiReplay
|
||||
public int HeadDownRapids { get; set; }
|
||||
}
|
||||
|
||||
internal sealed record CutMove(int Pierce, Vector From, Vector To, Vector? Center, bool Clockwise, string? Compensation);
|
||||
|
||||
/// <summary>
|
||||
/// Minimal replay of the post's own dialect: M98/M99 calls, G52/G92 frames,
|
||||
/// G0-G3 under G90 with incremental I/J, and M47 head raises (a block-deleted
|
||||
@@ -74,6 +82,7 @@ internal static class CincinnatiReplay
|
||||
var cutSincePierce = false;
|
||||
string? raise = null; // raise posted since the last pierce
|
||||
var headDown = false;
|
||||
string? compensation = null;
|
||||
var pc = 0;
|
||||
|
||||
while (pc < lines.Count)
|
||||
@@ -89,6 +98,10 @@ internal static class CincinnatiReplay
|
||||
line = line[1..];
|
||||
|
||||
var words = Words(line);
|
||||
if (line.Contains("G41", System.StringComparison.Ordinal))
|
||||
compensation = "G41";
|
||||
else if (line.Contains("G42", System.StringComparison.Ordinal))
|
||||
compensation = "G42";
|
||||
if (words.TryGetValue("M", out var m))
|
||||
{
|
||||
if (m == "30")
|
||||
@@ -127,6 +140,9 @@ internal static class CincinnatiReplay
|
||||
|
||||
switch (g)
|
||||
{
|
||||
case "40":
|
||||
compensation = null;
|
||||
break;
|
||||
case "84":
|
||||
case "85":
|
||||
run.Pierces++;
|
||||
@@ -150,9 +166,11 @@ internal static class CincinnatiReplay
|
||||
case "3":
|
||||
{
|
||||
var target = g92 + g52 + new Vector(Num(words["X"]), Num(words["Y"]));
|
||||
Vector? arcCenter = null;
|
||||
if (g is "2" or "3")
|
||||
{
|
||||
var center = position + new Vector(Num(words["I"]), Num(words["J"]));
|
||||
arcCenter = center;
|
||||
var radius = position.DistanceTo(center);
|
||||
Assert.True(
|
||||
System.Math.Abs(target.DistanceTo(center) - radius) < 1e-3,
|
||||
@@ -167,7 +185,10 @@ internal static class CincinnatiReplay
|
||||
{
|
||||
cutSincePierce = true;
|
||||
if (pierced)
|
||||
{
|
||||
run.Cuts.Add((position, target));
|
||||
run.Moves.Add(new CutMove(run.Pierces, position, target, arcCenter, g == "2", compensation));
|
||||
}
|
||||
}
|
||||
position = target;
|
||||
break;
|
||||
|
||||
@@ -21,6 +21,12 @@ public sealed class FeatureContext
|
||||
/// <summary>True when this feature cuts the part's exterior (selects ExteriorM47).</summary>
|
||||
public bool IsExteriorFeature { get; set; }
|
||||
public bool IsEtch { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compensation that keeps the kerf in the scrap for the way this contour runs
|
||||
/// (<see cref="FeatureUtils.KerfSides"/>): Left posts G41, Right G42, None neither.
|
||||
/// </summary>
|
||||
public KerfType Kerf { get; set; } = KerfType.Left;
|
||||
public string LibraryFile { get; set; } = "";
|
||||
public double CutDistance { get; set; }
|
||||
public double SheetDiagonal { get; set; }
|
||||
@@ -148,13 +154,9 @@ public sealed class CincinnatiFeatureWriter
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Kerf compensation on first cutting move (skip for etch)
|
||||
if (
|
||||
!ctx.IsEtch
|
||||
&& !kerfEmitted
|
||||
&& _config.KerfCompensation == KerfMode.ControllerSide
|
||||
)
|
||||
if (!kerfEmitted && Compensates(ctx))
|
||||
{
|
||||
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 ");
|
||||
sb.Append(ctx.Kerf == KerfType.Left ? "G41 " : "G42 ");
|
||||
kerfEmitted = true;
|
||||
}
|
||||
|
||||
@@ -188,13 +190,9 @@ public sealed class CincinnatiFeatureWriter
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Kerf compensation on first cutting move (skip for etch)
|
||||
if (
|
||||
!ctx.IsEtch
|
||||
&& !kerfEmitted
|
||||
&& _config.KerfCompensation == KerfMode.ControllerSide
|
||||
)
|
||||
if (!kerfEmitted && Compensates(ctx))
|
||||
{
|
||||
sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 ");
|
||||
sb.Append(ctx.Kerf == KerfType.Left ? "G41 " : "G42 ");
|
||||
kerfEmitted = true;
|
||||
}
|
||||
|
||||
@@ -303,6 +301,9 @@ public sealed class CincinnatiFeatureWriter
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool Compensates(FeatureContext ctx) =>
|
||||
!ctx.IsEtch && ctx.Kerf != KerfType.None && _config.KerfCompensation == KerfMode.ControllerSide;
|
||||
|
||||
private Vector FindPiercePoint(List<ICode> codes)
|
||||
{
|
||||
foreach (var code in codes)
|
||||
|
||||
@@ -42,6 +42,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
/// <summary>
|
||||
/// Writes a complete part sub-program for the given normalized program.
|
||||
/// The program must be in absolute mode with coordinates normalized to origin (0,0).
|
||||
/// With <paramref name="holes"/> every contour is a hole, as in a hole sub-program.
|
||||
/// </summary>
|
||||
public void Write(
|
||||
TextWriter w,
|
||||
@@ -50,7 +51,8 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
int subNumber,
|
||||
string cutLibrary,
|
||||
string etchLibrary,
|
||||
double sheetDiagonal
|
||||
double sheetDiagonal,
|
||||
bool holes = false
|
||||
)
|
||||
{
|
||||
var allFeatures = FeatureUtils.SplitByRapids(normalizedProgram.Codes);
|
||||
@@ -60,6 +62,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
// Classify and order: etch features first, then cut features
|
||||
var ordered = FeatureUtils.ClassifyAndOrder(allFeatures);
|
||||
var exterior = FeatureUtils.FindExterior(ordered);
|
||||
var kerf = FeatureUtils.KerfSides(ordered, holes);
|
||||
|
||||
w.WriteLine("(*****************************************************)");
|
||||
w.WriteLine($":{subNumber}");
|
||||
@@ -90,6 +93,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
IsFirstFeatureOfPart = false,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsExteriorFeature = exterior[i],
|
||||
Kerf = kerf[i],
|
||||
IsEtch = isEtch,
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
|
||||
@@ -18,18 +18,6 @@ namespace OpenNest.Posts.Cincinnati
|
||||
PreApplied,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies which side of the cut line kerf compensation is applied to.
|
||||
/// </summary>
|
||||
public enum KerfSide
|
||||
{
|
||||
/// <summary>Kerf applied to the left side of the cut.</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Kerf applied to the right side of the cut.</summary>
|
||||
Right,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how the M47 head raise before each rapid is posted.
|
||||
/// </summary>
|
||||
@@ -209,11 +197,6 @@ namespace OpenNest.Posts.Cincinnati
|
||||
[PostSetting(KerfSection, 0)]
|
||||
public KerfMode KerfCompensation { get; set; } = KerfMode.ControllerSide;
|
||||
|
||||
[DisplayName("Default kerf side")]
|
||||
[Description("Default side for kerf compensation (Left or Right).")]
|
||||
[PostSetting(KerfSection, 1)]
|
||||
public KerfSide DefaultKerfSide { get; set; } = KerfSide.Left;
|
||||
|
||||
[DisplayName("Interior M47")]
|
||||
[Description("How M47 is posted after features other than a part's exterior, such as cutouts and etch.")]
|
||||
[PostSetting(HeadRaiseSection, 0)]
|
||||
|
||||
@@ -250,7 +250,8 @@ namespace OpenNest.Posts.Cincinnati
|
||||
subNum,
|
||||
initialCutLibrary,
|
||||
etchLibrary,
|
||||
sheetDiagonal
|
||||
sheetDiagonal,
|
||||
holes: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ public sealed class CincinnatiSheetWriter
|
||||
public List<ICode> Codes { get; init; }
|
||||
public bool IsEtch { get; init; }
|
||||
public bool IsExterior { get; init; }
|
||||
public KerfType Kerf { get; init; }
|
||||
public PartSubprogram Subprogram { get; init; }
|
||||
|
||||
/// <summary>First rapid target: the sub-program corner, else the first pierce.</summary>
|
||||
@@ -166,6 +167,7 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
var features = FeatureUtils.SplitAndClassify(part);
|
||||
var exterior = FeatureUtils.FindExterior(features);
|
||||
var kerf = FeatureUtils.KerfSides(features);
|
||||
for (var f = 0; f < features.Count; f++)
|
||||
{
|
||||
var (codes, isEtch) = features[f];
|
||||
@@ -178,6 +180,7 @@ public sealed class CincinnatiSheetWriter
|
||||
Codes = codes,
|
||||
IsEtch = isEtch,
|
||||
IsExterior = exterior[f],
|
||||
Kerf = kerf[f],
|
||||
Entry = start,
|
||||
Start = start,
|
||||
End = end,
|
||||
@@ -261,6 +264,7 @@ public sealed class CincinnatiSheetWriter
|
||||
IsFirstFeatureOfPart = isFirstFeatureOfPart,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsExteriorFeature = item.IsExterior,
|
||||
Kerf = item.Kerf,
|
||||
IsEtch = item.IsEtch,
|
||||
LibraryFile = item.IsEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = FeatureUtils.ComputeCutDistance(codes),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.Posting;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
@@ -239,6 +240,52 @@ public static class FeatureUtils
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each feature's kerf side from Core's shared winding rule (<see cref="PostContours"/>):
|
||||
/// the side that keeps the kerf in the scrap the way the contour actually runs. The part's
|
||||
/// perimeter is picked across all its cut features; with <paramref name="holes"/> every
|
||||
/// feature is a hole, as in a hole sub-program. Etch features and hole calls get
|
||||
/// <see cref="KerfType.None"/>: etch is never compensated and a hole sub-program picks its own.
|
||||
/// </summary>
|
||||
public static KerfType[] KerfSides(
|
||||
IReadOnlyList<(List<ICode> codes, bool isEtch)> features,
|
||||
bool holes = false
|
||||
)
|
||||
{
|
||||
var contours = new List<PostContour>();
|
||||
var owners = new List<int>();
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
{
|
||||
var (codes, isEtch) = features[i];
|
||||
if (isEtch || (codes.Count == 1 && codes[0] is SubProgramCall))
|
||||
continue;
|
||||
|
||||
var program = new Program(Mode.Absolute);
|
||||
program.Codes.AddRange(codes);
|
||||
foreach (var contour in PostContours.Flatten(program, Vector.Zero))
|
||||
{
|
||||
contours.Add(contour);
|
||||
owners.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (holes)
|
||||
{
|
||||
foreach (var contour in contours)
|
||||
contour.Kerf = ContourWinding.CompensationSide(contour, isPerimeter: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
PostContours.Classify(contours);
|
||||
}
|
||||
|
||||
// A feature is one contour; its first contour decides if it ever splits.
|
||||
var sides = new KerfType[features.Count];
|
||||
for (var k = contours.Count - 1; k >= 0; k--)
|
||||
sides[owners[k]] = contours[k].Kerf;
|
||||
return sides;
|
||||
}
|
||||
|
||||
private static Box CutBounds(List<ICode> codes)
|
||||
{
|
||||
var cut = new Program(Mode.Absolute);
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"UseAntiDive": true,
|
||||
"UseSmartRapids": false,
|
||||
"KerfCompensation": "ControllerSide",
|
||||
"DefaultKerfSide": "Left",
|
||||
"InteriorM47": "Always",
|
||||
"ExteriorM47": "Always",
|
||||
"M47OverrideDistanceThreshold": null,
|
||||
|
||||
@@ -104,7 +104,13 @@ block in a sheet or sub-program output follows this order
|
||||
5. `M130 (ANTI DIVE OFF)` — disable anti-dive, only if configured (§3.130).
|
||||
6. Contour moves:
|
||||
- `G41` (left) or `G42` (right) kerf compensation on the first cut move
|
||||
(§1.41 / §1.42), suppressed for etch features.
|
||||
(§1.41 / §1.42), suppressed for etch features. The side comes from Core's
|
||||
shared `PostContours` winding rule, the same one the CI Fiber post uses:
|
||||
whichever keeps the kerf in the scrap for the way the contour runs. Imported
|
||||
drawings (perimeter CW, holes CCW) get `G41` throughout; a contour wound the
|
||||
other way gets `G42`. The perimeter is the part's cut feature with the
|
||||
largest bounding box; contours in a hole sub-program are always holes.
|
||||
Paths that enclose no area, such as cut-off lines, keep `G41`.
|
||||
- `G1 X_ Y_ [F<feedvar>]` — linear cut move (§1.01). Feedrate references a
|
||||
machine variable such as `#148` and is emitted only when it changes.
|
||||
- `G2 X_ Y_ I_ J_ [F<feedvar>]` (CW) or `G3` (CCW) — arc (§1.02 / §1.03).
|
||||
|
||||
Reference in new issue
Block a user