Files
OpenNest/Posts/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs
T
ajandClaude Opus 5.5 c6b6678a2f feat(planning): raise the head over cut parts as the last resort
When no search finds a route that keeps every rapid off parts already
cut, Plan Cutting used to offer only an unverified safest route needing
explicit acceptance. Cincinnati's own software instead raises the head
for such a rapid. Now the safest-route fallback marks each planner-chosen
rapid that still crosses cut parts with a HEAD RAISE comment, and the
plan is Ready with every raise listed. The strict search never marks a
rapid, and the batch prefers fewer broken checks, then fewer raises,
then less travel, so a plate that can be cut head-down never gets one.

The reader flags marked rapids and the checker sets their crossings
apart, in replay and pre-post verification alike. The Cincinnati CL
post writes M47 before a marked rapid whatever the M47 mode (the
distance-based safety raise is unchanged); the CI Fiber post writes a
new Head raise block and refuses marked nests while it is blank, since
its raise code is unconfirmed. Rapids into locked programs, cut-offs
and hole sub-programs are never marked and keep their findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:37:33 -04:00

265 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);
}
// 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(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();
}
}