Files
OpenNest/OpenNest.Tests/CommonLines/CommonLinePostTests.cs
T
ajandClaude Opus 5.5 74937b7952 feat(cutting): start the owner's outline on its common line
The part cut first no longer makes a separate uncompensated centerline
cut with its own lead-in and lead-out, then re-pierces for its
remainder. It cuts its outside compensated, which puts its shared edge
on the kerf centerline, starting on the common line: a straight lead-in
from the scrap along the shared edge, then the line and on round its
perimeter with no lead-out. A member with spans already cut stops in
that kerf and cuts its last stretch from it, ending beside the part it
is cut before. Partners still cut only their remainders.

Replay's tabbed-ring accounting now accepts pieces that tile each span
exactly once, and the pre-post review counts a span as cut by a
member's compensated edge (or an older centerline cut).

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

163 lines
6.2 KiB
C#

using System.Text;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Posts.Cincinnati;
using OpenNest.Posts.CincinnatiCIFiber;
using OpenNest.Posts.GravographIS;
namespace OpenNest.Tests.CommonLines;
/// <summary>
/// Posting common lines cut once (docs/common-line-cutting.md): the part cut first is a normal
/// compensated perimeter that starts on its common line; the CI Fiber post writes each remainder
/// in the contour form, compensated on its perimeter's side; posts that cannot write remainders
/// refuse the nest.
/// </summary>
public class CommonLinePostTests
{
private static Nest PlannedPair()
{
var nest = new Nest("N26-295") { Thickness = 0.1875, Material = new Material("Mild Steel") };
var plate = CommonLinePlanningTests.Row(2);
nest.Plates.Add(plate);
var result = CommonLinePlanningTests.Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, CommonLinePlanningTests.Describe(result));
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
return nest;
}
private static string Body(string program)
{
var lines = program.Split("\r\n");
var start = Array.IndexOf(lines, "N0:") + 1;
var end = Array.LastIndexOf(lines, "/L \"L0\"");
return string.Join("\n", lines[start..end]);
}
[Fact]
public void CIFiber_WritesTheCommonLineAsThePerimetersStart_AndTheRemainderOnItsPerimetersSide()
{
var nest = PlannedPair();
var post = new CIFiberPostProcessor(new CIFiberPostConfig { ConfigurationName = "CI FIBER 4020" });
using var stream = new MemoryStream();
post.Post(nest, stream);
// Part 1 enters below the notch straight up its shared edge (x 8, compensated G42 onto the
// centerline), cuts the common line and carries on round its perimeter, closing where it
// began. Part 2's remainder is pierced in that kerf and compensated G42 as its
// counterclockwise perimeter would be.
const string golden = """
( Sheet number - 1 )
( Part #1 )
( PART:PT42 )
V.E.R4=1
N1:
/L "L0"
V.E.R3=1
G0X8Y2.13
/L "L4"
G42
G1X8Y2.38
/L "L6"
G1X8Y14.06
G1X7.62Y14.44
G1X2.38Y14.44
G1X2Y14.06
G1X2Y2.38
G1X2.38Y2
G1X7.62Y2
G1X8Y2.38
/L "ZHSOFF"
( PART END )
( Part #2 )
( PART:PT42 )
V.E.R4=2
N2:
/L "L0"
V.E.R3=2
G0X8.006Y2.63
/L "L4"
G42
G1X8.012Y2.38
/L "L6"
G1X8.392Y2
G1X13.632Y2
G1X14.012Y2.38
G1X14.012Y14.06
G1X13.632Y14.44
G1X8.392Y14.44
G1X8.012Y14.06
/L "ZHSOFF"
( PART END )
""";
Assert.Equal(golden.ReplaceLineEndings("\n"), Body(Encoding.UTF8.GetString(stream.ToArray())));
}
[Fact]
public void CIFiber_CompensatesAClockwisePerimetersRemaindersG41()
{
// Imported drawings are wound clockwise: the same pair drawn that way.
var drawing = CommonLinePlanningTests.Pt42();
var reversed = new Program();
var points = drawing.Program.Codes.OfType<Motion>().Select(m => m.EndPoint).Reverse().ToArray();
reversed.Codes.Add(new RapidMove(points[0]));
foreach (var point in points.Skip(1))
reversed.Codes.Add(new LinearMove(point));
var nest = new Nest("N26-295") { Thickness = 0.1875, Material = new Material("Mild Steel") };
var plate = CommonLinePlanningTests.Row(2, drawing: new Drawing("PT42", reversed));
nest.Plates.Add(plate);
var result = CommonLinePlanningTests.Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, CommonLinePlanningTests.Describe(result));
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
using var stream = new MemoryStream();
new CIFiberPostProcessor(new CIFiberPostConfig()).Post(nest, stream);
var lines = Body(Encoding.UTF8.GetString(stream.ToArray())).Split("\n");
Assert.Equal(2, lines.Count(line => line == "G41"));
Assert.DoesNotContain("G42", lines);
}
[Fact]
public void PostsThatCannotWriteSinglePassCommonLines_RefuseTheNest()
{
var nest = PlannedPair();
using var stream = new MemoryStream();
var cincinnati = Assert.Throws<InvalidOperationException>(() => new CincinnatiPostProcessor().Post(nest, stream));
var gravograph = Assert.Throws<InvalidOperationException>(() => new GravographISPostProcessor().Post(nest, stream));
Assert.Contains("cannot write single-pass common lines", cincinnati.Message);
Assert.Contains("Cut Common Lines Twice", gravograph.Message);
Assert.Equal(0, stream.Length);
}
[Fact]
public void Host_RefusesAPostWithoutSupport_AndPassesOneWithIt()
{
var nest = PlannedPair();
var refused = Assert.Throws<InvalidOperationException>(() =>
CommonLineCuts.EnsurePostable(nest, new NoCommonLinePost()));
Assert.Contains("Plate 1, part 2 cuts a common line once", refused.Message);
CommonLineCuts.EnsurePostable(nest, new CIFiberPostProcessor());
// Cut twice, nothing is single-pass, so any post may write it.
nest.Plates[0].CutCommonLinesTwice = true;
var again = CommonLinePlanningTests.Plan(nest.Plates[0]);
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([again]).Status);
CommonLineCuts.EnsurePostable(nest, new NoCommonLinePost());
}
private sealed class NoCommonLinePost : IPostProcessor
{
public string Name => "Plain";
public string Author => "Tests";
public string Description => "A post that knows nothing of common lines.";
public void Post(Nest nest, Stream outputStream) => throw new NotSupportedException();
public void Post(Nest nest, string outputFile) => throw new NotSupportedException();
}
}