mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-11 14:31:11 -05:00
Ahead of the new laser schedule, three ways a post could silently cut with the wrong process, and one way its settings silently reset: - Cincinnati CL wrote no G89 when no cut library matched the nest's material, thickness and gas (or no etch library for etch features), so the controller cut with whatever library it loaded last. It now refuses, unless the new "Require libraries" setting is turned off. - Cincinnati CL declared G20/G21 from its own setting without comparing the nest's units, so a metric nest posted 25.4x too large. It now refuses a mismatch. - CI Fiber posted any material missing from its table as the default code (MSN), so stainless or aluminum ran with mild-steel technology. A named, unmapped material is now refused; the default code covers only nests with no material. - Posts saved their settings beside their DLL, inside the install folder a Velopack update replaces, so every update reset them to the shipped defaults. Saves now go to %APPDATA%\OpenNest\Posts and the shipped JSON only seeds them; constructing a post no longer writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
7.4 KiB
C#
209 lines
7.4 KiB
C#
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,
|
|
RequireLibraries = false,
|
|
};
|
|
|
|
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);
|
|
}
|