Files
OpenNest/Posts/OpenNest.Posts.CincinnatiCIFiber/CIFiberContourExtractor.cs
T
ajandClaude Opus 5.5 f9b2713420 feat(common-line): cut each shared line once through Plan Cutting and the CI Fiber post
N26-295's PT42 pairs saved spacing in slice 1 but every shared edge was
still cut twice, costing a pass per edge and risking a freed part
tipping under the head on the second pass.

Plan Cutting now cuts a plate's common lines once unless the plate is set
to cut them twice. Each group is cut member by member, owner before
partner (a captured dependency that search and replay enforce, so a kept
order cutting the partner first is a constraint conflict): the member's
holes, then the shared cuts it owns as open CommonLine centerline cuts
entered through the notch on the span's axis, then its remainder pieces,
each ramped in from the kerf the shared cut opened. Capture marks the
shared spans as tabs on the owned clean copy, so replay's contour
accounting proves each perimeter is cut exactly once; a lead that fits
at neither end refuses as UnsupportedGeometry naming the span, never
shortened. Material still comes from the drawing, so utilization,
quantity and overlap results do not change. Members are never
support-tabbed; locked or excluded members and broken groups refuse.

The CI Fiber post writes shared cuts in the cut-off form with a lead-in
and no G41/G42, and remainders in the contour form on their perimeter's
compensation side, named by a remainder comment. Cincinnati CL (whose
PreApplied mode offsets nothing) and Gravograph refuse such nests, as do
the desktop and console for any post without ICommonLinePostProcessor;
nothing falls back to a silent double cut. Plate > Arrange > Cut Common
Lines Twice keeps slice 1's behavior, persists with the plate and makes
an open plan stale.

Pre-post review now checks each span is cut once, on its centerline, by
the member cut first, and the rapid check treats a finished member's
remainders as an obstacle. Compensation start in void and cancel in
kerf are unconfirmed on the CI FIBER 4020; release waits on a 3/16
stainless coupon trial.

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

224 lines
9.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>
/// True for a common line's shared cut (<see cref="LayerType.CommonLine"/>): an open
/// centerline cut made without kerf compensation.
/// </summary>
public bool IsCommonLine { get; set; }
/// <summary>
/// For a common-line remainder, the winding of the perimeter it belongs to
/// (<see cref="CommonLineCuts"/>): it is cut as the perimeter, compensated on that side.
/// Null for every other contour.
/// </summary>
public RotationType? RemainderWinding { 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 (CommonLineCuts.RemainderWinding(code) is { } winding)
{
current ??= new CIFiberContour { Pierce = origin };
current.RemainderWinding = winding;
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);
if (layer == LayerType.CommonLine)
current.IsCommonLine = true;
}
break;
}
}
Flush();
}
private static LayerType LayerOf(Motion motion)
{
return motion switch
{
LinearMove l => l.Layer,
ArcMove a => a.Layer,
_ => LayerType.Cut,
};
}
}
}