using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests.CommonLines;
///
/// Plan Cutting cuts each common line once (docs/common-line-cutting.md): a group is cut member by
/// member, each cutting its holes, then its outside compensated. A member that cuts a common line
/// starts its outside there, entered along the shared edge from the scrap, so the line leads
/// straight into its perimeter; its partner cuts only its remainder, which starts and ends in the
/// kerf the common line opened. The parts are PT42-like.
///
public class CommonLinePlanningTests
{
internal const double Kerf = 0.012;
internal const double Width = 6;
internal const double Height = 12.44;
internal const double Chamfer = 0.38;
internal static CuttingParameters Parameters(double leadIn = 0.25) => new()
{
ExternalLeadIn = new LineLeadIn { Length = leadIn, ApproachAngle = 90 },
InternalLeadIn = new LineLeadIn { Length = leadIn, ApproachAngle = 90 },
ExternalLeadOut = new NoLeadOut(),
InternalLeadOut = new NoLeadOut(),
PierceClearance = 0.05,
Kerf = Kerf,
};
/// A PT42-like drawing: Width x Height with chamfered corners, optionally a round hole.
internal static Drawing Pt42(bool hole = false)
{
var program = new Program();
program.Codes.Add(new RapidMove(Chamfer, 0));
program.Codes.Add(new LinearMove(Width - Chamfer, 0));
program.Codes.Add(new LinearMove(Width, Chamfer));
program.Codes.Add(new LinearMove(Width, Height - Chamfer));
program.Codes.Add(new LinearMove(Width - Chamfer, Height));
program.Codes.Add(new LinearMove(Chamfer, Height));
program.Codes.Add(new LinearMove(0, Height - Chamfer));
program.Codes.Add(new LinearMove(0, Chamfer));
program.Codes.Add(new LinearMove(Chamfer, 0));
if (hole)
{
program.Codes.Add(new RapidMove(Width / 2 + 0.5, Height / 2));
program.Codes.Add(new ArcMove(new Vector(Width / 2 + 0.5, Height / 2), new Vector(Width / 2, Height / 2),
RotationType.CCW));
}
return new Drawing("PT42", program);
}
/// A plate with PT42s in a row, one kerf apart and joined.
internal static Plate Row(int count, bool hole = false, double x = 2, double y = 2, Drawing drawing = null)
{
var plate = new Plate(48, 120) { PartSpacing = 0.25 };
drawing ??= Pt42(hole);
var parts = new List();
for (var i = 0; i < count; i++)
{
var part = new Part(drawing, new Vector(x + i * (Width + Kerf), y));
plate.Parts.Add(part);
parts.Add(part);
}
plate.JoinCommonLine(parts, Kerf);
return plate;
}
internal static CuttingPlanResult Plan(Plate plate, CuttingParameters parameters = null, bool keepOrder = false) =>
CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero, 20000, parameters ?? Parameters(),
preservePartOrder: keepOrder));
internal static IReadOnlyList Motions(FixedProgramPlacement proposal) =>
ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, null, default).Motions;
internal static IReadOnlyList Motions(Part part) =>
ExecutionMotionReader.Read(part.Program, part.Location, null, default).Motions;
internal static string Describe(CuttingPlanResult result) =>
$"{result.Status}: " + string.Join(" | ", result.Findings.Select(f => f.Message));
// The centerline between the parts at positions i and i + 1 of a row.
private static double Centerline(int i, double x = 2) => x + (i + 1) * Width + i * Kerf + Kerf / 2;
private static bool IsCut(ExecutionMotion m) => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display;
[Fact]
public void Pair_TheFirstPartCutsTheCommonLineAsTheStartOfItsPerimeter()
{
var plate = Row(2);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
Assert.True(result.IndependentlyReplayed);
Assert.Same(plate.Parts[0], result.ProposedOrder[0].SourcePart);
var all = result.ProposedOrder.SelectMany(Motions).ToArray();
Assert.DoesNotContain(all, m => m.Layer == LayerType.CommonLine);
// One pierce below the notch, a lead-in straight up the shared edge's axis, then the shared
// edge itself and on round the perimeter to close where it began: no lead-out.
var motions = Motions(result.ProposedOrder[0]).ToList();
Assert.Single(motions, m => m.Layer == LayerType.Leadin);
var lead = motions.Single(m => m.Layer == LayerType.Leadin);
Assert.Equal(2 + Width, lead.Start!.Value.X, 9);
Assert.Equal(2 + Width, lead.End.X, 9);
Assert.Equal(2 + Chamfer, lead.End.Y, 9);
var cuts = motions.Where(IsCut).ToList();
Assert.Equal(2 + Width, cuts[0].End.X, 9);
Assert.Equal(2 + Height - Chamfer, cuts[0].End.Y, 9);
Assert.Equal(0, cuts[^1].End.DistanceTo(lead.End), 9);
Assert.DoesNotContain(motions, m => m.Layer == LayerType.Leadout);
Assert.False(CommonLineCuts.IsSinglePass(result.ProposedOrder[0].CopyProgram()));
}
[Fact]
public void Row_CutsEachMembersHolesThenItsOutside_StartingOnItsCommonLine()
{
var plate = Row(3, hole: true);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
// The group is cut contiguously, owners before partners.
Assert.Equal(plate.Parts, result.ProposedOrder.Select(p => p.SourcePart));
Assert.DoesNotContain(result.ProposedOrder.SelectMany(Motions), m => m.Layer == LayerType.CommonLine);
for (var i = 0; i < 3; i++)
{
var motions = Motions(result.ProposedOrder[i]).ToList();
var hole = motions.FindLastIndex(m => IsCut(m) && !m.Curve.IsLine);
var outside = motions.FindIndex(m => IsCut(m) && m.Curve.IsLine);
Assert.True(hole >= 0 && outside > hole, $"Part {i + 1} does not cut its hole before its outside.");
var lead = motions[outside - 1];
Assert.Equal(LayerType.Leadin, lead.Layer);
if (i < 2)
{
// Entered from below the notch along its right edge, the common line it cuts.
var edge = 2 + (i + 1) * Width + i * Kerf;
Assert.Equal(edge, lead.Start!.Value.X, 9);
Assert.Equal(edge, motions[outside].End.X, 9);
Assert.Equal(2 + Height - Chamfer, motions[outside].End.Y, 9);
}
else
Assert.Equal(Centerline(1), lead.Start!.Value.X, 9); // The last only cuts a remainder.
}
// The middle part stops at the kerf its left neighbour opened, then cuts the stretch below
// from that kerf, ending where it began, beside the part it is cut before.
var middle = Motions(result.ProposedOrder[1]).ToList();
var entries = middle.Where((m, i) => m.Layer == LayerType.Leadin && i + 1 < middle.Count
&& IsCut(middle[i + 1]) && middle[i + 1].Curve.IsLine).ToList();
Assert.Equal(2, entries.Count);
var start = entries[0].End;
Assert.Equal(0, middle.Last(IsCut).End.DistanceTo(start), 9);
}
[Fact]
public void Remainders_StartInTheSharedKerf_AndEndOnTheirSpan()
{
var plate = Row(2);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
var motions = Motions(result.ProposedOrder[1]);
var lead = motions.Last(m => m.Layer == LayerType.Leadin);
// Pierced on the centerline (nothing left to pierce), entering the partner's own edge.
Assert.Equal(Centerline(0), lead.Start!.Value.X, 9);
Assert.Equal(2 + Width + Kerf, lead.End.X, 9);
// The remainder ends at the other end of its shared span, on the partner's edge.
Assert.Equal(2 + Width + Kerf, motions.Last(IsCut).End.X, 9);
Assert.True(CommonLineCuts.IsSinglePass(result.ProposedOrder[1].CopyProgram()));
}
[Fact]
public void Members_CutTheirPerimeterExactlyOnce_SharedSpansByTheFirstPart()
{
var plate = Row(2);
var order = CommonLineCutting.Order(plate.CommonLines[0], CommonLineGeometry.Check(plate.CommonLines[0]));
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
foreach (var proposal in result.ProposedOrder)
{
var part = proposal.SourcePart;
var marked = CommonLineCutting.MarkSharedSpans(part.BaseDrawing.Program, part.Location,
order.PathsCutBefore(part).ToArray(), Kerf);
var material = LeadMaterialSnapshot.Capture(marked, part.Location, default, sharedSpans: true);
Assert.True(material.IsComplete, material.Reason);
var planned = ExecutionMotionReader.Read(proposal.CopyProgram(), part.Location, null, default);
Assert.True(ContourProgramVerifier.Verify(planned, material, null, default));
if (ReferenceEquals(part, plate.Parts[1]))
{
// The partner cutting its whole perimeter would cut the common line a second time.
var whole = ExecutionMotionReader.Read(part.BaseDrawing.Program, part.Location, null, default);
Assert.False(ContourProgramVerifier.Verify(whole, material, null, default));
}
}
}
[Fact]
public void CommonLineStartBlocked_RefusesNamingTheLine()
{
var plate = Row(2, x: 2, y: 2);
// A strip just below the notch stops the straight lead-in up the shared edge.
var strip = new Program();
strip.Codes.Add(new RapidMove(0, 0));
strip.Codes.Add(new LinearMove(0, 0.2));
strip.Codes.Add(new LinearMove(4, 0.2));
strip.Codes.Add(new LinearMove(4, 0));
strip.Codes.Add(new LinearMove(0, 0));
plate.Parts.Add(new Part(new Drawing("strip", strip), new Vector(Centerline(0) - 2, 2 - 0.3)));
var result = Plan(plate, Parameters(leadIn: 0.5));
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
Assert.Contains(result.Findings, f => f.Message.Contains("No lead fits the start of the common line between parts 1 and 2"));
}
[Fact]
public void KeptOrderCuttingThePartnerFirst_IsAConstraintConflict()
{
var plate = Row(2);
plate.Parts.Reorder([plate.Parts[1], plate.Parts[0]]);
var result = Plan(plate, keepOrder: true);
Assert.Equal(CuttingPlanStatus.ConstraintConflict, result.Status);
Assert.Contains(result.Findings, f => f.Message.Contains("common-line partner that cuts their shared line"));
}
[Fact]
public void Replay_RefusesARemainderBeforeItsSharedCut()
{
var plate = Row(2);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
var swapped = CuttingPlanService.ReplayPrograms(result.Snapshot,
[result.ProposedOrder[1], result.ProposedOrder[0]], 0, default);
Assert.Equal(CuttingPlanStatus.InvalidInput, swapped.Status);
Assert.Contains(swapped.Findings, f => f.Message.Contains("common-line partner"));
}
[Fact]
public void Apply_KeepsUtilizationQuantityAndOverlapResults()
{
var nest = new Nest("N26-295");
var plate = Row(3, hole: true);
nest.Plates.Add(plate);
var drawing = plate.Parts[0].BaseDrawing;
nest.Drawings.Add(drawing);
var utilization = plate.Utilization();
var nested = drawing.Quantity.Nested;
var overlaps = plate.HasOverlappingParts(out _);
var pairs = PlateOverlapAnalyzer.Analyze(plate.Parts.ToList()).Pairs.Count;
var area = plate.Parts.Sum(p => p.BaseDrawing.Area);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
// The first part cuts a normal closed perimeter; the others cut remainders.
Assert.False(CommonLineCuts.IsSinglePass(plate.Parts[0].Program));
Assert.All(plate.Parts.Skip(1), p => Assert.True(CommonLineCuts.IsSinglePass(p.Program)));
Assert.Equal(utilization, plate.Utilization(), 12);
Assert.Equal(nested, drawing.Quantity.Nested);
Assert.Equal(overlaps, plate.HasOverlappingParts(out _));
Assert.Equal(pairs, PlateOverlapAnalyzer.Analyze(plate.Parts.ToList()).Pairs.Count);
Assert.Equal(area, plate.Parts.Sum(p => p.BaseDrawing.Area), 12);
Assert.True(CommonLineGeometry.Check(plate.CommonLines[0]).IsIntact);
}
[Fact]
public void Applied_PassesPrePostReview()
{
var nest = new Nest("N26-295");
var plate = Row(3, hole: true);
plate.CuttingParameters = Parameters();
nest.Plates.Add(plate);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
var findings = PostVerificationAnalyzer.Analyze(nest).Findings;
Assert.DoesNotContain(findings, f => f.Kind is PostVerificationKind.CommonLine or PostVerificationKind.Incomplete
or PostVerificationKind.RapidCrossing or PostVerificationKind.MissingLeadIn or PostVerificationKind.PartSpacing);
}
[Fact]
public void PrePost_ReportsACommonLineCutTwice()
{
var nest = new Nest("N26-295");
var plate = Row(2);
nest.Plates.Add(plate);
// Planned to cut twice, then switched back to once without replanning.
plate.CutCommonLinesTwice = true;
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
plate.CutCommonLinesTwice = false;
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings,
f => f.Kind == PostVerificationKind.CommonLine);
Assert.Contains("cut more than once", finding.Message);
}
[Fact]
public void CutTwice_CutsEveryMembersWholePerimeterAndNoSharedCut()
{
var plate = Row(2);
plate.CutCommonLinesTwice = true;
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
var all = result.ProposedOrder.SelectMany(Motions).ToArray();
Assert.DoesNotContain(all, m => m.Layer == LayerType.CommonLine);
foreach (var proposal in result.ProposedOrder)
{
var cuts = Motions(proposal).Where(IsCut).ToArray();
Assert.Equal(cuts[0].Curve.Start.X, cuts[^1].End.X, 9);
Assert.Equal(cuts[0].Curve.Start.Y, cuts[^1].End.Y, 9);
// No lead-in enters the shared edge, which would cut the partner one kerf away.
var lead = Motions(proposal).First(m => m.Layer == LayerType.Leadin);
var sharedEdge = ReferenceEquals(proposal.SourcePart, plate.Parts[0]) ? 2 + Width : 2 + Width + Kerf;
Assert.False(System.Math.Abs(lead.End.X - sharedEdge) < 1e-9
&& lead.End.Y > 2 + Chamfer && lead.End.Y < 2 + Height - Chamfer, "A lead-in enters the shared edge.");
}
}
[Fact]
public void LockedMember_Refuses()
{
var plate = Row(2);
plate.Parts[1].LeadInsLocked = true;
var result = Plan(plate);
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
Assert.Contains(result.Findings, f => f.Message.Contains("Part 2 shares a common line"));
}
[Fact]
public void BrokenGroup_Refuses()
{
var plate = Row(2);
plate.Parts[1].Offset(0.5, 0);
var result = Plan(plate);
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
Assert.Contains(result.Findings, f => f.Message.Contains("no longer shares"));
}
[Fact]
public void LeadInLongerThanASharedSpan_Refuses()
{
var result = Plan(Row(2), Parameters(leadIn: 12));
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
Assert.Contains(result.Findings, f => f.Message.Contains("shorter than the 12 external lead-in"));
}
[Fact]
public void SupportTabs_AreNeverAppliedToMembers()
{
var plate = Row(2);
var single = new Part(plate.Parts[0].BaseDrawing, new Vector(30, 2));
plate.Parts.Add(single);
var parameters = Parameters();
parameters.SupportTabs = true;
parameters.TabBySize = true;
parameters.TabSizeMinWidth = 0;
parameters.TabSizeMaxWidth = 100;
parameters.TabConfig = new NormalTab { Size = 0.1 };
var result = Plan(plate, parameters);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
var perimeter = 2 * (Width + Height) - 8 * Chamfer + 4 * Chamfer * System.Math.Sqrt(2);
var alone = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, single));
Assert.True(Motions(alone).Where(IsCut).Sum(m => m.Length) < perimeter - 0.05, "The lone part is not tabbed.");
// The first member cuts its whole perimeter, common line included; its partner the rest.
var first = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, plate.Parts[0]));
var partner = result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, plate.Parts[1]));
Assert.Equal(perimeter, Motions(first).Where(IsCut).Sum(m => m.Length), 6);
Assert.Equal(perimeter - (Height - 2 * Chamfer), Motions(partner).Where(IsCut).Sum(m => m.Length), 6);
}
[Fact]
public void SwitchingToCutTwiceAfterPlanning_MakesThePlanStale()
{
var plate = Row(2);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
plate.CutCommonLinesTwice = true;
Assert.Equal(CuttingCommitStatus.Stale, CuttingPlanService.Apply([result]).Status);
}
[Fact]
public void Applied_RoundTripsThroughTheNestFile()
{
var nest = new Nest("N26-295");
var plate = Row(2);
nest.Plates.Add(plate);
nest.Drawings.Add(plate.Parts[0].BaseDrawing);
var result = Plan(plate);
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
var twice = Row(2, drawing: plate.Parts[0].BaseDrawing);
twice.CutCommonLinesTwice = true;
nest.Plates.Add(twice);
var path = Path.Combine(Path.GetTempPath(), $"common-line-{Guid.NewGuid():N}.nest");
try
{
new NestWriter(nest).Write(path);
var read = new NestReader(path).Read();
Assert.False(read.Plates[0].CutCommonLinesTwice);
Assert.True(read.Plates[1].CutCommonLinesTwice);
var parts = read.Plates[0].Parts;
Assert.Equal(Motions(plate.Parts[0]).Select(m => (m.Layer, m.End)), Motions(parts[0]).Select(m => (m.Layer, m.End)));
Assert.False(CommonLineCuts.IsSinglePass(parts[0].Program));
Assert.True(CommonLineCuts.IsSinglePass(parts[1].Program));
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(read).Findings, f => f.PlateNumber == 1
&& f.Kind is PostVerificationKind.CommonLine or PostVerificationKind.RapidCrossing);
}
finally
{
File.Delete(path);
}
}
}