refactor(cnc): extract rapid enumeration into RapidEnumerator
Pulls the rapid-walk logic (sub-program unwrapping, first-pierce lookup, incremental-vs-absolute handling, first-rapid skipping) out of PlateRenderer.DrawRapids into a reusable RapidEnumerator in Core so it can be unit-tested and reused outside the renderer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
using OpenNest.Geometry;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
public static class RapidEnumerator
|
||||
{
|
||||
public readonly record struct Segment(Vector From, Vector To);
|
||||
|
||||
public static List<Segment> Enumerate(Program pgm, Vector basePos, Vector startPos)
|
||||
{
|
||||
var results = new List<Segment>();
|
||||
|
||||
// Draw the rapid from the previous tool position to the program's first
|
||||
// pierce point. This also primes pos so the interior walk interprets
|
||||
// Incremental deltas from the correct absolute location (basePos), which
|
||||
// matters for raw pre-lead-in programs that are emitted Incremental.
|
||||
var firstPierce = FirstPiercePoint(pgm, basePos);
|
||||
results.Add(new Segment(startPos, firstPierce));
|
||||
|
||||
var pos = firstPierce;
|
||||
Walk(pgm, basePos, ref pos, skipFirst: true, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
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.EndPoint + basePos;
|
||||
}
|
||||
return basePos;
|
||||
}
|
||||
|
||||
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: true, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
|
||||
namespace OpenNest.Tests.CNC
|
||||
{
|
||||
public class RapidEnumeratorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Enumerate_AbsoluteProgram_OffsetsMotionsByBasePos()
|
||||
{
|
||||
var pgm = new Program(Mode.Absolute);
|
||||
pgm.Codes.Add(new RapidMove(1, 0));
|
||||
pgm.Codes.Add(new LinearMove(2, 0));
|
||||
pgm.Codes.Add(new RapidMove(3, 3));
|
||||
|
||||
var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0));
|
||||
|
||||
// Origin → first pierce, then interior rapid from contour end to next rapid target.
|
||||
Assert.Equal(2, segments.Count);
|
||||
Assert.Equal(new Vector(0, 0), segments[0].From);
|
||||
Assert.Equal(new Vector(101, 200), segments[0].To);
|
||||
Assert.Equal(new Vector(102, 200), segments[1].From);
|
||||
Assert.Equal(new Vector(103, 203), segments[1].To);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerate_IncrementalProgram_InterpretsDeltasFromBasePos()
|
||||
{
|
||||
// Pre-lead-in raw program: first rapid normalized to (0,0), Mode=Incremental
|
||||
// (matches ConvertGeometry.ToProgram output).
|
||||
var pgm = new Program(Mode.Incremental);
|
||||
pgm.Codes.Add(new RapidMove(0, 0));
|
||||
pgm.Codes.Add(new LinearMove(5, 0));
|
||||
pgm.Codes.Add(new LinearMove(0, 5));
|
||||
pgm.Codes.Add(new RapidMove(1, 1));
|
||||
|
||||
var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0));
|
||||
|
||||
Assert.Equal(2, segments.Count);
|
||||
// First rapid: plate origin → part pierce at basePos.
|
||||
Assert.Equal(new Vector(0, 0), segments[0].From);
|
||||
Assert.Equal(new Vector(100, 200), segments[0].To);
|
||||
// Interior rapid: after deltas (5,0) and (0,5) from basePos, rapid delta (1,1).
|
||||
Assert.Equal(new Vector(105, 205), segments[1].From);
|
||||
Assert.Equal(new Vector(106, 206), segments[1].To);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerate_SubProgramCall_RapidEndsAtAbsoluteHolePierce()
|
||||
{
|
||||
// Main program: lead-in rapid, a line, then a SubProgramCall for a hole.
|
||||
// Sub-program (incremental) starts with RapidMove(radius, 0) to the hole pierce.
|
||||
var sub = new Program(Mode.Incremental);
|
||||
sub.Codes.Add(new RapidMove(0.5, 0));
|
||||
sub.Codes.Add(new LinearMove(0, 0.1));
|
||||
|
||||
var pgm = new Program(Mode.Absolute);
|
||||
pgm.Codes.Add(new RapidMove(0.2, 0.3)); // first pierce (perimeter lead-in)
|
||||
pgm.Codes.Add(new LinearMove(1.0, 1.0)); // contour move
|
||||
pgm.Codes.Add(new SubProgramCall
|
||||
{
|
||||
Id = 1,
|
||||
Program = sub,
|
||||
Offset = new Vector(2, 2), // hole center (drawing-local)
|
||||
});
|
||||
|
||||
var basePos = new Vector(100, 200); // part.Location
|
||||
var segments = RapidEnumerator.Enumerate(pgm, basePos, startPos: new Vector(0, 0));
|
||||
|
||||
// Expected rapids:
|
||||
// 1. origin → first pierce (0.2+100, 0.3+200) = (100.2, 200.3)
|
||||
// 2. end of contour (1+100, 1+200) = (101, 201) → hole pierce (2+100+0.5, 2+200) = (102.5, 202)
|
||||
// The sub's internal first rapid is skipped (already drawn in #2).
|
||||
Assert.Equal(2, segments.Count);
|
||||
|
||||
Assert.Equal(new Vector(0, 0), segments[0].From);
|
||||
Assert.Equal(new Vector(100.2, 200.3), segments[0].To);
|
||||
|
||||
Assert.Equal(new Vector(101, 201), segments[1].From);
|
||||
Assert.Equal(new Vector(102.5, 202), segments[1].To);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,85 +385,20 @@ namespace OpenNest.Controls
|
||||
|
||||
private void DrawRapids(Graphics g)
|
||||
{
|
||||
var pen = view.ColorScheme.RapidPen;
|
||||
var pos = new Vector(0, 0);
|
||||
|
||||
for (var i = 0; i < view.Plate.Parts.Count; ++i)
|
||||
{
|
||||
var part = view.Plate.Parts[i];
|
||||
var pgm = part.Program;
|
||||
var segments = RapidEnumerator.Enumerate(part.Program, part.Location, pos);
|
||||
|
||||
var piercePoint = GetFirstPiercePoint(pgm, part.Location);
|
||||
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
|
||||
|
||||
pos = piercePoint;
|
||||
DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector GetFirstPiercePoint(Program pgm, Vector partLocation)
|
||||
{
|
||||
for (var i = 0; i < pgm.Length; i++)
|
||||
{
|
||||
if (pgm[i] is SubProgramCall call && call.Program != null)
|
||||
return GetFirstPiercePoint(call.Program, partLocation + call.Offset);
|
||||
|
||||
if (pgm[i] is Motion motion)
|
||||
foreach (var seg in segments)
|
||||
{
|
||||
return motion.EndPoint + partLocation;
|
||||
DrawLine(g, seg.From, seg.To, pen);
|
||||
pos = seg.To;
|
||||
}
|
||||
}
|
||||
return partLocation;
|
||||
}
|
||||
|
||||
private void DrawRapids(Graphics g, Program pgm, Vector basePos, ref Vector pos, bool skipFirstRapid = false)
|
||||
{
|
||||
var firstRapidSkipped = false;
|
||||
|
||||
for (var i = 0; i < pgm.Length; ++i)
|
||||
{
|
||||
var code = pgm[i];
|
||||
|
||||
if (code is SubProgramCall { Program: { } program } call)
|
||||
{
|
||||
// A SubProgramCall is a coordinate-frame shift, not a physical
|
||||
// rapid to the hole center. The Cincinnati post emits it as a
|
||||
// G52 bracket, so the physical rapid is the sub-program's first
|
||||
// motion, which goes straight from here to the lead-in pierce.
|
||||
// Look ahead for that pierce point and draw the direct rapid,
|
||||
// then recurse with skipFirstRapid so the sub doesn't also draw
|
||||
// its first rapid on top. See docs/cincinnati-post-output.md.
|
||||
var holeBase = basePos + call.Offset;
|
||||
var firstPierce = GetFirstPiercePoint(program, holeBase);
|
||||
|
||||
if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
|
||||
DrawLine(g, pos, firstPierce, view.ColorScheme.RapidPen);
|
||||
|
||||
var subPos = holeBase;
|
||||
DrawRapids(g, program, holeBase, ref subPos, skipFirstRapid: true);
|
||||
pos = subPos;
|
||||
}
|
||||
else if (code is Motion motion)
|
||||
{
|
||||
var endpt = pgm.Mode == Mode.Incremental
|
||||
? motion.EndPoint + pos
|
||||
: motion.EndPoint;
|
||||
|
||||
if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
|
||||
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
|
||||
|
||||
pos = endpt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldDrawRapid(bool skipFirstRapid, ref bool firstRapidSkipped)
|
||||
{
|
||||
if (skipFirstRapid && !firstRapidSkipped)
|
||||
{
|
||||
firstRapidSkipped = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DrawAllPiercePoints(Graphics g)
|
||||
|
||||
Reference in New Issue
Block a user