using OpenNest.CNC;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
namespace OpenNest.Tests.CommonLines;
///
/// Common-line groups: span derivation, joining, the spacing exemption and the pre-post review.
/// The part is shaped like N26-295's PT42: 6 x 12.44 with 0.38 x 45° chamfered corners.
///
public class CommonLineTests
{
private const double Kerf = 0.012;
private const double Width = 6;
private const double Height = 12.44;
private const double Chamfer = 0.38;
[Fact]
public void Spans_LongEdgesOneKerfApart_ShareTheStraightPart()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var span = Assert.Single(CommonLineGeometry.Spans(a, b, Kerf));
Assert.Equal(Height - 2 * Chamfer, span.Length, 6);
Assert.Equal(Width + Kerf / 2, span.Start.X, 6);
Assert.Equal(Width + Kerf / 2, span.End.X, 6);
}
[Theory]
[InlineData(System.Math.PI / 2)]
[InlineData(System.Math.PI)]
[InlineData(3 * System.Math.PI / 2)]
public void Spans_RotatedParts_StillShare(double rotation)
{
var a = Chamfered(0, 0, rotation);
var b = Chamfered(0, 0, rotation);
var box = a.BoundingBox;
b.Offset(box.Length + Kerf, 0);
var span = Assert.Single(CommonLineGeometry.Spans(a, b, Kerf));
Assert.True(span.Length > Chamfer);
}
[Theory]
[InlineData(Kerf + 0.002)]
[InlineData(Kerf - 0.002)]
[InlineData(0.25)]
public void Spans_GapOffTheKerf_ShareNothing(double gap)
{
Assert.Empty(CommonLineGeometry.Spans(Chamfered(0, 0), Chamfered(Width + gap, 0), Kerf));
}
[Fact]
public void Spans_EdgesThatDoNotFace_ShareNothing()
{
// Side by side but offset past the straight part: only chamfers would meet.
Assert.Empty(CommonLineGeometry.Spans(Chamfered(0, 0), Chamfered(Width + Kerf, Height - 0.1), Kerf));
}
[Fact]
public void Join_SlidesPartsToOneKerf_AndGroupsThem()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + 0.25, 0.1);
var plate = Plate(a, b);
var result = CommonLineJoiner.Join(plate, new[] { b, a }, Kerf, 1);
Assert.True(result.Succeeded, result.Failure);
Assert.Same(result.Group, Assert.Single(plate.CommonLines));
Assert.Equal(Width + Kerf, b.Location.X, 9);
Assert.Equal(0.1, b.Location.Y, 9);
Assert.Equal(Vector.Zero, a.Location);
Assert.True(CommonLineGeometry.Check(result.Group).IsIntact);
}
[Fact]
public void Join_RowOfFour_FormsOneChain()
{
var parts = Enumerable.Range(0, 4).Select(i => Chamfered(i * (Width + 0.25), 0)).ToArray();
var plate = Plate(parts);
var result = CommonLineJoiner.Join(plate, parts, Kerf, 1);
Assert.True(result.Succeeded, result.Failure);
for (var i = 0; i < 4; i++)
Assert.Equal(i * (Width + Kerf), parts[i].Location.X, 9);
Assert.Equal(3, CommonLineGeometry.Check(result.Group).Spans.Count);
}
[Fact]
public void Join_NoFacingEdge_MovesNothing()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + 5, 0);
var plate = Plate(a, b);
var result = CommonLineJoiner.Join(plate, new[] { a, b }, Kerf, 1);
Assert.False(result.Succeeded);
Assert.Equal(Width + 5, b.Location.X, 9);
Assert.Empty(plate.CommonLines);
}
[Fact]
public void Join_ExistingGroupMovesAsOneAndMerges()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var c = Chamfered(2 * Width + Kerf + 0.3, 0);
var plate = Plate(a, b, c);
plate.JoinCommonLine(new[] { a, b }, Kerf);
// Joining c to b anchors the pair nearest the origin and slides c.
var result = CommonLineJoiner.Join(plate, new[] { c, b }, Kerf, 1);
Assert.True(result.Succeeded, result.Failure);
var group = Assert.Single(plate.CommonLines);
Assert.Equal(3, group.Members.Count);
Assert.Equal(2 * (Width + Kerf), c.Location.X, 9);
}
[Fact]
public void Join_ZeroKerf_IsRefused()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + 0.25, 0);
var result = CommonLineJoiner.Join(Plate(a, b), new[] { a, b }, 0, 1);
Assert.False(result.Succeeded);
Assert.Contains("kerf", result.Failure);
}
[Fact]
public void RemovingAPart_DropsItFromItsGroup_AndAPairDisappears()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var c = Chamfered(2 * (Width + Kerf), 0);
var plate = Plate(a, b, c);
var group = plate.JoinCommonLine(new[] { a, b, c }, Kerf);
plate.Parts.Remove(c);
Assert.Equal(new[] { a, b }, group.Members);
plate.Parts.Remove(b);
Assert.Empty(plate.CommonLines);
}
[Fact]
public void SpacingReview_GroupedPairAtKerf_IsClear_UngroupedIsNot()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var nest = NestWith(a, b);
var ungrouped = PostVerificationAnalyzer.Analyze(nest);
Assert.Single(ungrouped.Findings, f => f.Kind == PostVerificationKind.PartSpacing);
nest.Plates[0].JoinCommonLine(new[] { a, b }, Kerf);
// Unplanned programs cut every perimeter whole: these plates cut their common lines twice.
nest.Plates[0].CutCommonLinesTwice = true;
var grouped = PostVerificationAnalyzer.Analyze(nest);
Assert.DoesNotContain(grouped.Findings, f => f.Kind is PostVerificationKind.PartSpacing
or PostVerificationKind.CommonLine or PostVerificationKind.Overlap);
}
[Fact]
public void SpacingReview_GroupedPairCloserThanKerf_Warns()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var nest = NestWith(a, b);
nest.Plates[0].JoinCommonLine(new[] { a, b }, Kerf);
// Unplanned programs cut every perimeter whole: these plates cut their common lines twice.
nest.Plates[0].CutCommonLinesTwice = true;
b.Offset(-Kerf / 2, 0);
var findings = PostVerificationAnalyzer.Analyze(nest).Findings;
var spacing = Assert.Single(findings, f => f.Kind == PostVerificationKind.PartSpacing);
Assert.Contains("kerf", spacing.Message);
Assert.Contains(findings, f => f.Kind == PostVerificationKind.CommonLine);
}
[Fact]
public void DesktopSpacingCheck_HonorsTheGroup()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var plate = NestWith(a, b).Plates[0];
Assert.Single(PlateOverlapAnalyzer.Analyze(PlateOverlapAnalyzer.CapturePlate(plate)).SpacingPairs);
plate.JoinCommonLine(new[] { a, b }, Kerf);
Assert.Empty(PlateOverlapAnalyzer.Analyze(PlateOverlapAnalyzer.CapturePlate(plate)).SpacingPairs);
}
[Fact]
public void Review_MovedMember_BreaksTheGroup()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var nest = NestWith(a, b);
nest.Plates[0].JoinCommonLine(new[] { a, b }, Kerf);
// Unplanned programs cut every perimeter whole: these plates cut their common lines twice.
nest.Plates[0].CutCommonLinesTwice = true;
b.Offset(1, 0);
var findings = PostVerificationAnalyzer.Analyze(nest).Findings
.Where(f => f.Kind == PostVerificationKind.CommonLine).ToArray();
Assert.Equal(2, findings.Length);
Assert.All(findings, f => Assert.Contains("no longer shares", f.Message));
Assert.False(PostVerificationAnalyzer.Analyze(nest).CanPost(false));
}
[Fact]
public void Review_PlateKerfChanged_Warns()
{
var a = Chamfered(0, 0);
var b = Chamfered(Width + Kerf, 0);
var nest = NestWith(a, b);
nest.Plates[0].JoinCommonLine(new[] { a, b }, Kerf);
// Unplanned programs cut every perimeter whole: these plates cut their common lines twice.
nest.Plates[0].CutCommonLinesTwice = true;
nest.Plates[0].CuttingParameters = new CuttingParameters { Kerf = 0.02 };
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings,
f => f.Kind == PostVerificationKind.CommonLine);
Assert.Contains("0.02 kerf", finding.Message);
}
[Fact]
public void Review_LeadOnTheSharedEdge_CutsThePartner()
{
// A's lead-in starts beyond its right edge, inside B one kerf away.
var program = new Program();
program.Codes.Add(new RapidMove(Width + 0.1, 6));
program.Codes.Add(new LinearMove(Width, 6) { Layer = LayerType.Leadin });
program.Codes.Add(new LinearMove(Width, Height));
program.Codes.Add(new LinearMove(0, Height));
program.Codes.Add(new LinearMove(0, 0));
program.Codes.Add(new LinearMove(Width, 0));
program.Codes.Add(new LinearMove(Width, 6));
var a = new Part(new Drawing("lead", program), Vector.Zero);
var b = new Part(new Drawing("plain", Rectangle()), new Vector(Width + Kerf, 0));
var nest = NestWith(a, b);
nest.Plates[0].JoinCommonLine(new[] { a, b }, Kerf);
// Unplanned programs cut every perimeter whole: these plates cut their common lines twice.
nest.Plates[0].CutCommonLinesTwice = true;
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings,
f => f.Kind == PostVerificationKind.CommonLine);
Assert.Equal((1, 2), (finding.PartNumber, finding.OtherPartNumber));
Assert.Contains("runs into", finding.Message);
}
private static Program Rectangle()
{
var program = new Program();
program.Codes.Add(new RapidMove(0, 0));
program.Codes.Add(new LinearMove(Width, 0));
program.Codes.Add(new LinearMove(Width, Height));
program.Codes.Add(new LinearMove(0, Height));
program.Codes.Add(new LinearMove(0, 0));
return program;
}
/// A PT42-like part: Width x Height, every corner chamfered, clockwise from the origin side.
private static Part Chamfered(double x, double y, double rotation = 0)
{
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));
var part = new Part(new Drawing("PT42", program));
if (rotation != 0)
part.Rotate(rotation);
part.Offset(x - part.Location.X, y - part.Location.Y);
return part;
}
private static Plate Plate(params Part[] parts)
{
var plate = new Plate(48, 120) { PartSpacing = 0.25 };
foreach (var part in parts)
plate.Parts.Add(part);
return plate;
}
private static Nest NestWith(params Part[] parts)
{
var nest = new Nest();
var plate = nest.CreatePlate();
plate.PartSpacing = 0.25;
foreach (var part in parts)
plate.Parts.Add(part);
return nest;
}
}