Files
OpenNest/OpenNest.Core/CNC/RapidEnumerator.cs
T
aj 8f568482bc fix(planning): keep a blank's outline on its applied program
An applied cutting program for a display-perimeter blank held only its
cutouts, so the part shrank to them: a 30 x 20 door reported an 11.5 x 20
extent, overlap and the plate view lost the blank, and running automatic
cut-offs afterwards added lines straight through it.

A complete emission now ends with a display tail: a marker comment, a
rapid to the outline and the outline on the display perimeter layer. The
part keeps the blank's bounds and material, so later cut-offs still have
nothing to cut and the door plans again as before. Nothing executes the
tail: executed reads stop at the marker, so search, replay and the
pre-post review see only the cuts, and cut time, the rapid display and the
cut simulation leave it out. DisplayPerimeterTail.ForPosting gives posts
the program without it and refuses an unplanned display perimeter.
2026-10-22 20:06:15 -04:00

124 lines
4.3 KiB
C#

using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC
{
public static class RapidEnumerator
{
public readonly record struct Segment(Vector From, Vector To);
/// <summary>
/// Enumerates plate rapids in cutting order, advancing through all cutting
/// motions before connecting to the next part (including scrap cutoffs).
/// </summary>
public static List<Segment> Enumerate(IEnumerable<Part> parts)
{
var results = new List<Segment>();
var pos = Vector.Zero;
foreach (var part in parts)
pos = AppendProgram(part.Program, part.Location, pos, results);
return results;
}
public static List<Segment> Enumerate(Program pgm, Vector basePos, Vector startPos)
{
var results = new List<Segment>();
AppendProgram(pgm, basePos, startPos, results);
return results;
}
private static Vector AppendProgram(Program pgm, Vector basePos, Vector startPos, List<Segment> results)
{
// The head never travels to a display perimeter's tail.
pgm = DisplayPerimeterTail.WithoutTail(pgm);
// Draw the approach to the first rapid destination, or to the program
// origin when it starts cutting without a rapid. Only an initial rapid
// (or call arrival) is already represented by this approach; a later
// first rapid must remain visible. Walk starts at the program origin,
// so an incremental initial rapid is not applied twice.
var firstPierce = FirstPiercePoint(pgm, basePos);
results.Add(new Segment(startPos, firstPierce));
var pos = basePos;
Walk(pgm, basePos, ref pos, skipFirst: StartsWithRapidOrCall(pgm), results);
// The last rapid ends at a pierce, not necessarily the final tool position.
return pos;
}
private static Vector FirstPiercePoint(Program pgm, Vector basePos)
{
for (var i = 0; i < pgm.Length; i++)
{
if (pgm[i] is SubProgramCall call && call.Program != null)
return FirstPiercePoint(call.Program, basePos + call.Offset);
if (pgm[i] is Motion motion)
return motion is RapidMove ? motion.EndPoint + basePos : basePos;
}
return basePos;
}
private static bool StartsWithRapidOrCall(Program pgm)
{
for (var i = 0; i < pgm.Length; i++)
{
if (pgm[i] is SubProgramCall { Program: not null })
return true;
if (pgm[i] is Motion motion)
return motion is RapidMove;
}
return false;
}
private static void Walk(
Program pgm,
Vector basePos,
ref Vector pos,
bool skipFirst,
List<Segment> results
)
{
var skipped = !skipFirst;
for (var i = 0; i < pgm.Length; ++i)
{
var code = pgm[i];
if (code is SubProgramCall { Program: { } program } call)
{
var holeBase = basePos + call.Offset;
var firstPierce = FirstPiercePoint(program, holeBase);
if (!skipped)
skipped = true;
else
results.Add(new Segment(pos, firstPierce));
var subPos = holeBase;
Walk(program, holeBase, ref subPos, skipFirst: StartsWithRapidOrCall(program), results);
pos = subPos;
}
else if (code is Motion motion)
{
var endpt =
pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint + basePos;
if (code.Type == CodeType.RapidMove)
{
if (!skipped)
skipped = true;
else
results.Add(new Segment(pos, endpt));
}
pos = endpt;
}
}
}
}
}