mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 08:01:35 -05:00
The Cincinnati CL post always drew an "incomplete final rapid sequence" warning because it never declared IPostVerificationSupport. It writes etch features before cuts, inserts or drops rapids, and with part sub-programs rapids to each part's corner, so it cannot declare it unconditionally. IPostVerificationSupport gains a nest-aware PlacedProgramOrderChange(nest) (null = preserved; otherwise the reason, shown in the warning). The CL post answers it from its own writer: part sub-programs and smart rapids off, and every part's and hole's posted motions exactly equal to its placed ones. Plan Cutting cuts scribes first, so planned nests qualify. CNC output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
275 lines
10 KiB
C#
275 lines
10 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)),
|
|
NextRapidRaised = !isLastFeature && FeatureUtils.IsRaised(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(FeatureUtils.PostedProgram(part));
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The absolute copy a hole sub-program is written from: suppressed moves left uncut and
|
|
/// a leading rapid ensured. <see cref="Write"/> then puts its etch features first.
|
|
/// </summary>
|
|
internal static Program PostedHole(Program hole)
|
|
{
|
|
var absolute = PostedCopy(hole);
|
|
EnsureLeadingRapid(absolute);
|
|
return absolute;
|
|
}
|
|
|
|
// 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 = PostedHole(call.Program);
|
|
|
|
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(FeatureUtils.PostedProgram(part));
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|