mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 09:05:44 -05:00
N26-295's PT42 pairs saved spacing in slice 1 but every shared edge was still cut twice, costing a pass per edge and risking a freed part tipping under the head on the second pass. Plan Cutting now cuts a plate's common lines once unless the plate is set to cut them twice. Each group is cut member by member, owner before partner (a captured dependency that search and replay enforce, so a kept order cutting the partner first is a constraint conflict): the member's holes, then the shared cuts it owns as open CommonLine centerline cuts entered through the notch on the span's axis, then its remainder pieces, each ramped in from the kerf the shared cut opened. Capture marks the shared spans as tabs on the owned clean copy, so replay's contour accounting proves each perimeter is cut exactly once; a lead that fits at neither end refuses as UnsupportedGeometry naming the span, never shortened. Material still comes from the drawing, so utilization, quantity and overlap results do not change. Members are never support-tabbed; locked or excluded members and broken groups refuse. The CI Fiber post writes shared cuts in the cut-off form with a lead-in and no G41/G42, and remainders in the contour form on their perimeter's compensation side, named by a remainder comment. Cincinnati CL (whose PreApplied mode offsets nothing) and Gravograph refuse such nests, as do the desktop and console for any post without ICommonLinePostProcessor; nothing falls back to a silent double cut. Plate > Arrange > Cut Common Lines Twice keeps slice 1's behavior, persists with the plate and makes an open plan stale. Pre-post review now checks each span is cut once, on its centerline, by the member cut first, and the rapid check treats a finished member's remainders as an obstacle. Compensation start in void and cancel in kerf are unconfirmed on the CI FIBER 4020; release waits on a 3/16 stainless coupon trial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
307 lines
11 KiB
C#
307 lines
11 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Diagnostics;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.CommonLines;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>A PT42-like part: Width x Height, every corner chamfered, clockwise from the origin side.</summary>
|
|
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;
|
|
}
|
|
}
|