Files
OpenNest/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberContourExtractor.cs
T
ajandClaude Opus 5.5 a0d6c7212b fix(cutting): raise the head for rapids into hole sub-programs
Round holes are cut as sub-programs, which are never marked because they
may be shared, so the safest-route fallback could not raise the head for
the rapid into a hole: on interlocked layouts every such crossing stayed
a finding and the plan stayed unverified. A HEAD RAISE marker may now sit
directly before a sub-program call, raising the call's arrival rapid.

- Motion reader: a marked call raises its first rapid; each sub-program
  motion records the part's own call that runs it.
- RaisedRapids marks the call when the crossing rapid is its arrival.
- Cincinnati CL keeps the marker with its hole call and writes M47 before
  it; part sub-program dedupe now tells marked programs apart.
- CI Fiber raises before the hole's first G0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:54:59 -05:00

202 lines
8.1 KiB
C#

using System;
using System.Collections.Generic;
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Posts.CincinnatiCIFiber
{
/// <summary>
/// One contour (pierce + lead-in + cut path) reduced to sheet-absolute
/// moves, ready to emit as a TF5200 N-label block.
/// </summary>
public sealed class CIFiberContour
{
/// <summary>Pierce point (sheet-absolute) — rapid target.</summary>
public Vector Pierce { get; set; }
/// <summary>
/// Lead-in move to the contour start (sheet-absolute endpoint), or
/// null when the contour begins cutting immediately at the pierce.
/// Per TF5200 §13.2.4.1 the first motion block after G41/G42 selection
/// must be LINEAR, so callers must reject contours whose lead-in is an
/// arc unless the config tolerates it.
/// </summary>
public Motion LeadIn { get; set; }
/// <summary>
/// Additional lead-in layer moves after the first (e.g. the arc that
/// follows CleanHoleLeadIn's line). Emitted before the cut layer turns
/// on, like the sample's lead block.
/// </summary>
public List<Motion> LeadInExtra { get; } = new();
/// <summary>Cutting moves (sheet-absolute) executed after the cut layer turns on.</summary>
public List<Motion> Cuts { get; } = new();
/// <summary>True when the contour is an external perimeter (exterior).</summary>
public bool IsExterior { get; set; }
/// <summary>
/// Head-height lock changes in a cut-off's overcut: each is written before the cut at
/// <c>Before</c> (<see cref="Cuts"/>.Count: after the last cut), locking when
/// <c>Lock</c> is true and unlocking otherwise.
/// </summary>
public List<(int Before, bool Lock)> HeadLocks { get; } = new();
/// <summary>Whether the rapid to the pierce raises the head over parts already cut (<see cref="HeadRaise"/>).</summary>
public bool Raised { get; set; }
}
/// <summary>
/// Extracts cut contours for one part in sheet coordinates.
/// Circular holes arrive as <see cref="SubProgramCall"/>s in an incremental
/// hole-local program; they are flattened here to sheet-absolute moves the
/// same way <see cref="NestPolylineExtractor"/> walks them for HPGL output.
/// Moves with <see cref="Motion.Suppressed"/> are skipped.
/// </summary>
public static class CIFiberContourExtractor
{
public static List<CIFiberContour> Extract(Part part)
{
if (part == null)
throw new ArgumentNullException(nameof(part));
var contours = new List<CIFiberContour>();
// A display perimeter's tail is never cut; an unplanned one is refused.
var program = DisplayPerimeterTail.ForPosting(part.Program, DisplayPerimeterTail.PartName(part));
Flatten(program, part.Location, Vector.Zero, contours);
return contours;
}
/// <summary>
/// Walk one program whose coordinates are relative to (partLocation +
/// callOffset). Sub-program calls recurse with the extra offset.
/// A contour starts at each rapid move; moves before the first rapid
/// form a contour whose pierce is the last move's endpoint (closed-loop
/// continuation), matching FeatureUtils' synthetic-rapid convention.
/// <paramref name="raiseFirst"/> raises the head for the program's first
/// contour: a marker before the call that runs it.
/// </summary>
private static void Flatten(
Program program,
Vector partLocation,
Vector subOffset,
List<CIFiberContour> contours,
bool raiseFirst = false
)
{
if (program == null)
return;
var origin = partLocation + subOffset;
// Work on a clone when conversion is needed so caller programs are
// never mutated (the CI sample inlines absolute moves).
var codes = program.Codes;
if (program.Mode == Mode.Incremental)
{
var clone = (Program)program.Clone();
clone.Mode = Mode.Absolute;
codes = clone.Codes;
}
CIFiberContour current = null;
var raise = raiseFirst;
void Flush()
{
if (current != null && (current.LeadIn != null || current.Cuts.Count > 0))
contours.Add(current);
current = null;
}
foreach (var code in codes)
{
if (code is Motion suppressed && suppressed.Suppressed)
continue;
// A marker raises the head for the rapid straight after it.
if (HeadRaise.IsMarker(code))
{
raise = true;
continue;
}
if (HeadHeightLock.IsLock(code) || HeadHeightLock.IsUnlock(code))
{
current ??= new CIFiberContour { Pierce = origin };
current.HeadLocks.Add((current.Cuts.Count, HeadHeightLock.IsLock(code)));
continue;
}
switch (code)
{
case RapidMove rapid:
Flush();
current = new CIFiberContour { Pierce = rapid.EndPoint + origin, Raised = raise };
raise = false;
break;
case SubProgramCall call:
// A hole call is its own contour: flatten into a temp
// list, then merge as one contour anchored at the call
// offset.
Flush();
var sub = new List<CIFiberContour>();
Flatten(call.Program, partLocation, subOffset + call.Offset, sub, raise);
raise = false;
contours.AddRange(sub);
break;
case Motion motion:
if (current == null)
{
// Contour with no explicit rapid (program starts
// mid-contour): in absolute mode the implicit
// start position is the frame origin, which is the
// closure point of a closed contour = the pierce.
current = new CIFiberContour { Pierce = origin, Raised = raise };
raise = false;
}
var absolute = (Motion)motion.Clone();
absolute.Offset(origin);
var layer = LayerOf(motion);
if (layer == LayerType.Leadin)
{
// First lead-in move opens the lead block; further
// lead-in moves (CleanHole line+arc) extend it.
if (current.LeadIn == null)
current.LeadIn = absolute;
else
current.LeadInExtra.Add(absolute);
}
else
{
// Cut, Leadout and Display all cut: OpenNest tags
// contour moves Display by default (see
// ContourCuttingStrategy.ConvertShapeToMoves),
// only Scribe is reliably a mark. Scribe contours
// are dropped by the writer when SkipScribe is on.
current.Cuts.Add(absolute);
}
break;
}
}
Flush();
}
private static LayerType LayerOf(Motion motion)
{
return motion switch
{
LinearMove l => l.Layer,
ArcMove a => a.Layer,
_ => LayerType.Cut,
};
}
}
}