mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 02:41:06 -04:00
fix(posts): Cincinnati CL sub-program frames, head raises and dead settings
Part sub-program calls restored the sheet frame with G92 at the part corner while the tool sat at the sub-program's last endpoint, shifting every later part. The call now raises the head, rapids back to local 0,0 and restores there. Parts share a sub-program only when their posted toolpaths match, so per-part lead-ins are kept. Suppressed moves are left uncut: the cut resumes with a rapid past them. Head raises go before the rapid that leaves a feature, one per rapid: the safety raise now precedes the rapid to a new part instead of following its first feature, and sub-program mode no longer posts M47 then M47 P2000. M47Mode.Auto posts M47 before rapids longer than M47OverrideDistanceThreshold and /M47 otherwise. ExteriorM47 applies to the contour spanning the part's cut extents. The sheet GOTO targets FeatureLineNumberStart and is omitted without line numbers. PrepareForNest pre-selects only the gas- and thickness-matching library. Part programs are read through absolute copies, never switched to absolute in place. Removes the unimplemented UseSheetSubprograms, CoordModeBetweenParts and ProcessParameterMode (G89Mode); old configs still load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5dc13a03ae
commit
2b3f8e7b52
16 files changed
+1203
-544
No files matched your search
@@ -14,7 +14,6 @@ public class CincinnatiFeatureWriterTests
|
||||
UseAntiDive = true,
|
||||
KerfCompensation = KerfMode.ControllerSide,
|
||||
DefaultKerfSide = KerfSide.Left,
|
||||
ProcessParameterMode = G89Mode.LibraryFile,
|
||||
InteriorM47 = M47Mode.Always,
|
||||
ExteriorM47 = M47Mode.Always,
|
||||
UseSpeedGas = false,
|
||||
@@ -303,7 +302,6 @@ public class CincinnatiFeatureWriterTests
|
||||
public void G89_EmittedWithLibraryFile()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.ProcessParameterMode = G89Mode.LibraryFile;
|
||||
var ctx = SimpleContext();
|
||||
ctx.LibraryFile = "MILD10";
|
||||
ctx.CutDistance = 18.0;
|
||||
@@ -317,7 +315,6 @@ public class CincinnatiFeatureWriterTests
|
||||
public void G89_WarningEmittedWhenNoLibrary()
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.ProcessParameterMode = G89Mode.LibraryFile;
|
||||
var ctx = SimpleContext();
|
||||
ctx.LibraryFile = "";
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
using static OpenNest.Tests.Cincinnati.CincinnatiReplay;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// Posts off-origin parts of several drawings and replays the G-code to check what the
|
||||
/// machine actually does: where it cuts, what it leaves uncut and where it raises the head.
|
||||
/// </summary>
|
||||
public class CincinnatiPostRegressionTests
|
||||
{
|
||||
[Fact]
|
||||
public void PartSubprograms_CutEveryPartWhereInlinePostingDoes()
|
||||
{
|
||||
// Outer square first, so each part's toolpath ends away from its corner.
|
||||
var nest = SquaresNest(
|
||||
OuterFirst,
|
||||
("A", new Vector(5, 5)),
|
||||
("B", new Vector(30, 20)),
|
||||
("A", new Vector(50, 60))
|
||||
);
|
||||
|
||||
var inline = Run(Post(nest, partSubprograms: false));
|
||||
var grouped = Run(Post(nest, partSubprograms: true));
|
||||
|
||||
var expected = nest.Plates[0].Parts
|
||||
.SelectMany(p => new[] { p.Location, p.Location + new Vector(2, 2) })
|
||||
.ToList();
|
||||
AssertPoints(expected, inline.PierceLog.Select(p => p.Point).ToList());
|
||||
AssertSamePath(inline, grouped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartSubprograms_PartsWithOwnLeadIns_PostTheirOwnToolpath()
|
||||
{
|
||||
var nest = new Nest("N");
|
||||
var plate = new Plate(48, 96);
|
||||
nest.Plates.Add(plate);
|
||||
var drawing = new Drawing("A") { Program = HoledSquare() };
|
||||
AddLeadInPart(plate, drawing, 0.25, new Vector(5, 5));
|
||||
AddLeadInPart(plate, drawing, 0.75, new Vector(20, 5));
|
||||
AddLeadInPart(plate, new Drawing("B") { Program = HoledSquare() }, 0.25, new Vector(5, 30));
|
||||
AddLeadInPart(plate, drawing, 0.25, new Vector(20, 30));
|
||||
|
||||
var output = Post(nest, partSubprograms: true);
|
||||
|
||||
// Lead-ins differ between the two (drawing, rotation) twins; the identical one shares.
|
||||
Assert.Equal(2, Count(output, "M99 (END OF A)"));
|
||||
Assert.Equal(1, Count(output, "M99 (END OF B)"));
|
||||
AssertSamePath(Run(Post(nest, partSubprograms: false)), Run(output));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SuppressedMoves_AreLeftUncut(bool partSubprograms)
|
||||
{
|
||||
var suppressed = SquaresProgram();
|
||||
((Motion)suppressed.Codes[7]).Suppressed = true; // outer (10,0) -> (10,10)
|
||||
var nest = new Nest("N");
|
||||
var plate = new Plate(48, 96);
|
||||
nest.Plates.Add(plate);
|
||||
var a = AddPart(plate, "A", suppressed, new Vector(5, 5));
|
||||
AddPart(plate, "B", SquaresProgram(), new Vector(30, 20));
|
||||
|
||||
var run = Run(Post(nest, partSubprograms));
|
||||
|
||||
Assert.DoesNotContain(run.Cuts, c => Near(c.To, a.Location + new Vector(10, 10)) && Near(c.From, a.Location + new Vector(10, 0)));
|
||||
Assert.DoesNotContain(run.Cuts, c => Near(c.From, a.Location + new Vector(10, 0)) && !Near(c.To, a.Location + new Vector(10, 0)));
|
||||
Assert.Contains(run.PierceLog, p => Near(p.Point, a.Location + new Vector(10, 10)));
|
||||
Assert.Equal(15, run.Cuts.Count);
|
||||
Assert.Equal(0, run.EmptyPierces);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SafetyHeadraise_IsTheOneRaiseBeforeTheRapidToADifferentPart(bool partSubprograms)
|
||||
{
|
||||
var nest = SquaresNest(("A", new Vector(5, 5)), ("A", new Vector(20, 5)), ("B", new Vector(40, 30)));
|
||||
var b = nest.Plates[0].Parts[2];
|
||||
|
||||
var run = Run(Post(nest, partSubprograms));
|
||||
|
||||
Assert.Equal(0, run.DoubleRaises);
|
||||
Assert.Equal(0, run.HeadDownRapids);
|
||||
var safety = Assert.Single(run.PierceLog, p => p.Raise == "M47 P2000");
|
||||
Assert.True(Near(safety.Point, b.Location + new Vector(2, 2)), $"Safety raise before {safety.Point}");
|
||||
Assert.All(run.PierceLog.Where(p => p.Raise != "M47 P2000"), p => Assert.Equal("M47", p.Raise));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SheetGoto_TargetsThePostedFirstFeatureNumber(bool partSubprograms)
|
||||
{
|
||||
var nest = SquaresNest(("A", new Vector(5, 5)), ("B", new Vector(30, 20)));
|
||||
var config = Config(partSubprograms);
|
||||
config.FeatureLineNumberStart = 5;
|
||||
|
||||
var numbered = Sheet(Post(nest, config));
|
||||
config.UseLineNumbers = false;
|
||||
var unnumbered = Sheet(Post(nest, config));
|
||||
|
||||
Assert.Contains("GOTO5( Goto Feature )", numbered);
|
||||
Assert.Contains(numbered, l => l.StartsWith("N5 ", System.StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(unnumbered, l => l.StartsWith("GOTO", System.StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void AutoM47_RaisesFullyOnlyBeforeLongRapids(bool partSubprograms)
|
||||
{
|
||||
var nest = SquaresNest(("A", new Vector(5, 5)), ("B", new Vector(40, 30)));
|
||||
var config = Config(partSubprograms);
|
||||
config.InteriorM47 = M47Mode.Auto;
|
||||
config.ExteriorM47 = M47Mode.Auto;
|
||||
config.SafetyHeadraiseDistance = null;
|
||||
config.M47OverrideDistanceThreshold = 5;
|
||||
|
||||
var run = Run(Post(nest, config));
|
||||
config.M47OverrideDistanceThreshold = null;
|
||||
var unlimited = Run(Post(nest, config));
|
||||
|
||||
// Pierces: header raise, A outer (short hop), B inner (long rapid), B outer (short hop).
|
||||
Assert.Equal(new[] { "M47", "/M47", "M47", "/M47" }, run.PierceLog.Select(p => p.Raise));
|
||||
Assert.All(unlimited.PierceLog, p => Assert.Equal("M47", p.Raise));
|
||||
Assert.Equal(0, run.HeadDownRapids);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ExteriorM47_AppliesAfterThePartExterior(bool partSubprograms)
|
||||
{
|
||||
var nest = SquaresNest(("A", new Vector(5, 5)), ("B", new Vector(40, 30)));
|
||||
var config = Config(partSubprograms);
|
||||
config.InteriorM47 = M47Mode.BlockDelete;
|
||||
config.ExteriorM47 = M47Mode.Always;
|
||||
config.SafetyHeadraiseDistance = null;
|
||||
|
||||
var run = Run(Post(nest, config));
|
||||
|
||||
Assert.Equal(new[] { "M47", "/M47", "M47", "/M47" }, run.PierceLog.Select(p => p.Raise));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("N2", 0.25, "MS250N2")]
|
||||
[InlineData("O2", 0.25, "MS250O2")]
|
||||
[InlineData("N2", 0.30, "")]
|
||||
[InlineData("O2", 0.188, "")]
|
||||
public void PrepareForNest_SelectsOnlyTheGasAndThicknessMatch(string gas, double thickness, string expected)
|
||||
{
|
||||
var nest = SquaresNest(("A", new Vector(5, 5)), ("B", new Vector(30, 20)));
|
||||
nest.Material = new Material("Carbon Steel");
|
||||
nest.Thickness = thickness;
|
||||
nest.AssistGas = gas;
|
||||
var config = Config(partSubprograms: false);
|
||||
config.MaterialLibraries = new()
|
||||
{
|
||||
new() { Material = "Carbon Steel", Thickness = 0.25, Gas = "O2", Library = "MS250O2" },
|
||||
new() { Material = "Carbon Steel", Thickness = 0.25, Gas = "N2", Library = "MS250N2" },
|
||||
new() { Material = "Carbon Steel", Thickness = 0.188, Gas = "N2", Library = "MS188N2" },
|
||||
};
|
||||
var post = new CincinnatiPostProcessor(config);
|
||||
|
||||
post.PrepareForNest(nest);
|
||||
|
||||
Assert.Equal(expected, config.SelectedLibrary);
|
||||
var libraries = Sheet(Post(nest, config))
|
||||
.Where(l => l.StartsWith("G89 P", System.StringComparison.Ordinal))
|
||||
.Select(l => l.Split(' ')[1])
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (expected == "")
|
||||
Assert.Empty(libraries);
|
||||
else
|
||||
Assert.Equal(new[] { $"P{expected}.lib" }, libraries);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void IncrementalPartPrograms_PostWithoutChangingMode(bool partSubprograms)
|
||||
{
|
||||
var nest = new Nest("N");
|
||||
var plate = new Plate(48, 96);
|
||||
nest.Plates.Add(plate);
|
||||
var large = HolePlacementRegressionTests.AddPart(plate, "Large", 0.5, new Vector(10, 20));
|
||||
var small = HolePlacementRegressionTests.AddPart(plate, "Small", 0.25, new Vector(30, 40));
|
||||
large.Program.Mode = Mode.Incremental;
|
||||
small.Program.Mode = Mode.Incremental;
|
||||
|
||||
var run = Run(Post(nest, partSubprograms));
|
||||
|
||||
Assert.Equal(Mode.Incremental, large.Program.Mode);
|
||||
Assert.Equal(Mode.Incremental, small.Program.Mode);
|
||||
HolePlacementRegressionTests.AssertHole(run, large, 0.5);
|
||||
HolePlacementRegressionTests.AssertHole(run, small, 0.25);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Config_WithRemovedSettings_StillLoads()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"UseSheetSubprograms": true,
|
||||
"CoordModeBetweenParts": "G92",
|
||||
"ProcessParameterMode": "Explicit",
|
||||
"InteriorM47": "Auto",
|
||||
"M47OverrideDistanceThreshold": 2.5
|
||||
}
|
||||
""";
|
||||
var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } };
|
||||
|
||||
var config = JsonSerializer.Deserialize<CincinnatiPostConfig>(json, options)!;
|
||||
|
||||
Assert.Equal(M47Mode.Auto, config.InteriorM47);
|
||||
Assert.Equal(2.5, config.M47OverrideDistanceThreshold);
|
||||
}
|
||||
|
||||
// Inner square cut first, then the outer square: pierces at (2,2) and (0,0).
|
||||
private static Program SquaresProgram()
|
||||
{
|
||||
var pgm = new Program(Mode.Absolute);
|
||||
pgm.Codes.Add(new RapidMove(2, 2));
|
||||
pgm.Codes.Add(new LinearMove(4, 2));
|
||||
pgm.Codes.Add(new LinearMove(4, 4));
|
||||
pgm.Codes.Add(new LinearMove(2, 4));
|
||||
pgm.Codes.Add(new LinearMove(2, 2));
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
pgm.Codes.Add(new LinearMove(10, 0));
|
||||
pgm.Codes.Add(new LinearMove(10, 10));
|
||||
pgm.Codes.Add(new LinearMove(0, 10));
|
||||
pgm.Codes.Add(new LinearMove(0, 0));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Program OuterFirst()
|
||||
{
|
||||
var pgm = SquaresProgram();
|
||||
var inner = pgm.Codes.GetRange(0, 5);
|
||||
pgm.Codes.RemoveRange(0, 5);
|
||||
pgm.Codes.AddRange(inner);
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Program HoledSquare()
|
||||
{
|
||||
var pgm = new Program(Mode.Absolute);
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
pgm.Codes.Add(new LinearMove(10, 0));
|
||||
pgm.Codes.Add(new LinearMove(10, 10));
|
||||
pgm.Codes.Add(new LinearMove(0, 10));
|
||||
pgm.Codes.Add(new LinearMove(0, 0));
|
||||
pgm.Codes.Add(new RapidMove(3.5, 3));
|
||||
pgm.Codes.Add(new ArcMove(new Vector(3.5, 3), new Vector(3, 3), RotationType.CW));
|
||||
return pgm;
|
||||
}
|
||||
|
||||
private static Nest SquaresNest(params (string Name, Vector Location)[] parts) =>
|
||||
SquaresNest(SquaresProgram, parts);
|
||||
|
||||
private static Nest SquaresNest(Func<Program> program, params (string Name, Vector Location)[] parts)
|
||||
{
|
||||
var nest = new Nest("N");
|
||||
var plate = new Plate(48, 96);
|
||||
nest.Plates.Add(plate);
|
||||
var drawings = new Dictionary<string, Drawing>();
|
||||
foreach (var (name, location) in parts)
|
||||
{
|
||||
if (!drawings.TryGetValue(name, out var drawing))
|
||||
drawings[name] = drawing = new Drawing(name) { Program = program() };
|
||||
plate.Parts.Add(new Part(drawing) { Location = location });
|
||||
}
|
||||
return nest;
|
||||
}
|
||||
|
||||
private static Part AddPart(Plate plate, string name, Program program, Vector location)
|
||||
{
|
||||
var part = new Part(new Drawing(name) { Program = program }) { Location = location };
|
||||
plate.Parts.Add(part);
|
||||
return part;
|
||||
}
|
||||
|
||||
private static void AddLeadInPart(Plate plate, Drawing drawing, double leadIn, Vector location)
|
||||
{
|
||||
var part = new Part(drawing);
|
||||
part.ApplyLeadIns(
|
||||
new CuttingParameters
|
||||
{
|
||||
ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 },
|
||||
ArcCircleLeadOut = new NoLeadOut(),
|
||||
ExternalLeadIn = new LineLeadIn { Length = leadIn, ApproachAngle = 90 },
|
||||
ExternalLeadOut = new NoLeadOut(),
|
||||
},
|
||||
new Vector(10, 10)
|
||||
);
|
||||
part.Location = location;
|
||||
plate.Parts.Add(part);
|
||||
}
|
||||
|
||||
private static List<string> Sheet(string output)
|
||||
{
|
||||
var lines = output.Split('\n').Select(l => l.Trim()).ToList();
|
||||
var start = lines.IndexOf(":101");
|
||||
var end = lines.FindIndex(start, l => l.StartsWith("M99 (END OF", System.StringComparison.Ordinal));
|
||||
return lines.GetRange(start, end - start + 1);
|
||||
}
|
||||
|
||||
private static int Count(string output, string line) =>
|
||||
output.Split('\n').Count(l => l.Trim() == line);
|
||||
|
||||
private static void AssertPoints(IReadOnlyList<Vector> expected, IReadOnlyList<Vector> actual)
|
||||
{
|
||||
Assert.Equal(expected.Count, actual.Count);
|
||||
for (var i = 0; i < expected.Count; i++)
|
||||
Assert.True(Near(expected[i], actual[i]), $"Point {i}: expected {expected[i]}, got {actual[i]}");
|
||||
}
|
||||
|
||||
private static void AssertSamePath(Result expected, Result actual)
|
||||
{
|
||||
AssertPoints(expected.PierceLog.Select(p => p.Point).ToList(), actual.PierceLog.Select(p => p.Point).ToList());
|
||||
AssertPoints(expected.Cuts.Select(c => c.From).ToList(), actual.Cuts.Select(c => c.From).ToList());
|
||||
AssertPoints(expected.Cuts.Select(c => c.To).ToList(), actual.Cuts.Select(c => c.To).ToList());
|
||||
Assert.Equal(expected.FullCircles.Count, actual.FullCircles.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Posts.Cincinnati;
|
||||
|
||||
namespace OpenNest.Tests.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// Posts nests with the Cincinnati CL post and replays the G-code to see where the
|
||||
/// machine actually cuts.
|
||||
/// </summary>
|
||||
internal static class CincinnatiReplay
|
||||
{
|
||||
/// <summary>A config whose output is easy to replay: no anti-dive, no pallet exchange.</summary>
|
||||
internal static CincinnatiPostConfig Config(bool partSubprograms) => new()
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
UsePartSubprograms = partSubprograms,
|
||||
UseAntiDive = false,
|
||||
PalletExchange = PalletMode.None,
|
||||
};
|
||||
|
||||
internal static string Post(Nest nest, bool partSubprograms) => Post(nest, Config(partSubprograms));
|
||||
|
||||
internal static string Post(Nest nest, CincinnatiPostConfig config)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
new CincinnatiPostProcessor(config).Post(nest, ms);
|
||||
return Encoding.UTF8.GetString(ms.ToArray()).Replace("\r", "");
|
||||
}
|
||||
|
||||
internal sealed class Result
|
||||
{
|
||||
public List<(Vector Center, double Radius)> FullCircles { get; } = new();
|
||||
public int Pierces { get; set; }
|
||||
public int EmptyPierces { get; set; }
|
||||
|
||||
/// <summary>Every pierce with the head raise posted before it (null for none).</summary>
|
||||
public List<(Vector Point, string? Raise)> PierceLog { get; } = new();
|
||||
|
||||
/// <summary>Every cutting move, start to end, in machine coordinates.</summary>
|
||||
public List<(Vector From, Vector To)> Cuts { get; } = new();
|
||||
|
||||
/// <summary>Head raises posted while the head was already raised.</summary>
|
||||
public int DoubleRaises { get; set; }
|
||||
|
||||
/// <summary>Rapids taken after a cut with no head raise since.</summary>
|
||||
public int HeadDownRapids { get; set; }
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// /M47 counts as a raise). Every arc must end on its own radius.
|
||||
/// </summary>
|
||||
internal static Result Run(string output)
|
||||
{
|
||||
var lines = output.Split('\n').Select(Clean).ToList();
|
||||
var labels = new Dictionary<int, int>();
|
||||
for (var i = 0; i < lines.Count; i++)
|
||||
{
|
||||
if (lines[i].StartsWith(':'))
|
||||
labels[int.Parse(lines[i][1..], CultureInfo.InvariantCulture)] = i;
|
||||
}
|
||||
|
||||
var run = new Result();
|
||||
var position = Vector.Zero; // machine position
|
||||
var g92 = Vector.Zero; // work origin in machine frame
|
||||
var g52 = Vector.Zero; // local shift in the work frame
|
||||
var returns = new Stack<int>();
|
||||
var pierced = false;
|
||||
var cutSincePierce = false;
|
||||
string? raise = null; // raise posted since the last pierce
|
||||
var headDown = false;
|
||||
var pc = 0;
|
||||
|
||||
while (pc < lines.Count)
|
||||
{
|
||||
var line = lines[pc++];
|
||||
if (line.Length == 0 || line.StartsWith(':') || line.StartsWith("GOTO", System.StringComparison.Ordinal))
|
||||
continue;
|
||||
if (line.StartsWith("#", System.StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var blockDelete = line.StartsWith('/');
|
||||
if (blockDelete)
|
||||
line = line[1..];
|
||||
|
||||
var words = Words(line);
|
||||
if (words.TryGetValue("M", out var m))
|
||||
{
|
||||
if (m == "30")
|
||||
break;
|
||||
if (m == "98")
|
||||
{
|
||||
returns.Push(pc);
|
||||
pc = labels[int.Parse(words["P"], CultureInfo.InvariantCulture)] + 1;
|
||||
continue;
|
||||
}
|
||||
if (m == "99")
|
||||
{
|
||||
pc = returns.Pop();
|
||||
continue;
|
||||
}
|
||||
if (m is "35" or "135")
|
||||
{
|
||||
if (pierced && !cutSincePierce)
|
||||
run.EmptyPierces++;
|
||||
pierced = false;
|
||||
}
|
||||
if (m == "47")
|
||||
{
|
||||
if (raise != null)
|
||||
run.DoubleRaises++;
|
||||
raise = blockDelete ? "/" + line : line;
|
||||
headDown = false;
|
||||
}
|
||||
if (m == "42")
|
||||
headDown = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!words.TryGetValue("G", out var g))
|
||||
continue;
|
||||
|
||||
switch (g)
|
||||
{
|
||||
case "84":
|
||||
case "85":
|
||||
run.Pierces++;
|
||||
run.PierceLog.Add((position, raise));
|
||||
raise = null;
|
||||
pierced = true;
|
||||
headDown = true;
|
||||
cutSincePierce = false;
|
||||
break;
|
||||
case "52":
|
||||
g52 = new Vector(Num(words["X"]), Num(words["Y"]));
|
||||
break;
|
||||
case "92":
|
||||
g92 = words["X"].StartsWith('#')
|
||||
? Vector.Zero
|
||||
: position - g52 - new Vector(Num(words["X"]), Num(words["Y"]));
|
||||
break;
|
||||
case "0":
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
{
|
||||
var target = g92 + g52 + new Vector(Num(words["X"]), Num(words["Y"]));
|
||||
if (g is "2" or "3")
|
||||
{
|
||||
var center = position + new Vector(Num(words["I"]), Num(words["J"]));
|
||||
var radius = position.DistanceTo(center);
|
||||
Assert.True(
|
||||
System.Math.Abs(target.DistanceTo(center) - radius) < 1e-3,
|
||||
$"Arc ends off its radius: {line}"
|
||||
);
|
||||
if (target.DistanceTo(position) < 1e-6)
|
||||
run.FullCircles.Add((center, radius));
|
||||
}
|
||||
if (g == "0" && headDown)
|
||||
run.HeadDownRapids++;
|
||||
if (g != "0")
|
||||
{
|
||||
cutSincePierce = true;
|
||||
if (pierced)
|
||||
run.Cuts.Add((position, target));
|
||||
}
|
||||
position = target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return run;
|
||||
}
|
||||
|
||||
internal static bool Near(Vector a, Vector b) => a.DistanceTo(b) < 1e-3;
|
||||
|
||||
private static string Clean(string line)
|
||||
{
|
||||
var text = line.Trim();
|
||||
var comment = text.IndexOf('(');
|
||||
if (comment >= 0)
|
||||
text = text[..comment].Trim();
|
||||
if (text.StartsWith('N'))
|
||||
{
|
||||
var space = text.IndexOf(' ');
|
||||
text = space < 0 ? "" : text[(space + 1)..];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Words in a block, keyed by letter; a leading G41/G42 gives way to the motion code after it.
|
||||
private static Dictionary<string, string> Words(string line)
|
||||
{
|
||||
var words = new Dictionary<string, string>();
|
||||
foreach (var word in line.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
|
||||
words[word[..1]] = word[1..];
|
||||
return words;
|
||||
}
|
||||
|
||||
private static double Num(string value) => double.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class HoleCallOutputCharacterizationTests
|
||||
|
||||
var body = FirstHole(route, lineNumbers ? "N73 G52 X-12.35 Y6.79" : "G52 X-12.35 Y6.79")
|
||||
+ Lines("M98 P7", "G52 X0 Y0");
|
||||
Assert.Equal(WrapOutput(route, body), output.ToString());
|
||||
Assert.Equal(WrapOutput(route, body, lineNumbers ? 73 : null), output.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -61,7 +61,7 @@ public class HoleCallOutputCharacterizationTests
|
||||
"M98 P0",
|
||||
"G52 X0 Y0"
|
||||
);
|
||||
Assert.Equal(WrapOutput(route, body), output.ToString());
|
||||
Assert.Equal(WrapOutput(route, body, lineNumbers ? 73 : null), output.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -92,6 +92,7 @@ public class HoleCallOutputCharacterizationTests
|
||||
if (route != "part")
|
||||
body += Lines("( PART: Sample )");
|
||||
body += Lines(
|
||||
"(WARNING: No library found)",
|
||||
"G84",
|
||||
"G1 X2 Y1 F#148",
|
||||
"G1 X1 Y1",
|
||||
@@ -102,12 +103,13 @@ public class HoleCallOutputCharacterizationTests
|
||||
"G52 X0 Y0",
|
||||
"M47",
|
||||
lineNumbers ? "N1003 G0 X3 Y3" : "G0 X3 Y3",
|
||||
"(WARNING: No library found)",
|
||||
"G84",
|
||||
"G1 X4 Y3 F#148",
|
||||
"G1 X3 Y3",
|
||||
"M35"
|
||||
);
|
||||
Assert.Equal(WrapOutput(route, body), output.ToString());
|
||||
Assert.Equal(WrapOutput(route, body, lineNumbers ? 73 : null), output.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -127,14 +129,14 @@ public class HoleCallOutputCharacterizationTests
|
||||
write(first, program);
|
||||
|
||||
Assert.Equal(
|
||||
WrapOutput(route, FirstHole(route, "N87 G52 X-12.35 Y6.79") + Lines("M98 P7", "G52 X0 Y0")),
|
||||
WrapOutput(route, FirstHole(route, "N87 G52 X-12.35 Y6.79") + Lines("M98 P7", "G52 X0 Y0"), 87),
|
||||
first.ToString()
|
||||
);
|
||||
config.UseLineNumbers = false;
|
||||
using var second = new StringWriter { NewLine = "\n" };
|
||||
write(second, program);
|
||||
Assert.Equal(
|
||||
WrapOutput(route, FirstHole(route, "G52 X-12.35 Y6.79") + Lines("M98 P7", "G52 X0 Y0")),
|
||||
WrapOutput(route, FirstHole(route, "G52 X-12.35 Y6.79") + Lines("M98 P7", "G52 X0 Y0"), null),
|
||||
second.ToString()
|
||||
);
|
||||
}
|
||||
@@ -152,7 +154,7 @@ public class HoleCallOutputCharacterizationTests
|
||||
var error = Assert.Throws<IOException>(() => write(output, program));
|
||||
|
||||
Assert.Same(output.Failure, error);
|
||||
Assert.Equal(Header(route) + FirstHole(route, "N73 G52 X-12.35 Y6.79"), output.ToString());
|
||||
Assert.Equal(Header(route, 73) + FirstHole(route, "N73 G52 X-12.35 Y6.79"), output.ToString());
|
||||
}
|
||||
|
||||
private static CincinnatiPostConfig CreateConfig(bool lineNumbers) => new()
|
||||
@@ -161,7 +163,6 @@ public class HoleCallOutputCharacterizationTests
|
||||
FeatureLineNumberStart = 73,
|
||||
UseLineNumbers = lineNumbers,
|
||||
PalletExchange = PalletMode.None,
|
||||
ProcessParameterMode = G89Mode.Explicit,
|
||||
KerfCompensation = KerfMode.PreApplied,
|
||||
UseAntiDive = false,
|
||||
};
|
||||
@@ -204,7 +205,7 @@ public class HoleCallOutputCharacterizationTests
|
||||
}
|
||||
|
||||
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager(), byProgram);
|
||||
Dictionary<(int, long), int>? partMapping = route == "grouped-sheet" ? new() : null;
|
||||
Dictionary<Part, PartSubprogram>? partMapping = route == "grouped-sheet" ? new() : null;
|
||||
return (output, program) =>
|
||||
{
|
||||
var plate = new Plate(48, 96);
|
||||
@@ -234,15 +235,17 @@ public class HoleCallOutputCharacterizationTests
|
||||
}
|
||||
|
||||
// Literal snapshots deliberately do not call the production emitter/formatter.
|
||||
private static string WrapOutput(string route, string body) => Header(route) + body +
|
||||
private static string WrapOutput(string route, string body, int? gotoTarget) => Header(route, gotoTarget) + body +
|
||||
(route == "part" ? Lines("M99 (END OF Sample)") : Lines("M42", "M99 (END OF Nest.001)"));
|
||||
|
||||
private static string Header(string route)
|
||||
// The sheet's GOTO names the first feature's line number, posted only with line numbers.
|
||||
private static string Header(string route, int? gotoTarget)
|
||||
{
|
||||
if (route == "part")
|
||||
return Lines("(*****************************************************)", ":201", "( PART: Sample )");
|
||||
|
||||
return Lines(
|
||||
var lines = new List<string>
|
||||
{
|
||||
"(*****************************************************)",
|
||||
"( START OF Nest.001 )",
|
||||
":101",
|
||||
@@ -257,8 +260,10 @@ public class HoleCallOutputCharacterizationTests
|
||||
"M98 P100 (Variable Declaration)",
|
||||
"G90",
|
||||
"M47",
|
||||
"GOTO1( Goto Feature )"
|
||||
);
|
||||
};
|
||||
if (gotoTarget != null)
|
||||
lines.Add($"GOTO{gotoTarget}( Goto Feature )");
|
||||
return Lines(lines.ToArray());
|
||||
}
|
||||
|
||||
// A sheet part that opens with a hole call names the part after the call's G52 line.
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Geometry;
|
||||
@@ -27,7 +24,7 @@ public class HolePlacementRegressionTests
|
||||
var small = AddPart(plate, "Small", 0.25, new Vector(30, 20));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var run = Replay(Post(nest, partSubprograms: false));
|
||||
var run = CincinnatiReplay.Run(CincinnatiReplay.Post(nest, partSubprograms: false));
|
||||
|
||||
AssertHole(run, large, 0.5);
|
||||
AssertHole(run, small, 0.25);
|
||||
@@ -46,7 +43,7 @@ public class HolePlacementRegressionTests
|
||||
nest.Plates.Add(first);
|
||||
nest.Plates.Add(second);
|
||||
|
||||
var run = Replay(Post(nest, partSubprograms: true));
|
||||
var run = CincinnatiReplay.Run(CincinnatiReplay.Post(nest, partSubprograms: true));
|
||||
|
||||
AssertHole(run, large, 0.5);
|
||||
AssertHole(run, small, 0.25);
|
||||
@@ -67,20 +64,17 @@ public class HolePlacementRegressionTests
|
||||
HoleCall(large).Id = 1;
|
||||
HoleCall(small).Id = 1;
|
||||
|
||||
var output = Post(nest, partSubprograms);
|
||||
var output = CincinnatiReplay.Post(nest, partSubprograms);
|
||||
|
||||
var holeSubs = output.Split('\n').Count(l => l.Trim() == "( PART: HOLE )");
|
||||
Assert.Equal(2, holeSubs);
|
||||
var run = Replay(output);
|
||||
var run = CincinnatiReplay.Run(output);
|
||||
AssertHole(run, large, 0.5);
|
||||
// Part sub-program calls restore the sheet frame with G92 at the part corner while
|
||||
// the tool is elsewhere, shifting later parts on the sheet (a separate known defect).
|
||||
if (!partSubprograms)
|
||||
AssertHole(run, small, 0.25);
|
||||
AssertHole(run, small, 0.25);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InlineSheet_PartStartingWithHole_KeepsPartCommentAndSafetyHeadraise()
|
||||
public void InlineSheet_PartStartingWithHole_KeepsPartCommentAndSafetyHeadraiseBeforeItsRapid()
|
||||
{
|
||||
var nest = new Nest("N");
|
||||
var plate = new Plate(48, 96);
|
||||
@@ -88,13 +82,15 @@ public class HolePlacementRegressionTests
|
||||
AddPart(plate, "Second", 0.5, new Vector(20, 0));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var lines = Post(nest, partSubprograms: false).Split('\n').Select(l => l.Trim()).ToList();
|
||||
var lines = CincinnatiReplay.Post(nest, partSubprograms: false).Split('\n').Select(l => l.Trim()).ToList();
|
||||
|
||||
var second = lines.IndexOf("( PART: Second )");
|
||||
Assert.True(second > 0);
|
||||
Assert.EndsWith("G52 X23 Y3", lines[second - 1]);
|
||||
Assert.Single(lines, l => l == "( PART: First )");
|
||||
Assert.Equal("M47 P2000 (Safety Headraise)", lines[second + 3]);
|
||||
// The safety raise ends the previous part, before the rapid to this one.
|
||||
Assert.Equal("M47 P2000 (Safety Headraise)", lines[second - 2]);
|
||||
Assert.Equal("M47", lines[second + 3]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -108,7 +104,7 @@ public class HolePlacementRegressionTests
|
||||
nest.Plates.Add(plate);
|
||||
Assert.IsType<SubProgramCall>(part.Program.Codes[0]);
|
||||
|
||||
var run = Replay(Post(nest, partSubprograms));
|
||||
var run = CincinnatiReplay.Run(CincinnatiReplay.Post(nest, partSubprograms));
|
||||
|
||||
Assert.Equal(0, run.EmptyPierces);
|
||||
Assert.Equal(2, run.Pierces);
|
||||
@@ -126,7 +122,7 @@ public class HolePlacementRegressionTests
|
||||
var hole = HoleCall(part).Program;
|
||||
var before = hole.Codes.Count;
|
||||
|
||||
Post(nest, partSubprograms);
|
||||
CincinnatiReplay.Post(nest, partSubprograms);
|
||||
|
||||
Assert.Equal(Mode.Incremental, hole.Mode);
|
||||
Assert.Equal(before, hole.Codes.Count);
|
||||
@@ -145,7 +141,7 @@ public class HolePlacementRegressionTests
|
||||
AddPart(plate, "Second", 0.5, new Vector(20, 0));
|
||||
nest.Plates.Add(plate);
|
||||
|
||||
var output = Post(nest, partSubprograms);
|
||||
var output = CincinnatiReplay.Post(nest, partSubprograms);
|
||||
|
||||
var sheet = output[output.IndexOf(":101", System.StringComparison.Ordinal)..];
|
||||
var order = sheet
|
||||
@@ -157,7 +153,7 @@ public class HolePlacementRegressionTests
|
||||
Assert.Equal(new[] { "( PART: Third )", "( PART: First )", "( PART: Second )" }, order);
|
||||
}
|
||||
|
||||
private static Part AddPart(Plate plate, string name, double holeRadius, Vector location)
|
||||
internal static Part AddPart(Plate plate, string name, double holeRadius, Vector location)
|
||||
{
|
||||
var pgm = new Program(Mode.Absolute);
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
@@ -186,10 +182,10 @@ public class HolePlacementRegressionTests
|
||||
return part;
|
||||
}
|
||||
|
||||
private static SubProgramCall HoleCall(Part part) =>
|
||||
internal static SubProgramCall HoleCall(Part part) =>
|
||||
part.Program.Codes.OfType<SubProgramCall>().Single();
|
||||
|
||||
private static void AssertHole(Replayed run, Part part, double radius)
|
||||
internal static void AssertHole(CincinnatiReplay.Result run, Part part, double radius)
|
||||
{
|
||||
var expected = part.Location + HoleCenter;
|
||||
Assert.Contains(
|
||||
@@ -197,154 +193,4 @@ public class HolePlacementRegressionTests
|
||||
c => c.Center.DistanceTo(expected) < 1e-3 && System.Math.Abs(c.Radius - radius) < 1e-3
|
||||
);
|
||||
}
|
||||
|
||||
private static string Post(Nest nest, bool partSubprograms)
|
||||
{
|
||||
var config = new CincinnatiPostConfig
|
||||
{
|
||||
PostedAccuracy = 4,
|
||||
UsePartSubprograms = partSubprograms,
|
||||
ProcessParameterMode = G89Mode.Explicit,
|
||||
UseAntiDive = false,
|
||||
PalletExchange = PalletMode.None,
|
||||
};
|
||||
using var ms = new MemoryStream();
|
||||
new CincinnatiPostProcessor(config).Post(nest, ms);
|
||||
return Encoding.UTF8.GetString(ms.ToArray()).Replace("\r", "");
|
||||
}
|
||||
|
||||
private sealed class Replayed
|
||||
{
|
||||
public List<(Vector Center, double Radius)> FullCircles { get; } = new();
|
||||
public int Pierces { get; set; }
|
||||
public int EmptyPierces { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal replay of the post's own dialect: M98/M99 calls, G52/G92 frames,
|
||||
/// G0-G3 under G90 with incremental I/J. Every arc must end on its own radius.
|
||||
/// </summary>
|
||||
private static Replayed Replay(string output)
|
||||
{
|
||||
var lines = output.Split('\n').Select(Clean).ToList();
|
||||
var labels = new Dictionary<int, int>();
|
||||
for (var i = 0; i < lines.Count; i++)
|
||||
{
|
||||
if (lines[i].StartsWith(':'))
|
||||
labels[int.Parse(lines[i][1..], CultureInfo.InvariantCulture)] = i;
|
||||
}
|
||||
|
||||
var run = new Replayed();
|
||||
var position = Vector.Zero; // machine position
|
||||
var g92 = Vector.Zero; // work origin in machine frame
|
||||
var g52 = Vector.Zero; // local shift in the work frame
|
||||
var returns = new Stack<int>();
|
||||
var pierced = false;
|
||||
var cutSincePierce = false;
|
||||
var pc = 0;
|
||||
|
||||
while (pc < lines.Count)
|
||||
{
|
||||
var line = lines[pc++];
|
||||
if (line.Length == 0 || line.StartsWith(':') || line.StartsWith("GOTO", System.StringComparison.Ordinal))
|
||||
continue;
|
||||
if (line.StartsWith("#", System.StringComparison.Ordinal) || line.StartsWith("/", System.StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var words = Words(line);
|
||||
if (words.TryGetValue("M", out var m))
|
||||
{
|
||||
if (m == "30")
|
||||
break;
|
||||
if (m == "98")
|
||||
{
|
||||
returns.Push(pc);
|
||||
pc = labels[int.Parse(words["P"], CultureInfo.InvariantCulture)] + 1;
|
||||
continue;
|
||||
}
|
||||
if (m == "99")
|
||||
{
|
||||
pc = returns.Pop();
|
||||
continue;
|
||||
}
|
||||
if (m is "35" or "135")
|
||||
{
|
||||
if (pierced && !cutSincePierce)
|
||||
run.EmptyPierces++;
|
||||
pierced = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!words.TryGetValue("G", out var g))
|
||||
continue;
|
||||
|
||||
switch (g)
|
||||
{
|
||||
case "84":
|
||||
case "85":
|
||||
run.Pierces++;
|
||||
pierced = true;
|
||||
cutSincePierce = false;
|
||||
break;
|
||||
case "52":
|
||||
g52 = new Vector(Num(words["X"]), Num(words["Y"]));
|
||||
break;
|
||||
case "92":
|
||||
g92 = words["X"].StartsWith('#')
|
||||
? Vector.Zero
|
||||
: position - g52 - new Vector(Num(words["X"]), Num(words["Y"]));
|
||||
break;
|
||||
case "0":
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
{
|
||||
var target = g92 + g52 + new Vector(Num(words["X"]), Num(words["Y"]));
|
||||
if (g is "2" or "3")
|
||||
{
|
||||
var center = position + new Vector(Num(words["I"]), Num(words["J"]));
|
||||
var radius = position.DistanceTo(center);
|
||||
Assert.True(
|
||||
System.Math.Abs(target.DistanceTo(center) - radius) < 1e-3,
|
||||
$"Arc ends off its radius: {line}"
|
||||
);
|
||||
if (target.DistanceTo(position) < 1e-6)
|
||||
run.FullCircles.Add((center, radius));
|
||||
}
|
||||
if (g != "0")
|
||||
cutSincePierce = true;
|
||||
position = target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return run;
|
||||
}
|
||||
|
||||
private static string Clean(string line)
|
||||
{
|
||||
var text = line.Trim();
|
||||
var comment = text.IndexOf('(');
|
||||
if (comment >= 0)
|
||||
text = text[..comment].Trim();
|
||||
if (text.StartsWith('N'))
|
||||
{
|
||||
var space = text.IndexOf(' ');
|
||||
text = space < 0 ? "" : text[(space + 1)..];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Words in a block, keyed by letter; a leading G41/G42 gives way to the motion code after it.
|
||||
private static Dictionary<string, string> Words(string line)
|
||||
{
|
||||
var words = new Dictionary<string, string>();
|
||||
foreach (var word in line.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
|
||||
words[word[..1]] = word[1..];
|
||||
return words;
|
||||
}
|
||||
|
||||
private static double Num(string value) => double.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -17,13 +17,26 @@ public sealed class FeatureContext
|
||||
public string PartName { get; set; } = "";
|
||||
public bool IsFirstFeatureOfPart { get; set; }
|
||||
public bool IsLastFeatureOnSheet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True when the rapid after this feature goes to a different part, so its head
|
||||
/// raise is the safety head raise.
|
||||
/// </summary>
|
||||
public bool IsSafetyHeadraise { get; set; }
|
||||
|
||||
/// <summary>True when this feature cuts the part's exterior (selects ExteriorM47).</summary>
|
||||
public bool IsExteriorFeature { get; set; }
|
||||
public bool IsEtch { get; set; }
|
||||
public string LibraryFile { get; set; } = "";
|
||||
public double CutDistance { get; set; }
|
||||
public double SheetDiagonal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rapid travel from this feature's end to the next pierce, used by
|
||||
/// <see cref="M47Mode.Auto"/>. Null when unknown.
|
||||
/// </summary>
|
||||
public double? RapidDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Part location on the plate. Added to all output X/Y coordinates
|
||||
/// so part-relative programs become plate-absolute under G90.
|
||||
@@ -93,19 +106,16 @@ public sealed class CincinnatiFeatureWriter
|
||||
writer.WriteLine(CoordinateFormatter.Comment($"PART: {ctx.PartName}"));
|
||||
|
||||
// 3. G89 process params
|
||||
if (_config.ProcessParameterMode == G89Mode.LibraryFile)
|
||||
var lib = ctx.LibraryFile;
|
||||
if (!string.IsNullOrEmpty(lib))
|
||||
{
|
||||
var lib = ctx.LibraryFile;
|
||||
if (!string.IsNullOrEmpty(lib))
|
||||
{
|
||||
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
writer.WriteLine($"G89 P{lib} ({speedClass} {cutDist})");
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteLine("(WARNING: No library found)");
|
||||
}
|
||||
var speedClass = _speedClassifier.Classify(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
var cutDist = _speedClassifier.FormatCutDist(ctx.CutDistance, ctx.SheetDiagonal);
|
||||
writer.WriteLine($"G89 P{lib} ({speedClass} {cutDist})");
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteLine("(WARNING: No library found)");
|
||||
}
|
||||
|
||||
// 4. Pierce/beam on — G85 for etch (no pierce), G84 for cut
|
||||
@@ -322,25 +332,14 @@ public sealed class CincinnatiFeatureWriter
|
||||
|
||||
private void WriteM47(TextWriter writer, FeatureContext ctx)
|
||||
{
|
||||
if (ctx.IsSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
|
||||
{
|
||||
writer.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value} (Safety Headraise)");
|
||||
return;
|
||||
}
|
||||
|
||||
var mode = ctx.IsExteriorFeature ? _config.ExteriorM47 : _config.InteriorM47;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case M47Mode.Always:
|
||||
writer.WriteLine("M47");
|
||||
break;
|
||||
case M47Mode.BlockDelete:
|
||||
writer.WriteLine("/M47");
|
||||
break;
|
||||
case M47Mode.None:
|
||||
break;
|
||||
}
|
||||
var line = CincinnatiHeadRaise.Line(
|
||||
_config,
|
||||
ctx.IsSafetyHeadraise,
|
||||
ctx.IsExteriorFeature,
|
||||
ctx.RapidDistance
|
||||
);
|
||||
if (line != null)
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
|
||||
private static string GetLinearFeedVariable(LayerType layer)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the M47 head raise (§3.47) written before the rapid that leaves a feature.
|
||||
/// </summary>
|
||||
internal static class CincinnatiHeadRaise
|
||||
{
|
||||
/// <param name="isSafetyHeadraise">True when the rapid goes to a different part.</param>
|
||||
/// <param name="isExterior">True when the feature just cut is a part exterior.</param>
|
||||
/// <param name="rapidDistance">Travel to the next pierce; null when unknown.</param>
|
||||
/// <returns>The block to write, or null for no head raise.</returns>
|
||||
internal static string Line(
|
||||
CincinnatiPostConfig config,
|
||||
bool isSafetyHeadraise,
|
||||
bool isExterior,
|
||||
double? rapidDistance
|
||||
)
|
||||
{
|
||||
if (isSafetyHeadraise && config.SafetyHeadraiseDistance.HasValue)
|
||||
return $"M47 P{config.SafetyHeadraiseDistance.Value} (Safety Headraise)";
|
||||
|
||||
var mode = isExterior ? config.ExteriorM47 : config.InteriorM47;
|
||||
return mode switch
|
||||
{
|
||||
M47Mode.Always => "M47",
|
||||
M47Mode.BlockDelete => "/M47",
|
||||
M47Mode.Auto => IsLongRapid(config, rapidDistance) ? "M47" : "/M47",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Unknown travel, or no threshold configured, always gets the full raise.
|
||||
private static bool IsLongRapid(CincinnatiPostConfig config, double? rapidDistance) =>
|
||||
config.M47OverrideDistanceThreshold is not { } threshold
|
||||
|| rapidDistance is not { } distance
|
||||
|| distance > threshold;
|
||||
}
|
||||
@@ -7,10 +7,18 @@ using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
/// <summary>
|
||||
/// A posted part sub-program and where one part's program frame puts its origin.
|
||||
/// </summary>
|
||||
/// <param name="Number">Posted sub-program number.</param>
|
||||
/// <param name="Program">The normalized, absolute program the sub-program is written from.</param>
|
||||
/// <param name="Corner">The sub-program origin in the part's program frame.</param>
|
||||
public sealed record PartSubprogram(int Number, Program Program, Vector Corner);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a Cincinnati-format part sub-program definition.
|
||||
/// Each sub-program contains the complete cutting sequence for one unique part geometry
|
||||
/// (drawing + rotation), with coordinates normalized to origin (0,0).
|
||||
/// Each sub-program contains the complete cutting sequence for one unique posted part
|
||||
/// toolpath, with coordinates normalized to origin (0,0).
|
||||
/// Called via M98 from sheet sub-programs.
|
||||
/// </summary>
|
||||
public sealed class CincinnatiPartSubprogramWriter
|
||||
@@ -51,6 +59,7 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
|
||||
// Classify and order: etch features first, then cut features
|
||||
var ordered = FeatureUtils.ClassifyAndOrder(allFeatures);
|
||||
var exterior = FeatureUtils.FindExterior(ordered);
|
||||
|
||||
w.WriteLine("(*****************************************************)");
|
||||
w.WriteLine($":{subNumber}");
|
||||
@@ -81,11 +90,14 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
IsFirstFeatureOfPart = false,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsSafetyHeadraise = false,
|
||||
IsExteriorFeature = false,
|
||||
IsExteriorFeature = exterior[i],
|
||||
IsEtch = isEtch,
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
RapidDistance = isLastFeature
|
||||
? null
|
||||
: FeatureUtils.EndPoint(codes).DistanceTo(FeatureUtils.StartPoint(ordered[i + 1].codes)),
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
@@ -102,22 +114,19 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
internal static void EnsureLeadingRapid(Program pgm) => FeatureUtils.InsertLeadingRapid(pgm.Codes);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a sub-program key for matching parts to their sub-programs.
|
||||
/// </summary>
|
||||
internal static (int drawingId, long rotationKey) SubprogramKey(Part part) =>
|
||||
(part.BaseDrawing.Id, (long)System.Math.Round(part.Rotation * 1e6));
|
||||
|
||||
/// <summary>
|
||||
/// Scans all plates and builds a mapping of unique part geometries to sub-program numbers,
|
||||
/// along with their normalized programs for writing.
|
||||
/// Scans all plates and assigns each non-cutoff part the sub-program for its posted
|
||||
/// toolpath: parts share a sub-program only when their normalized programs, lead-ins
|
||||
/// included, post identically under the same drawing name. Part programs are copied,
|
||||
/// never modified.
|
||||
/// </summary>
|
||||
internal static (
|
||||
Dictionary<(int, long), int> mapping,
|
||||
Dictionary<Part, PartSubprogram> mapping,
|
||||
List<(int subNum, string name, Program program)> entries
|
||||
) BuildRegistry(IEnumerable<Plate> plates, int startNumber)
|
||||
{
|
||||
var mapping = new Dictionary<(int, long), int>();
|
||||
var mapping = new Dictionary<Part, PartSubprogram>(ReferenceEqualityComparer.Instance);
|
||||
var entries = new List<(int, string, Program)>();
|
||||
var contentIndex = new Dictionary<string, (int subNum, Program program)>();
|
||||
var nextSubNum = startNumber;
|
||||
|
||||
foreach (var plate in plates)
|
||||
@@ -126,32 +135,44 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
{
|
||||
if (part.BaseDrawing.IsCutOff)
|
||||
continue;
|
||||
var key = SubprogramKey(part);
|
||||
if (!mapping.ContainsKey(key))
|
||||
|
||||
var pgm = PostedCopy(part.Program);
|
||||
var bbox = pgm.BoundingBox();
|
||||
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
|
||||
|
||||
// If the program has no leading rapid, the feature writer
|
||||
// will use the first motion endpoint as the pierce point,
|
||||
// losing the first contour segment. Insert a synthetic rapid
|
||||
// at the contour's return point (last motion endpoint) so
|
||||
// the full contour is preserved.
|
||||
EnsureLeadingRapid(pgm);
|
||||
|
||||
var name = part.BaseDrawing.Name;
|
||||
var key = name + "\n" + ProgramToCanonical(pgm);
|
||||
if (!contentIndex.TryGetValue(key, out var entry))
|
||||
{
|
||||
var subNum = nextSubNum++;
|
||||
mapping[key] = subNum;
|
||||
|
||||
var pgm = part.Program.Clone() as Program;
|
||||
pgm.Mode = Mode.Absolute;
|
||||
var bbox = pgm.BoundingBox();
|
||||
pgm.Offset(-bbox.Location.X, -bbox.Location.Y);
|
||||
|
||||
// If the program has no leading rapid, the feature writer
|
||||
// will use the first motion endpoint as the pierce point,
|
||||
// losing the first contour segment. Insert a synthetic rapid
|
||||
// at the contour's return point (last motion endpoint) so
|
||||
// the full contour is preserved.
|
||||
EnsureLeadingRapid(pgm);
|
||||
|
||||
entries.Add((subNum, part.BaseDrawing.Name, pgm));
|
||||
entry = (nextSubNum++, pgm);
|
||||
contentIndex[key] = entry;
|
||||
entries.Add((entry.subNum, name, pgm));
|
||||
}
|
||||
|
||||
mapping[part] = new PartSubprogram(entry.subNum, entry.program, bbox.Location);
|
||||
}
|
||||
}
|
||||
|
||||
return (mapping, entries);
|
||||
}
|
||||
|
||||
// An absolute copy with suppressed moves left uncut, as the post writes it.
|
||||
private static Program PostedCopy(Program program)
|
||||
{
|
||||
var copy = FeatureUtils.ToAbsoluteCopy(program);
|
||||
var codes = FeatureUtils.RemoveSuppressed(copy.Codes);
|
||||
copy.Codes.Clear();
|
||||
copy.Codes.AddRange(codes);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a nest-level registry of unique hole sub-programs from every hole call in
|
||||
/// the plates' parts and in the part sub-program copies. Calls are keyed by their
|
||||
@@ -179,10 +200,10 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
{
|
||||
if (code is not SubProgramCall { Program: not null } call)
|
||||
continue;
|
||||
if (mapping.ContainsKey(call.Program))
|
||||
if (mapping.ContainsKey(call.Program) || !FeatureUtils.HasCut(call.Program))
|
||||
continue;
|
||||
|
||||
var absolute = FeatureUtils.ToAbsoluteCopy(call.Program);
|
||||
var absolute = PostedCopy(call.Program);
|
||||
EnsureLeadingRapid(absolute);
|
||||
|
||||
var canonical = ProgramToCanonical(absolute);
|
||||
@@ -232,6 +253,11 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
);
|
||||
else if (code is RapidMove rm)
|
||||
sb.Append(CultureInfo.InvariantCulture, $"R{rm.EndPoint.X:F6},{rm.EndPoint.Y:F6}");
|
||||
else if (code is SubProgramCall { Program: not null } call)
|
||||
sb.Append(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"S{call.Offset.X:F6},{call.Offset.Y:F6}[{ProgramToCanonical(PostedCopy(call.Program))}]"
|
||||
);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -5,33 +5,6 @@ using System.Linq;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies how coordinate positioning is handled between parts.
|
||||
/// </summary>
|
||||
public enum CoordinateMode
|
||||
{
|
||||
/// <summary>Set absolute position.</summary>
|
||||
G92,
|
||||
|
||||
/// <summary>Use relative/incremental positioning.</summary>
|
||||
G91,
|
||||
|
||||
/// <summary>Use machine coordinate system.</summary>
|
||||
G53,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how G89 (hole drilling/tapping parameters) are provided.
|
||||
/// </summary>
|
||||
public enum G89Mode
|
||||
{
|
||||
/// <summary>Use external library file for G89 parameters.</summary>
|
||||
LibraryFile,
|
||||
|
||||
/// <summary>Explicitly define G89 parameters in the program.</summary>
|
||||
Explicit,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies where kerf compensation is applied.
|
||||
/// </summary>
|
||||
@@ -57,17 +30,20 @@ namespace OpenNest.Posts.Cincinnati
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how M47 (optional stop) commands are used.
|
||||
/// Specifies how the M47 head raise before each rapid is posted.
|
||||
/// </summary>
|
||||
public enum M47Mode
|
||||
{
|
||||
/// <summary>Always include M47.</summary>
|
||||
Always,
|
||||
|
||||
/// <summary>Include M47 with block delete functionality.</summary>
|
||||
/// <summary>Post /M47, which the operator can skip with block delete.</summary>
|
||||
BlockDelete,
|
||||
|
||||
/// <summary>Automatically determine M47 placement.</summary>
|
||||
/// <summary>
|
||||
/// M47 before rapids longer than the M47 override distance threshold, /M47 before
|
||||
/// shorter ones. Without a threshold every raise is M47.
|
||||
/// </summary>
|
||||
Auto,
|
||||
|
||||
/// <summary>Do not use M47.</summary>
|
||||
@@ -120,11 +96,6 @@ namespace OpenNest.Posts.Cincinnati
|
||||
[Description("Starting line number for features.")]
|
||||
public int FeatureLineNumberStart { get; set; } = 1;
|
||||
|
||||
[Category("2. Subprograms")]
|
||||
[DisplayName("Use Sheet Subprograms")]
|
||||
[Description("Use subprograms for sheet operations.")]
|
||||
public bool UseSheetSubprograms { get; set; } = true;
|
||||
|
||||
[Category("2. Subprograms")]
|
||||
[DisplayName("Sheet Subprogram Start")]
|
||||
[Description("Starting subprogram number for sheet operations.")]
|
||||
@@ -147,16 +118,6 @@ namespace OpenNest.Posts.Cincinnati
|
||||
[Description("Subprogram number for variable declarations.")]
|
||||
public int VariableDeclarationSubprogram { get; set; } = 100;
|
||||
|
||||
[Category("3. Positioning")]
|
||||
[DisplayName("Coordinate Mode Between Parts")]
|
||||
[Description("How coordinate positioning is handled between parts (G92, G91, or G53).")]
|
||||
public CoordinateMode CoordModeBetweenParts { get; set; } = CoordinateMode.G92;
|
||||
|
||||
[Category("4. Process")]
|
||||
[DisplayName("Process Parameter Mode")]
|
||||
[Description("How G89 parameters are provided (LibraryFile or Explicit).")]
|
||||
public G89Mode ProcessParameterMode { get; set; } = G89Mode.LibraryFile;
|
||||
|
||||
[Category("4. Process")]
|
||||
[DisplayName("Default Assist Gas")]
|
||||
[Description("Default assist gas when Nest.AssistGas is empty.")]
|
||||
@@ -199,22 +160,28 @@ namespace OpenNest.Posts.Cincinnati
|
||||
|
||||
[Category("6. M47 (Optional Stop)")]
|
||||
[DisplayName("Interior M47")]
|
||||
[Description("How M47 is used in interior cuts.")]
|
||||
[Description("How M47 is posted after features other than a part's exterior, such as cutouts and etch.")]
|
||||
public M47Mode InteriorM47 { get; set; } = M47Mode.Always;
|
||||
|
||||
[Category("6. M47 (Optional Stop)")]
|
||||
[DisplayName("Exterior M47")]
|
||||
[Description("How M47 is used in exterior cuts.")]
|
||||
[Description(
|
||||
"How M47 is posted after a part's exterior: the cut contour spanning the part's cut extents, or a part sub-program call."
|
||||
)]
|
||||
public M47Mode ExteriorM47 { get; set; } = M47Mode.Always;
|
||||
|
||||
[Category("6. M47 (Optional Stop)")]
|
||||
[DisplayName("M47 Override Distance Threshold")]
|
||||
[Description("Distance threshold for M47 override. Null = no override.")]
|
||||
[Description(
|
||||
"Auto M47 mode: rapids to the next pierce longer than this get M47, shorter ones /M47. Program units. Null = Auto always posts M47."
|
||||
)]
|
||||
public double? M47OverrideDistanceThreshold { get; set; } = null;
|
||||
|
||||
[Category("7. Safety")]
|
||||
[DisplayName("Safety Headraise Distance")]
|
||||
[Description("Safety head raise distance in machine units. Null = disabled.")]
|
||||
[Description(
|
||||
"Safety head raise distance in machine units, posted as M47 P<distance> before the rapid to a different part. Null = disabled."
|
||||
)]
|
||||
public int? SafetyHeadraiseDistance { get; set; } = 2000;
|
||||
|
||||
[Category("8. Pallet")]
|
||||
@@ -309,21 +276,6 @@ namespace OpenNest.Posts.Cincinnati
|
||||
)]
|
||||
[TypeConverter(typeof(MaterialLibraryNameConverter))]
|
||||
public string SelectedLibrary { get; set; } = "";
|
||||
|
||||
public string FindBestLibrary(string materialName, double thickness)
|
||||
{
|
||||
if (MaterialLibraries == null || string.IsNullOrEmpty(materialName))
|
||||
return "";
|
||||
|
||||
return MaterialLibraries
|
||||
.Where(e =>
|
||||
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
.OrderBy(e => System.Math.Abs(e.Thickness - thickness))
|
||||
.Select(e => e.Library)
|
||||
.FirstOrDefault()
|
||||
?? "";
|
||||
}
|
||||
}
|
||||
|
||||
public class MaterialLibraryEntry
|
||||
|
||||
@@ -38,11 +38,24 @@ namespace OpenNest.Posts.Cincinnati
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-selects the library auto-resolution would post for this nest (material,
|
||||
/// thickness within tolerance and assist gas), so the configuration dialog shows
|
||||
/// it. With no matching entry the selection is cleared rather than guessed.
|
||||
/// </summary>
|
||||
public void PrepareForNest(Nest nest)
|
||||
{
|
||||
var materialName = nest?.Material?.Name ?? "";
|
||||
var thickness = nest?.Thickness ?? 0.0;
|
||||
Config.SelectedLibrary = Config.FindBestLibrary(materialName, thickness);
|
||||
Config.SelectedLibrary = "";
|
||||
if (nest == null)
|
||||
return;
|
||||
|
||||
var entry = MaterialLibraryResolver.FindCutEntry(
|
||||
Config.MaterialLibraries,
|
||||
nest.Material?.Name ?? "",
|
||||
nest.Thickness,
|
||||
MaterialLibraryResolver.ResolveGas(nest, Config)
|
||||
);
|
||||
Config.SelectedLibrary = entry?.Library ?? "";
|
||||
}
|
||||
|
||||
public CincinnatiPostProcessor()
|
||||
@@ -105,7 +118,7 @@ namespace OpenNest.Posts.Cincinnati
|
||||
);
|
||||
|
||||
// 5. Build part sub-program registry (if enabled)
|
||||
Dictionary<(int, long), int> partSubprograms = null;
|
||||
Dictionary<Part, PartSubprogram> partSubprograms = null;
|
||||
List<(int subNum, string name, Program program)> subprogramEntries = null;
|
||||
|
||||
if (Config.UsePartSubprograms)
|
||||
|
||||
@@ -57,10 +57,7 @@ public sealed class CincinnatiPreambleWriter
|
||||
|
||||
w.WriteLine("M42");
|
||||
|
||||
if (
|
||||
_config.ProcessParameterMode == G89Mode.LibraryFile
|
||||
&& !string.IsNullOrEmpty(initialLibrary)
|
||||
)
|
||||
if (!string.IsNullOrEmpty(initialLibrary))
|
||||
w.WriteLine($"G89 P{initialLibrary}");
|
||||
|
||||
w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Posts.Cincinnati;
|
||||
|
||||
@@ -38,8 +38,8 @@ public sealed class CincinnatiSheetWriter
|
||||
/// <param name="cutLibrary">Resolved G89 library file for cut operations.</param>
|
||||
/// <param name="etchLibrary">Resolved G89 library file for etch operations.</param>
|
||||
/// <param name="partSubprograms">
|
||||
/// Optional mapping of (drawingId, rotationKey) to sub-program number.
|
||||
/// When provided, non-cutoff parts are emitted as M98 calls instead of inline features.
|
||||
/// Optional mapping of parts (by reference) to their posted part sub-programs.
|
||||
/// When provided, mapped non-cutoff parts are emitted as M98 calls instead of inline features.
|
||||
/// </param>
|
||||
public void Write(
|
||||
TextWriter w,
|
||||
@@ -49,7 +49,7 @@ public sealed class CincinnatiSheetWriter
|
||||
int subNumber,
|
||||
string cutLibrary,
|
||||
string etchLibrary,
|
||||
Dictionary<(int, long), int> partSubprograms = null,
|
||||
IReadOnlyDictionary<Part, PartSubprogram> partSubprograms = null,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping = null
|
||||
)
|
||||
{
|
||||
@@ -87,36 +87,18 @@ public sealed class CincinnatiSheetWriter
|
||||
w.WriteLine("M47");
|
||||
if (!string.IsNullOrEmpty(cutLibrary))
|
||||
w.WriteLine($"G89 P{cutLibrary}");
|
||||
w.WriteLine("GOTO1( Goto Feature )");
|
||||
|
||||
// The operator edits this target to restart at a feature. It names the first
|
||||
// feature's line number, which exists only when line numbers are posted.
|
||||
if (_config.UseLineNumbers)
|
||||
w.WriteLine($"GOTO{_config.FeatureLineNumberStart}( Goto Feature )");
|
||||
|
||||
// 3. Plate order is the cut sequence, cut-offs included: manual, cut-first
|
||||
// and automatic sequencing all set it, so the post must not reorder.
|
||||
var allParts = plate.Parts.ToList();
|
||||
var items = BuildItems(plate.Parts, partSubprograms);
|
||||
|
||||
// 4. Emit parts
|
||||
if (partSubprograms != null)
|
||||
WritePartsWithSubprograms(
|
||||
w,
|
||||
allParts,
|
||||
cutLibrary,
|
||||
etchLibrary,
|
||||
sheetDiagonal,
|
||||
width,
|
||||
length,
|
||||
partSubprograms,
|
||||
userVarMapping
|
||||
);
|
||||
else
|
||||
WritePartsInline(
|
||||
w,
|
||||
allParts,
|
||||
cutLibrary,
|
||||
etchLibrary,
|
||||
sheetDiagonal,
|
||||
width,
|
||||
length,
|
||||
userVarMapping
|
||||
);
|
||||
WriteItems(w, items, cutLibrary, etchLibrary, sheetDiagonal, width, length, userVarMapping);
|
||||
|
||||
// 5. Footer
|
||||
w.WriteLine("M42");
|
||||
@@ -125,182 +107,135 @@ public sealed class CincinnatiSheetWriter
|
||||
w.WriteLine($"M99 (END OF {nestName}.{layoutIndex:D3})");
|
||||
}
|
||||
|
||||
private void WritePartsWithSubprograms(
|
||||
/// <summary>
|
||||
/// One posted step of the sheet: a part feature, a hole call or a part sub-program
|
||||
/// call. Points are in sheet coordinates.
|
||||
/// </summary>
|
||||
private sealed class SheetItem
|
||||
{
|
||||
public Part Part { get; init; }
|
||||
public List<ICode> Codes { get; init; }
|
||||
public bool IsEtch { get; init; }
|
||||
public bool IsExterior { get; init; }
|
||||
public PartSubprogram Subprogram { get; init; }
|
||||
|
||||
/// <summary>First rapid target: the sub-program corner, else the first pierce.</summary>
|
||||
public Vector Entry { get; init; }
|
||||
|
||||
public Vector Start { get; init; }
|
||||
public Vector End { get; init; }
|
||||
|
||||
/// <summary>Where the tool leaves from: back at the corner after a sub-program call.</summary>
|
||||
public Vector Exit { get; init; }
|
||||
}
|
||||
|
||||
private static List<SheetItem> BuildItems(
|
||||
IEnumerable<Part> parts,
|
||||
IReadOnlyDictionary<Part, PartSubprogram> partSubprograms
|
||||
)
|
||||
{
|
||||
var items = new List<SheetItem>();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (
|
||||
partSubprograms != null
|
||||
&& !part.BaseDrawing.IsCutOff
|
||||
&& partSubprograms.TryGetValue(part, out var sub)
|
||||
)
|
||||
{
|
||||
var subFeatures = FeatureUtils.ClassifyAndOrder(
|
||||
FeatureUtils.SplitByRapids(sub.Program.Codes)
|
||||
);
|
||||
if (subFeatures.Count == 0)
|
||||
continue;
|
||||
|
||||
var corner = part.Location + sub.Corner;
|
||||
items.Add(
|
||||
new SheetItem
|
||||
{
|
||||
Part = part,
|
||||
Subprogram = sub,
|
||||
Entry = corner,
|
||||
Start = corner + FeatureUtils.StartPoint(subFeatures[0].codes),
|
||||
End = corner + FeatureUtils.EndPoint(subFeatures[^1].codes),
|
||||
Exit = corner,
|
||||
}
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var features = FeatureUtils.SplitAndClassify(part);
|
||||
var exterior = FeatureUtils.FindExterior(features);
|
||||
for (var f = 0; f < features.Count; f++)
|
||||
{
|
||||
var (codes, isEtch) = features[f];
|
||||
var start = part.Location + FeatureUtils.StartPoint(codes);
|
||||
var end = part.Location + FeatureUtils.EndPoint(codes);
|
||||
items.Add(
|
||||
new SheetItem
|
||||
{
|
||||
Part = part,
|
||||
Codes = codes,
|
||||
IsEtch = isEtch,
|
||||
IsExterior = exterior[f],
|
||||
Entry = start,
|
||||
Start = start,
|
||||
End = end,
|
||||
Exit = end,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private void WriteItems(
|
||||
TextWriter w,
|
||||
List<Part> allParts,
|
||||
List<SheetItem> items,
|
||||
string cutLibrary,
|
||||
string etchLibrary,
|
||||
double sheetDiagonal,
|
||||
double plateWidth,
|
||||
double plateLength,
|
||||
Dictionary<(int, long), int> partSubprograms,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping
|
||||
)
|
||||
{
|
||||
var lastPartName = "";
|
||||
var featureIndex = 0;
|
||||
|
||||
for (var p = 0; p < allParts.Count; p++)
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var part = allParts[p];
|
||||
var item = items[i];
|
||||
var part = item.Part;
|
||||
var partName = part.BaseDrawing.Name;
|
||||
var isNewPart = partName != lastPartName;
|
||||
var isSafetyHeadraise = isNewPart && lastPartName != "";
|
||||
var isLastPart = p == allParts.Count - 1;
|
||||
var isFirstFeatureOfPart = partName != lastPartName;
|
||||
var next = i + 1 < items.Count ? items[i + 1] : null;
|
||||
var isLastFeature = next == null;
|
||||
|
||||
var key = CincinnatiPartSubprogramWriter.SubprogramKey(part);
|
||||
partSubprograms.TryGetValue(key, out var subNum);
|
||||
var hasSubprogram = !part.BaseDrawing.IsCutOff && subNum != 0;
|
||||
// A head raise belongs before the rapid that leaves this item, so the safety
|
||||
// raise goes on a part's last item when a different part follows.
|
||||
var isSafetyHeadraise = next != null && next.Part.BaseDrawing.Name != partName;
|
||||
double? rapidDistance =
|
||||
next == null
|
||||
? null
|
||||
: item.End.DistanceTo(item.Exit)
|
||||
+ item.Exit.DistanceTo(next.Entry)
|
||||
+ next.Entry.DistanceTo(next.Start);
|
||||
lastPartName = partName;
|
||||
|
||||
if (hasSubprogram)
|
||||
if (item.Subprogram != null)
|
||||
{
|
||||
WriteSubprogramCall(
|
||||
w,
|
||||
part,
|
||||
subNum,
|
||||
featureIndex,
|
||||
item,
|
||||
i,
|
||||
partName,
|
||||
isSafetyHeadraise,
|
||||
isLastPart
|
||||
rapidDistance,
|
||||
isLastFeature
|
||||
);
|
||||
featureIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inline features for cutoffs or parts without sub-programs
|
||||
var features = FeatureUtils.SplitAndClassify(part);
|
||||
for (var f = 0; f < features.Count; f++)
|
||||
{
|
||||
var (codes, isEtch) = features[f];
|
||||
var isLastFeature = isLastPart && f == features.Count - 1;
|
||||
|
||||
// SubProgramCall features are emitted as M98 hole calls
|
||||
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
|
||||
{
|
||||
CincinnatiHoleCallWriter.Write(
|
||||
w,
|
||||
holeCall,
|
||||
part.Location,
|
||||
featureIndex,
|
||||
isLastFeature,
|
||||
_config,
|
||||
_fmt,
|
||||
_holeSubprograms,
|
||||
isNewPart && f == 0 ? partName : null,
|
||||
isSafetyHeadraise && f == 0
|
||||
);
|
||||
featureIndex++;
|
||||
lastPartName = partName;
|
||||
continue;
|
||||
}
|
||||
|
||||
var featureNumber =
|
||||
featureIndex == 0
|
||||
? _config.FeatureLineNumberStart
|
||||
: 1000 + featureIndex + 1;
|
||||
|
||||
var cutDistance = FeatureUtils.ComputeCutDistance(codes);
|
||||
|
||||
var ctx = new FeatureContext
|
||||
{
|
||||
Codes = codes,
|
||||
FeatureNumber = featureNumber,
|
||||
PartName = partName,
|
||||
IsFirstFeatureOfPart = isNewPart && f == 0,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsSafetyHeadraise = isSafetyHeadraise && f == 0,
|
||||
IsExteriorFeature = false,
|
||||
IsEtch = isEtch,
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id,
|
||||
IsCutOff = part.BaseDrawing.IsCutOff,
|
||||
PlateWidth = plateWidth,
|
||||
PlateLength = plateLength,
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
featureIndex++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
lastPartName = partName;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteSubprogramCall(
|
||||
TextWriter w,
|
||||
Part part,
|
||||
int subNum,
|
||||
int featureIndex,
|
||||
string partName,
|
||||
bool isSafetyHeadraise,
|
||||
bool isLastPart
|
||||
)
|
||||
{
|
||||
// Safety headraise before rapid to new part
|
||||
if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue)
|
||||
w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value} (Safety Headraise)");
|
||||
|
||||
// Rapid to part position (bounding box lower-left)
|
||||
var featureNumber =
|
||||
featureIndex == 0 ? _config.FeatureLineNumberStart : 1000 + featureIndex + 1;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
if (_config.UseLineNumbers)
|
||||
sb.Append($"N{featureNumber} ");
|
||||
sb.Append($"G0 X{_fmt.FormatCoord(part.Left)} Y{_fmt.FormatCoord(part.Bottom)}");
|
||||
w.WriteLine(sb.ToString());
|
||||
|
||||
// Part name comment
|
||||
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
|
||||
|
||||
// Set local coordinate system at part position
|
||||
w.WriteLine("G92 X0 Y0");
|
||||
|
||||
// Call part sub-program
|
||||
w.WriteLine($"M98 P{subNum} ({partName})");
|
||||
|
||||
// Restore sheet coordinate system
|
||||
w.WriteLine($"G92 X{_fmt.FormatCoord(part.Left)} Y{_fmt.FormatCoord(part.Bottom)}");
|
||||
|
||||
// Head raise (unless last part on sheet)
|
||||
if (!isLastPart)
|
||||
w.WriteLine("M47");
|
||||
}
|
||||
|
||||
private void WritePartsInline(
|
||||
TextWriter w,
|
||||
List<Part> allParts,
|
||||
string cutLibrary,
|
||||
string etchLibrary,
|
||||
double sheetDiagonal,
|
||||
double plateWidth,
|
||||
double plateLength,
|
||||
Dictionary<(int drawingId, string varName), int> userVarMapping
|
||||
)
|
||||
{
|
||||
// Split and classify features, ordering etch before cut per part
|
||||
var features = new List<(Part part, List<ICode> codes, bool isEtch)>();
|
||||
foreach (var part in allParts)
|
||||
{
|
||||
var partFeatures = FeatureUtils.SplitAndClassify(part);
|
||||
foreach (var (codes, isEtch) in partFeatures)
|
||||
features.Add((part, codes, isEtch));
|
||||
}
|
||||
|
||||
// Emit features
|
||||
var lastPartName = "";
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
{
|
||||
var (part, codes, isEtch) = features[i];
|
||||
var partName = part.BaseDrawing.Name;
|
||||
var isFirstFeatureOfPart = partName != lastPartName;
|
||||
var isSafetyHeadraise = partName != lastPartName && lastPartName != "";
|
||||
var isLastFeature = i == features.Count - 1;
|
||||
var codes = item.Codes;
|
||||
|
||||
// SubProgramCall features are emitted as M98 hole calls
|
||||
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
|
||||
@@ -317,27 +252,23 @@ public sealed class CincinnatiSheetWriter
|
||||
isFirstFeatureOfPart ? partName : null,
|
||||
isSafetyHeadraise
|
||||
);
|
||||
lastPartName = partName;
|
||||
continue;
|
||||
}
|
||||
|
||||
var featureNumber = i == 0 ? _config.FeatureLineNumberStart : 1000 + i + 1;
|
||||
|
||||
var cutDistance = FeatureUtils.ComputeCutDistance(codes);
|
||||
|
||||
var ctx = new FeatureContext
|
||||
{
|
||||
Codes = codes,
|
||||
FeatureNumber = featureNumber,
|
||||
FeatureNumber = FeatureNumber(i),
|
||||
PartName = partName,
|
||||
IsFirstFeatureOfPart = isFirstFeatureOfPart,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsSafetyHeadraise = isSafetyHeadraise,
|
||||
IsExteriorFeature = false,
|
||||
IsEtch = isEtch,
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = cutDistance,
|
||||
IsExteriorFeature = item.IsExterior,
|
||||
IsEtch = item.IsEtch,
|
||||
LibraryFile = item.IsEtch ? etchLibrary : cutLibrary,
|
||||
CutDistance = FeatureUtils.ComputeCutDistance(codes),
|
||||
SheetDiagonal = sheetDiagonal,
|
||||
RapidDistance = rapidDistance,
|
||||
PartLocation = part.Location,
|
||||
UserVariableMapping = userVarMapping,
|
||||
DrawingId = part.BaseDrawing.Id,
|
||||
@@ -347,7 +278,59 @@ public sealed class CincinnatiSheetWriter
|
||||
};
|
||||
|
||||
_featureWriter.Write(w, ctx);
|
||||
lastPartName = partName;
|
||||
}
|
||||
}
|
||||
|
||||
private int FeatureNumber(int index) =>
|
||||
index == 0 ? _config.FeatureLineNumberStart : 1000 + index + 1;
|
||||
|
||||
/// <summary>
|
||||
/// Calls a part sub-program in a G92 frame at the part corner (§1.92). After the
|
||||
/// call the head is raised, the tool rapids back to local 0,0 and G92 gives that
|
||||
/// point its sheet coordinates again, so the frame is restored where the tool really
|
||||
/// is. The last part skips the restore: nothing moves before the next sheet
|
||||
/// re-anchors with G92 X#5021 Y#5022.
|
||||
/// </summary>
|
||||
private void WriteSubprogramCall(
|
||||
TextWriter w,
|
||||
SheetItem item,
|
||||
int index,
|
||||
string partName,
|
||||
bool isSafetyHeadraise,
|
||||
double? rapidDistance,
|
||||
bool isLastPart
|
||||
)
|
||||
{
|
||||
var x = _fmt.FormatCoord(item.Entry.X);
|
||||
var y = _fmt.FormatCoord(item.Entry.Y);
|
||||
|
||||
// Rapid to the sub-program origin (the part's toolpath corner)
|
||||
var sb = new StringBuilder();
|
||||
if (_config.UseLineNumbers)
|
||||
sb.Append($"N{FeatureNumber(index)} ");
|
||||
sb.Append($"G0 X{x} Y{y}");
|
||||
w.WriteLine(sb.ToString());
|
||||
|
||||
// Part name comment
|
||||
w.WriteLine(CoordinateFormatter.Comment($"PART: {partName}"));
|
||||
|
||||
// Set local coordinate system at part position
|
||||
w.WriteLine("G92 X0 Y0");
|
||||
|
||||
// Call part sub-program
|
||||
w.WriteLine($"M98 P{item.Subprogram.Number} ({partName})");
|
||||
|
||||
if (isLastPart)
|
||||
return;
|
||||
|
||||
// One head raise covers the rapid back and the rapid on. The part is complete,
|
||||
// so the raise follows the exterior M47 mode.
|
||||
var raise = CincinnatiHeadRaise.Line(_config, isSafetyHeadraise, true, rapidDistance);
|
||||
if (raise != null)
|
||||
w.WriteLine(raise);
|
||||
|
||||
// Restore the sheet coordinate system at the corner
|
||||
w.WriteLine("G0 X0 Y0");
|
||||
w.WriteLine($"G92 X{x} Y{y}");
|
||||
}
|
||||
}
|
||||
@@ -78,17 +78,205 @@ public static class FeatureUtils
|
||||
|
||||
/// <summary>
|
||||
/// Splits a part's program into features by rapids, classifies each as etch or cut,
|
||||
/// and orders etch features before cut features.
|
||||
/// and orders etch features before cut features. The part's program is not modified.
|
||||
/// </summary>
|
||||
public static List<(List<ICode> codes, bool isEtch)> SplitAndClassify(Part part)
|
||||
{
|
||||
part.Program.Mode = Mode.Absolute;
|
||||
var codes = new List<ICode>(part.Program.Codes);
|
||||
var codes = RemoveSuppressed(AbsoluteCodes(part.Program));
|
||||
InsertLeadingRapid(codes);
|
||||
|
||||
return ClassifyAndOrder(SplitByRapids(codes));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <paramref name="program"/>'s codes in absolute mode without changing the
|
||||
/// program. Hole calls are the program's own instances, so lookups by hole program
|
||||
/// reference still match; every other code of an incremental program is a copy.
|
||||
/// </summary>
|
||||
public static List<ICode> AbsoluteCodes(Program program)
|
||||
{
|
||||
if (program.Mode == Mode.Absolute)
|
||||
return new List<ICode>(program.Codes);
|
||||
|
||||
var copy = ToAbsoluteCopy(program);
|
||||
var codes = new List<ICode>(copy.Codes.Count);
|
||||
for (var i = 0; i < copy.Codes.Count; i++)
|
||||
codes.Add(copy.Codes[i] is SubProgramCall ? program.Codes[i] : copy.Codes[i]);
|
||||
return codes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leaves suppressed moves uncut, matching the other posts and the cut simulation.
|
||||
/// Absolute codes only. A cut that resumes after a suppressed run starts with a
|
||||
/// rapid to the run's end, so the gap is crossed with the beam off rather than cut
|
||||
/// as a chord. Rapids left with nothing to cut, and hole calls whose moves are all
|
||||
/// suppressed, are dropped so they cannot post an empty pierce.
|
||||
/// </summary>
|
||||
public static List<ICode> RemoveSuppressed(List<ICode> codes)
|
||||
{
|
||||
var kept = new List<ICode>(codes.Count);
|
||||
Vector? resumeAt = null;
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is Motion { Suppressed: true } suppressed)
|
||||
{
|
||||
resumeAt = suppressed.EndPoint;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code is SubProgramCall call && !HasCut(call.Program))
|
||||
continue;
|
||||
|
||||
if (resumeAt.HasValue && code is Motion and not RapidMove)
|
||||
kept.Add(new RapidMove(resumeAt.Value));
|
||||
resumeAt = null;
|
||||
kept.Add(code);
|
||||
}
|
||||
|
||||
var result = new List<ICode>(kept.Count);
|
||||
for (var i = 0; i < kept.Count; i++)
|
||||
{
|
||||
if (kept[i] is RapidMove && !CutFollows(kept, i + 1))
|
||||
continue;
|
||||
result.Add(kept[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// True when a cutting move comes before the next rapid or hole call.
|
||||
private static bool CutFollows(List<ICode> codes, int start)
|
||||
{
|
||||
for (var i = start; i < codes.Count; i++)
|
||||
{
|
||||
if (codes[i] is RapidMove or SubProgramCall)
|
||||
return false;
|
||||
if (codes[i] is Motion)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>True when <paramref name="program"/> has an unsuppressed cutting move.</summary>
|
||||
public static bool HasCut(Program program)
|
||||
{
|
||||
if (program == null)
|
||||
return false;
|
||||
foreach (var code in program.Codes)
|
||||
{
|
||||
if (code is Motion { Suppressed: false } and not RapidMove)
|
||||
return true;
|
||||
if (code is SubProgramCall call && HasCut(call.Program))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The pierce point of a feature in its program's frame: its first rapid, or for a
|
||||
/// hole call the hole's first pierce shifted to the call offset.
|
||||
/// </summary>
|
||||
public static Vector StartPoint(List<ICode> feature)
|
||||
{
|
||||
if (feature.Count == 1 && feature[0] is SubProgramCall call && call.Program != null)
|
||||
return call.Offset + StartPoint(AbsoluteCodes(call.Program));
|
||||
|
||||
foreach (var code in feature)
|
||||
{
|
||||
if (code is RapidMove rapid)
|
||||
return rapid.EndPoint;
|
||||
}
|
||||
foreach (var code in feature)
|
||||
{
|
||||
if (code is Motion motion)
|
||||
return motion.EndPoint;
|
||||
}
|
||||
return Vector.Zero;
|
||||
}
|
||||
|
||||
/// <summary>Where the tool stops at the end of a feature, in its program's frame.</summary>
|
||||
public static Vector EndPoint(List<ICode> feature)
|
||||
{
|
||||
if (feature.Count == 1 && feature[0] is SubProgramCall call && call.Program != null)
|
||||
return call.Offset + EndPoint(AbsoluteCodes(call.Program));
|
||||
|
||||
for (var i = feature.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (feature[i] is Motion motion)
|
||||
return motion.EndPoint;
|
||||
}
|
||||
return Vector.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks each part's exterior features: cut features whose cut moves (lead moves
|
||||
/// excluded) span the extents of all the part's cut moves. Etch features and hole
|
||||
/// calls are never exterior. A part whose outline is cut in pieces has none.
|
||||
/// </summary>
|
||||
public static bool[] FindExterior(IReadOnlyList<(List<ICode> codes, bool isEtch)> features)
|
||||
{
|
||||
var bounds = new Box[features.Count];
|
||||
Box all = null;
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
{
|
||||
var (codes, isEtch) = features[i];
|
||||
if (isEtch || (codes.Count == 1 && codes[0] is SubProgramCall))
|
||||
continue;
|
||||
bounds[i] = CutBounds(codes);
|
||||
if (bounds[i] != null)
|
||||
all = all == null ? bounds[i] : Union(all, bounds[i]);
|
||||
}
|
||||
|
||||
var result = new bool[features.Count];
|
||||
for (var i = 0; i < features.Count; i++)
|
||||
result[i] = bounds[i] != null && SameExtents(bounds[i], all);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Box CutBounds(List<ICode> codes)
|
||||
{
|
||||
var cut = new Program(Mode.Absolute);
|
||||
var position = Vector.Zero;
|
||||
var inCut = false;
|
||||
foreach (var code in codes)
|
||||
{
|
||||
if (code is not Motion motion)
|
||||
continue;
|
||||
|
||||
var layer = motion switch
|
||||
{
|
||||
LinearMove linear => linear.Layer,
|
||||
ArcMove arc => arc.Layer,
|
||||
_ => LayerType.Leadin,
|
||||
};
|
||||
var isCut = motion is not RapidMove && layer is not (LayerType.Leadin or LayerType.Leadout);
|
||||
if (isCut)
|
||||
{
|
||||
if (!inCut)
|
||||
cut.Codes.Add(new RapidMove(position));
|
||||
cut.Codes.Add(motion);
|
||||
}
|
||||
inCut = isCut;
|
||||
position = motion.EndPoint;
|
||||
}
|
||||
|
||||
return cut.Codes.Count == 0 ? null : cut.BoundingBox();
|
||||
}
|
||||
|
||||
private static Box Union(Box a, Box b)
|
||||
{
|
||||
var left = System.Math.Min(a.Left, b.Left);
|
||||
var bottom = System.Math.Min(a.Bottom, b.Bottom);
|
||||
var right = System.Math.Max(a.Right, b.Right);
|
||||
var top = System.Math.Max(a.Top, b.Top);
|
||||
return new Box(left, bottom, right - left, top - bottom);
|
||||
}
|
||||
|
||||
private static bool SameExtents(Box a, Box b) =>
|
||||
a.Left.IsEqualTo(b.Left)
|
||||
&& a.Right.IsEqualTo(b.Right)
|
||||
&& a.Bottom.IsEqualTo(b.Bottom)
|
||||
&& a.Top.IsEqualTo(b.Top);
|
||||
|
||||
/// <summary>
|
||||
/// If the first contour motion has no rapid before it, the feature writer would
|
||||
/// pierce at that motion's endpoint and lose the first segment. Inserts a synthetic
|
||||
|
||||
@@ -24,13 +24,26 @@ public sealed class MaterialLibraryResolver
|
||||
if (!string.IsNullOrEmpty(_selectedLibrary))
|
||||
return EnsureLibExtension(_selectedLibrary);
|
||||
|
||||
var entry = _materialLibraries.FirstOrDefault(e =>
|
||||
var entry = FindCutEntry(_materialLibraries, materialName, thickness, gas);
|
||||
return EnsureLibExtension(entry?.Library ?? "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The first cut library entry for this material, thickness (within tolerance) and
|
||||
/// assist gas, or null when none matches.
|
||||
/// </summary>
|
||||
public static MaterialLibraryEntry FindCutEntry(
|
||||
IEnumerable<MaterialLibraryEntry> entries,
|
||||
string materialName,
|
||||
double thickness,
|
||||
string gas
|
||||
)
|
||||
{
|
||||
return entries?.FirstOrDefault(e =>
|
||||
string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase)
|
||||
&& System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance
|
||||
&& string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
return EnsureLibExtension(entry?.Library ?? "");
|
||||
}
|
||||
|
||||
public string ResolveEtchLibrary(string gas)
|
||||
|
||||
@@ -4,13 +4,10 @@
|
||||
"PostedAccuracy": 4,
|
||||
"UseLineNumbers": true,
|
||||
"FeatureLineNumberStart": 1,
|
||||
"UseSheetSubprograms": true,
|
||||
"SheetSubprogramStart": 101,
|
||||
"UsePartSubprograms": false,
|
||||
"PartSubprogramStart": 200,
|
||||
"VariableDeclarationSubprogram": 100,
|
||||
"CoordModeBetweenParts": "G92",
|
||||
"ProcessParameterMode": "LibraryFile",
|
||||
"DefaultAssistGas": "O2",
|
||||
"DefaultEtchGas": "N2",
|
||||
"UseExactStopMode": false,
|
||||
|
||||
@@ -37,8 +37,11 @@ A generated file contains, in order:
|
||||
never reorders them.
|
||||
|
||||
4. **Part sub-programs** (`CincinnatiPartSubprogramWriter.Write`), one per
|
||||
unique `(drawing, rotation)` pair, only emitted when
|
||||
`Config.UsePartSubprograms` is enabled.
|
||||
unique posted part toolpath, only emitted when `Config.UsePartSubprograms`
|
||||
is enabled. Parts share a sub-program only when they have the same drawing
|
||||
name and their normalized programs (lead-ins, hole calls and suppression
|
||||
included) post identically, so parts of one drawing and rotation with
|
||||
different lead-ins get their own sub-programs.
|
||||
|
||||
5. **Hole sub-programs** (`CincinnatiPartSubprogramWriter.Write` reused with a
|
||||
`"HOLE"` label), one per unique posted hole toolpath. Calls are matched to
|
||||
@@ -49,6 +52,36 @@ A generated file contains, in order:
|
||||
|
||||
Sub-program bodies start with a `:<subNum>` label and end with `M99`.
|
||||
|
||||
The post reads part programs through absolute-mode copies and never changes the
|
||||
nest: an incremental part program stays incremental after posting.
|
||||
|
||||
### Sheet header
|
||||
|
||||
Each sheet sub-program re-anchors the work frame with `G92 X#5021 Y#5022`
|
||||
(§1.92; `#5021`/`#5022` are the machine X/Y system variables), loads the cut
|
||||
library with `G89 P<library>` (§2.89) when one resolved, and raises the head
|
||||
with `M47`. When `Config.UseLineNumbers` is on it then posts
|
||||
`GOTO<FeatureLineNumberStart>( Goto Feature )` (§5.5), which targets the
|
||||
sheet's first feature line; the operator edits it to restart at another
|
||||
feature. Without line numbers there is no target, so no `GOTO` is posted.
|
||||
|
||||
### Suppressed moves
|
||||
|
||||
Moves with `Motion.Suppressed` set are not cut, as in the CI Fiber post and the
|
||||
cut simulation. When cutting resumes after a suppressed run, the post ends the
|
||||
feature and starts a new one with a rapid to the run's end point, so the gap is
|
||||
crossed with the beam off. Rapids left with nothing to cut, and hole calls whose
|
||||
moves are all suppressed, are dropped.
|
||||
|
||||
### Cut library selection
|
||||
|
||||
`MaterialLibraryResolver` picks the first `MaterialLibraries` entry matching the
|
||||
nest material, thickness within 0.001 and assist gas (`Nest.AssistGas`, else
|
||||
`Config.DefaultAssistGas`). Before the configuration dialog opens,
|
||||
`PrepareForNest` pre-selects that same entry as `SelectedLibrary`, or clears the
|
||||
selection when nothing matches; it never substitutes another gas or thickness.
|
||||
A `SelectedLibrary` the operator then picks overrides auto-resolution.
|
||||
|
||||
## Feature blocks
|
||||
|
||||
A "feature" is a single contour: lead-in → cut moves → lead-out. Each feature
|
||||
@@ -58,7 +91,8 @@ block in a sheet or sub-program output follows this order
|
||||
1. `G0 X_ Y_` — rapid to the pierce point (§1.00).
|
||||
2. Optional part-name comment, only on the first feature of each part.
|
||||
3. `G89 P<library>` — load process parameters (§2.89). `P` is a library file
|
||||
name; the `(...)` trailing comment carries speed-class info.
|
||||
name; the `(...)` trailing comment carries speed-class info. With no
|
||||
resolved library the post writes `(WARNING: No library found)` instead.
|
||||
4. `G84` (cut) or `G85` (etch / no-pierce) — pierce and start cut, or start
|
||||
cut without pierce (§2.84 / §2.85).
|
||||
5. `M130 (ANTI DIVE OFF)` — disable anti-dive, only if configured (§3.130).
|
||||
@@ -72,13 +106,35 @@ block in a sheet or sub-program output follows this order
|
||||
7. `G40` — cancel kerf compensation (§1.40), only if it was applied.
|
||||
8. `M35` (or `M135` if SpeedGas is enabled) — beam off (§3.35 / §3.135).
|
||||
9. `M131 (ANTI DIVE ON)` — re-enable anti-dive (§3.131).
|
||||
10. `M47` or `M47 P<distance>` — raise Z-axis, unless this is the last feature
|
||||
on the sheet (§3.47). A leading `/` (block delete, §5.6) is prepended when
|
||||
the configured override distance exceeds the default.
|
||||
10. Head raise before the rapid to the next feature, unless this is the last
|
||||
feature on the sheet (§3.47). See [Head raises](#head-raises-m47).
|
||||
|
||||
Sheet sub-program and sheet-level feature calls add `G92 X#5021 Y#5022`
|
||||
(§1.92) at the top so the local origin is anchored to the machine's current
|
||||
absolute position (`#5021`/`#5022` are the machine X/Y system variables).
|
||||
## Head raises (M47)
|
||||
|
||||
Every raise is written at the end of the feature it follows, before the rapid
|
||||
that leaves it, and there is exactly one raise per rapid. The block is chosen
|
||||
in this order (`CincinnatiHeadRaise`):
|
||||
|
||||
1. `M47 P<SafetyHeadraiseDistance> (Safety Headraise)` when the next feature
|
||||
belongs to a part with a different drawing name and the distance is set.
|
||||
2. Otherwise the M47 mode for the feature just cut: `ExteriorM47` after a
|
||||
part's exterior, `InteriorM47` after anything else.
|
||||
- `Always`: `M47`.
|
||||
- `BlockDelete`: `/M47`; the operator can skip it with block delete (§5.6).
|
||||
- `Auto`: `M47` when the rapid travel to the next pierce exceeds
|
||||
`M47OverrideDistanceThreshold` (program units), `/M47` when it does not.
|
||||
With no threshold, `Auto` always posts `M47`.
|
||||
- `None`: no raise.
|
||||
|
||||
The exterior is the cut feature whose cut moves (lead-in and lead-out moves
|
||||
excluded) span the extents of all of the part's cut moves; etch features and
|
||||
hole calls are never exterior, and a part whose outline is cut in several
|
||||
features has no exterior. The raise after a part sub-program call follows
|
||||
`ExteriorM47`. These rules are this project's interpretation of the settings,
|
||||
not vendor-documented behavior.
|
||||
|
||||
Hole calls (see below) write `M47`, or the safety raise, after `G52 X0 Y0`
|
||||
regardless of the M47 mode.
|
||||
|
||||
## Sub-program call patterns
|
||||
|
||||
@@ -88,24 +144,32 @@ targets a whole-part sub-program or a hole sub-program.
|
||||
### Part sub-program call (`WriteSubprogramCall`)
|
||||
|
||||
Used when `Config.UsePartSubprograms` is enabled. The tool physically rapids
|
||||
to the part corner, then G92 sets the current position as the local origin,
|
||||
the sub-program executes in its own local coordinate frame, and G92 restores
|
||||
the original absolute position after return.
|
||||
to the corner of the part's toolpath, G92 sets that position as the local
|
||||
origin, and the sub-program executes in its own local coordinate frame. After
|
||||
it returns, the tool is wherever the sub-program's last feature ended, so the
|
||||
head is raised, the tool rapids back to local `0,0` (the corner) and G92 gives
|
||||
that point its sheet coordinates again.
|
||||
|
||||
```
|
||||
G0 X<left> Y<bottom> ; rapid to part bounding box corner (§1.00)
|
||||
(PART: <name>)
|
||||
G0 X<left> Y<bottom> ; rapid to the toolpath corner (§1.00)
|
||||
( PART: <name> )
|
||||
G92 X0 Y0 ; set local origin at current position (§1.92)
|
||||
M98 P<partSubNum> (<name>) ; call the part sub-program (§3.98)
|
||||
M47 ; head raise for the rapids that follow (§3.47)
|
||||
G0 X0 Y0 ; back to the corner (§1.00)
|
||||
G92 X<left> Y<bottom> ; restore the sheet coordinate system (§1.92)
|
||||
M47 ; head raise unless this is the last part (§3.47)
|
||||
```
|
||||
|
||||
This pattern uses G92 because the tool is physically positioned at the part
|
||||
corner first. The sub-program's coordinates are part-local, so they are
|
||||
interpreted against the new origin until G92 restores the sheet frame.
|
||||
The last part on a sheet stops after `M98`: nothing moves before the next
|
||||
sheet re-anchors with `G92 X#5021 Y#5022`. The sub-program's last feature
|
||||
posts no raise, so the raise after the call is the only one before the rapids,
|
||||
chosen by the [head raise](#head-raises-m47) rules.
|
||||
|
||||
### Hole sub-program call (`WriteHoleSubprogramCall`)
|
||||
This pattern uses G92 rather than G52 because hole calls inside a part
|
||||
sub-program use G52 themselves, and a nested G52 would replace the part shift
|
||||
instead of adding to it.
|
||||
|
||||
### Hole sub-program call (`CincinnatiHoleCallWriter.Write`)
|
||||
|
||||
Used for the `SubProgramCall` codes that a `ContourCuttingStrategy` emits for
|
||||
each circular hole. Unlike parts, we do **not** want a physical rapid to the
|
||||
|
||||
Reference in new issue
Block a user