mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 18:31:08 -04:00
The safety raise (M47 P<SafetyHeadraiseDistance>) used to fire only when the next part had a different drawing name, which has nothing to do with crash risk: a long rapid between copies of the same part got a plain M47, while a short hop between neighbouring different drawings got the full raise. Sheets are rarely flat (5/8" plate can be ~1.5" out of flat within supplier tolerance), and longer rapids cross more of that bow. Add SafetyHeadraiseRapidDistance (program units, default 12"): any rapid to the next pierce longer than it takes the safety raise, regardless of drawing. Unknown travel never does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
264 lines
9.9 KiB
C#
264 lines
9.9 KiB
C#
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using OpenNest.CNC;
|
|
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 posted part
|
|
/// toolpath, with coordinates normalized to origin (0,0).
|
|
/// Called via M98 from sheet sub-programs.
|
|
/// </summary>
|
|
public sealed class CincinnatiPartSubprogramWriter
|
|
{
|
|
private readonly CincinnatiPostConfig _config;
|
|
private readonly CincinnatiFeatureWriter _featureWriter;
|
|
private readonly CoordinateFormatter _fmt;
|
|
private readonly IReadOnlyDictionary<Program, int> _holeSubprograms;
|
|
|
|
public CincinnatiPartSubprogramWriter(
|
|
CincinnatiPostConfig config,
|
|
IReadOnlyDictionary<Program, int> holeSubprograms = null
|
|
)
|
|
{
|
|
_config = config;
|
|
_featureWriter = new CincinnatiFeatureWriter(config);
|
|
_fmt = new CoordinateFormatter(config.PostedAccuracy);
|
|
_holeSubprograms = holeSubprograms;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a complete part sub-program for the given normalized program.
|
|
/// The program must be in absolute mode with coordinates normalized to origin (0,0).
|
|
/// </summary>
|
|
public void Write(
|
|
TextWriter w,
|
|
Program normalizedProgram,
|
|
string drawingName,
|
|
int subNumber,
|
|
string cutLibrary,
|
|
string etchLibrary,
|
|
double sheetDiagonal
|
|
)
|
|
{
|
|
var allFeatures = FeatureUtils.SplitByRapids(normalizedProgram.Codes);
|
|
if (allFeatures.Count == 0)
|
|
return;
|
|
|
|
// Classify and order: etch features first, then cut features
|
|
var ordered = FeatureUtils.ClassifyAndOrder(allFeatures);
|
|
var exterior = FeatureUtils.FindExterior(ordered);
|
|
|
|
w.WriteLine("(*****************************************************)");
|
|
w.WriteLine($":{subNumber}");
|
|
w.WriteLine(CoordinateFormatter.Comment($"PART: {drawingName}"));
|
|
|
|
for (var i = 0; i < ordered.Count; i++)
|
|
{
|
|
var (codes, isEtch) = ordered[i];
|
|
var isLastFeature = i == ordered.Count - 1;
|
|
|
|
// SubProgramCall features are emitted as M98 hole calls
|
|
if (codes.Count == 1 && codes[0] is SubProgramCall holeCall)
|
|
{
|
|
CincinnatiHoleCallWriter.Write(
|
|
w, holeCall, Vector.Zero, i, isLastFeature, _config, _fmt, _holeSubprograms
|
|
);
|
|
continue;
|
|
}
|
|
|
|
var featureNumber = i == 0 ? _config.FeatureLineNumberStart : 1000 + i + 1;
|
|
var cutDistance = FeatureUtils.ComputeCutDistance(codes);
|
|
|
|
var ctx = new FeatureContext
|
|
{
|
|
Codes = codes,
|
|
FeatureNumber = featureNumber,
|
|
PartName = drawingName,
|
|
IsFirstFeatureOfPart = false,
|
|
IsLastFeatureOnSheet = isLastFeature,
|
|
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);
|
|
}
|
|
|
|
w.WriteLine($"M99 (END OF {drawingName})");
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the program has no leading rapid, inserts a synthetic rapid at the
|
|
/// last motion endpoint (the contour return point). This ensures the feature
|
|
/// writer knows the true pierce location and preserves the first contour segment.
|
|
/// </summary>
|
|
internal static void EnsureLeadingRapid(Program pgm) => FeatureUtils.InsertLeadingRapid(pgm.Codes);
|
|
|
|
/// <summary>
|
|
/// 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<Part, PartSubprogram> mapping,
|
|
List<(int subNum, string name, Program program)> entries
|
|
) BuildRegistry(IEnumerable<Plate> plates, int startNumber)
|
|
{
|
|
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)
|
|
{
|
|
foreach (var part in plate.Parts)
|
|
{
|
|
if (part.BaseDrawing.IsCutOff)
|
|
continue;
|
|
|
|
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))
|
|
{
|
|
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
|
|
/// program reference, because hole call ids are only unique within one drawing's
|
|
/// program; identical hole geometry shares one sub-program. Entries are absolute-mode
|
|
/// copies, so the caller's (incremental) hole programs are never modified.
|
|
/// </summary>
|
|
internal static (
|
|
Dictionary<Program, int> mapping,
|
|
List<(int subNum, Program program)> entries
|
|
) BuildHoleRegistry(
|
|
IEnumerable<Plate> plates,
|
|
IEnumerable<Program> partSubprograms,
|
|
int startNumber
|
|
)
|
|
{
|
|
var mapping = new Dictionary<Program, int>(ReferenceEqualityComparer.Instance);
|
|
var entries = new List<(int, Program)>();
|
|
var contentIndex = new Dictionary<string, int>();
|
|
var nextSubNum = startNumber;
|
|
|
|
void Register(Program owner)
|
|
{
|
|
foreach (var code in owner.Codes)
|
|
{
|
|
if (code is not SubProgramCall { Program: not null } call)
|
|
continue;
|
|
if (mapping.ContainsKey(call.Program) || !FeatureUtils.HasCut(call.Program))
|
|
continue;
|
|
|
|
var absolute = PostedCopy(call.Program);
|
|
EnsureLeadingRapid(absolute);
|
|
|
|
var canonical = ProgramToCanonical(absolute);
|
|
if (!contentIndex.TryGetValue(canonical, out var subNum))
|
|
{
|
|
subNum = nextSubNum++;
|
|
contentIndex[canonical] = subNum;
|
|
entries.Add((subNum, absolute));
|
|
}
|
|
|
|
mapping[call.Program] = subNum;
|
|
}
|
|
}
|
|
|
|
foreach (var plate in plates)
|
|
{
|
|
foreach (var part in plate.Parts)
|
|
{
|
|
if (!part.BaseDrawing.IsCutOff)
|
|
Register(part.Program);
|
|
}
|
|
}
|
|
|
|
if (partSubprograms != null)
|
|
{
|
|
foreach (var program in partSubprograms)
|
|
Register(program);
|
|
}
|
|
|
|
return (mapping, entries);
|
|
}
|
|
|
|
private static string ProgramToCanonical(Program pgm)
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (var code in pgm.Codes)
|
|
{
|
|
if (code is LinearMove lm)
|
|
sb.Append(
|
|
CultureInfo.InvariantCulture,
|
|
$"L{lm.EndPoint.X:F6},{lm.EndPoint.Y:F6},{(int)lm.Layer}"
|
|
);
|
|
else if (code is ArcMove am)
|
|
sb.Append(
|
|
CultureInfo.InvariantCulture,
|
|
$"A{am.EndPoint.X:F6},{am.EndPoint.Y:F6},{am.CenterPoint.X:F6},{am.CenterPoint.Y:F6},{(int)am.Rotation},{(int)am.Layer}"
|
|
);
|
|
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();
|
|
}
|
|
}
|