mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-29 03:41:12 -04:00
Plates with staged automatic scrap cut-offs could not be planned: P260810-01 plate 1 ran out of budget even at 10x effort, while the same plate plans Ready in 3.6k expansions without the lines. A cut-off was one fixed program cut whole before every part its line passes through, which dragged the tour to the middle of the sheet and left the head at the far edge of the line; and cutting every line first (the rejected alternative) weakens the sheet and breaks the containment operators rely on when they fast-forward a changed program. A cut-off stays one definition, but Plate.RegenerateCutOffs now makes one part per trimmed segment, each with its own place in the cut sequence. CutOffSegments gives each segment the span of the nominal line it stands for (end of the segment before it to start of the one after), and the dependency rules of the planner and Part Sequencing use that span, so a segment precedes only the parts beside it. The free-order search plans the parts as if the segments were not there, so adding lines no longer changes a plate's route, then CutOffInsertion slides each segment into the latest gap before its first part where the rapid into its pierce and the rapid from its end cross nothing cut before; only when one fits nowhere is the plate searched with the segments among the parts. The search looks through segments wherever they sit (entries face the next part, hole routes rank from the pre-segment position, rapids are still checked from the segment's end), so an applied plan replanned with the order kept replays Ready. Segments saved out of consecutive order are recorded per segment in the .nest file (`segments`); older readers load them consecutively. P260810-01 plate 1 now plans Ready in 3639 expansions with every check passing; rapid travel 1836 against 1547 without the lines. Plates without cut-offs keep their PlanningCharacterizationTests digests, and the problem-nest corpus has no plate below its baseline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
217 lines
9.9 KiB
C#
217 lines
9.9 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.IO;
|
|
|
|
namespace OpenNest.Tests.CutOffs;
|
|
|
|
public class AutomaticCutOffBatchTests
|
|
{
|
|
private static readonly AutomaticCutOffOptions Options = new() { Spacing = 35 };
|
|
|
|
[Fact]
|
|
public void Apply_AllPlates_UsesEachSheetAndPreservesRealPartsAndManualSequence()
|
|
{
|
|
var settings = new CutOffSettings();
|
|
var plates = new[] { CreatePlate(1, 36, 120), CreatePlate(3, 50, 100), new Plate(60, 120) };
|
|
var realParts = plates.SelectMany(p => p.Parts).ToArray();
|
|
var realPrograms = realParts.Select(p => p.Program).ToArray();
|
|
var quantities = realParts.Select(p => p.BaseDrawing.Quantity.Nested).ToArray();
|
|
var manual = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
|
|
plates[0].CutOffs.Add(manual);
|
|
plates[0].RegenerateCutOffs(settings);
|
|
var manualPart = plates[0].Parts[^1];
|
|
plates[0].Parts.Remove(manualPart);
|
|
plates[0].Parts.Insert(0, manualPart);
|
|
|
|
var preview = AutomaticCutOffBatch.Create(plates, Options, settings);
|
|
Assert.Same(manual, Assert.Single(plates[0].CutOffs));
|
|
Assert.Empty(plates[1].CutOffs);
|
|
Assert.Empty(preview[2].Definitions);
|
|
|
|
var plans = AutomaticCutOffBatch.Apply(plates, Options, settings);
|
|
|
|
Assert.All(plans, p => Assert.False(p.HasBlockingDiagnostics));
|
|
Assert.Equal(3, plans[0].Definitions.Count);
|
|
Assert.Equal(3, plans[1].Definitions.Count);
|
|
Assert.Same(manual, plates[0].CutOffs[0]);
|
|
Assert.Same(manual.Drawing, plates[0].Parts[0].BaseDrawing);
|
|
Assert.Same(realParts[0], plates[0].Parts[1]);
|
|
Assert.Same(realParts[1], plates[1].Parts[0]);
|
|
Assert.All(plates.Take(2), p => Assert.Equal(TestHelpers.CutOffPartCount(p), p.Parts.Count(x => x.BaseDrawing.IsCutOff)));
|
|
Assert.All(plates[1].CutOffs, c => Assert.True(c.Position.X < 0));
|
|
var tailProgram = plates[1].Parts[^1].Program;
|
|
Assert.Equal(50, tailProgram.BoundingBox().Width, 6);
|
|
Assert.Empty(plates[2].Parts);
|
|
Assert.Empty(plates[2].CutOffs);
|
|
Assert.Equal(realPrograms, realParts.Select(p => p.Program));
|
|
Assert.Equal(quantities, realParts.Select(p => p.BaseDrawing.Quantity.Nested));
|
|
Assert.All(preview.SelectMany(p => p.PreviewParts), p =>
|
|
Assert.DoesNotContain(p, plates.SelectMany(plate => plate.Parts)));
|
|
|
|
var originalParts = plates.Select(p => p.Parts.ToArray()).ToArray();
|
|
var repeated = AutomaticCutOffBatch.Apply(plates, Options, settings);
|
|
Assert.All(repeated, p => Assert.Empty(p.Definitions));
|
|
for (var i = 0; i < plates.Length; i++)
|
|
Assert.Equal(originalParts[i], plates[i].Parts.ToArray());
|
|
|
|
var nest = new Nest("batch-cutoffs");
|
|
foreach (var part in realParts)
|
|
nest.Drawings.Add(part.BaseDrawing);
|
|
nest.Plates.AddRange(plates);
|
|
using var stream = new MemoryStream();
|
|
Assert.True(new NestWriter(nest).Write(stream));
|
|
stream.Position = 0;
|
|
var loaded = new NestReader(stream).Read();
|
|
// The existing writer omits empty plates.
|
|
Assert.Equal(2, loaded.Plates.Count);
|
|
for (var i = 0; i < loaded.Plates.Count; i++)
|
|
{
|
|
Assert.Equal(plates[i].CutOffs.Select(c => (c.Position.X, c.Axis, c.StartLimit, c.EndLimit)),
|
|
loaded.Plates[i].CutOffs.Select(c => (c.Position.X, c.Axis, c.StartLimit, c.EndLimit)));
|
|
Assert.Equal(plates[i].Parts.Select(p => p.BaseDrawing.IsCutOff),
|
|
loaded.Plates[i].Parts.Select(p => p.BaseDrawing.IsCutOff));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Apply_ConflictOnLaterPlate_ChangesNothingOnAnyPlate()
|
|
{
|
|
var plates = new[] { CreatePlate(), CreatePlate() };
|
|
var settings = new CutOffSettings();
|
|
var preview = AutomaticCutOffBatch.Create(plates, Options, settings);
|
|
Assert.All(preview, p => Assert.NotEmpty(p.Definitions));
|
|
var limited = new CutOff(new Vector(35, 0), CutOffAxis.Vertical) { EndLimit = 2 };
|
|
plates[1].CutOffs.Add(limited);
|
|
var beforeParts = plates.Select(p => p.Parts.ToArray()).ToArray();
|
|
|
|
var result = AutomaticCutOffBatch.Apply(plates, Options, settings);
|
|
|
|
Assert.True(result[1].HasBlockingDiagnostics);
|
|
Assert.Empty(plates[0].CutOffs);
|
|
Assert.Same(limited, Assert.Single(plates[1].CutOffs));
|
|
for (var i = 0; i < plates.Length; i++)
|
|
Assert.Equal(beforeParts[i], plates[i].Parts.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void Apply_InvalidLaterPlate_ReportsPlateNumberBeforeChangingAnything()
|
|
{
|
|
var plates = new[] { CreatePlate(), CreatePlate() };
|
|
plates[1].Parts[0].Offset(500, 0);
|
|
|
|
var error = Assert.Throws<ArgumentException>(() =>
|
|
AutomaticCutOffBatch.Apply(plates, Options, new CutOffSettings()));
|
|
|
|
Assert.Contains("Plate 2:", error.Message);
|
|
Assert.All(plates, p => Assert.Empty(p.CutOffs));
|
|
Assert.All(plates, p => Assert.Single(p.Parts));
|
|
}
|
|
|
|
[Fact]
|
|
public void Apply_ReplansCurrentPartsAndSettingsInsteadOfAcceptingPreview()
|
|
{
|
|
var plates = new[] { CreatePlate(), CreatePlate() };
|
|
var settings = new CutOffSettings();
|
|
var preview = AutomaticCutOffBatch.Create(plates, Options, settings);
|
|
plates[1].Parts[0].Offset(5, 0);
|
|
settings.PartClearance = 2;
|
|
var expected = AutomaticCutOffPlanner.Create(plates[1], Options, settings);
|
|
|
|
var result = AutomaticCutOffBatch.Apply(plates, Options, settings);
|
|
|
|
Assert.NotEqual(preview[1].TailSeparatorX, result[1].TailSeparatorX);
|
|
Assert.Equal(expected.Definitions.Select(c => c.Position.X), plates[1].CutOffs.Select(c => c.Position.X));
|
|
}
|
|
|
|
[Fact]
|
|
public void Apply_RegenerationFailureOnLaterPlate_RestoresEveryTouchedPlate()
|
|
{
|
|
var plates = new[] { CreatePlate(), CreatePlate(), CreatePlate() };
|
|
var settings = new CutOffSettings();
|
|
foreach (var plate in plates)
|
|
{
|
|
plate.CutOffs.Add(new CutOff(new Vector(5, 0), CutOffAxis.Vertical));
|
|
plate.RegenerateCutOffs(settings);
|
|
var cutoffPart = plate.Parts[^1];
|
|
plate.Parts.Remove(cutoffPart);
|
|
plate.Parts.Insert(0, cutoffPart);
|
|
}
|
|
var beforeParts = plates.Select(p => p.Parts.ToArray()).ToArray();
|
|
var beforeDefinitions = plates.Select(p => p.CutOffs.ToArray()).ToArray();
|
|
var beforePrograms = plates.Select(p => p.CutOffs[0].Drawing.Program).ToArray();
|
|
var quantities = plates.Select(p => p.Parts[1].BaseDrawing.Quantity.Nested).ToArray();
|
|
var failOnce = true;
|
|
plates[1].PartAdded += (_, e) =>
|
|
{
|
|
if (failOnce && e.Item.BaseDrawing.IsCutOff)
|
|
{
|
|
failOnce = false;
|
|
throw new InvalidOperationException("Injected regeneration failure");
|
|
}
|
|
};
|
|
|
|
var error = Assert.Throws<InvalidOperationException>(() =>
|
|
AutomaticCutOffBatch.Apply(plates, Options, settings));
|
|
|
|
Assert.False(failOnce);
|
|
Assert.Contains("original cut-offs were restored", error.Message);
|
|
for (var i = 0; i < plates.Length; i++)
|
|
{
|
|
Assert.Equal(beforeParts[i], plates[i].Parts.ToArray());
|
|
Assert.Equal(beforeDefinitions[i], plates[i].CutOffs.ToArray());
|
|
Assert.Same(beforePrograms[i], plates[i].CutOffs[0].Drawing.Program);
|
|
Assert.Equal(quantities[i], plates[i].Parts[1].BaseDrawing.Quantity.Nested);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Apply_MinimumTailIsEvaluatedIndependentlyForEachPlate()
|
|
{
|
|
var plates = new[] { CreatePlate(length: 90), CreatePlate(), new Plate(36, 90) };
|
|
var options = new AutomaticCutOffOptions { Spacing = 35, MinimumTailLength = 12 };
|
|
|
|
var result = AutomaticCutOffBatch.Apply(plates, options, new CutOffSettings());
|
|
|
|
Assert.False(result[0].HasSeparatedTail);
|
|
Assert.Equal(new[] { 35.0, 70.0 }, plates[0].CutOffs.Select(c => c.Position.X));
|
|
Assert.True(result[1].HasSeparatedTail);
|
|
Assert.Equal(3, plates[1].CutOffs.Count);
|
|
Assert.Empty(plates[2].CutOffs);
|
|
Assert.Empty(AutomaticCutOffBatch.Apply(plates, options, new CutOffSettings()).SelectMany(p => p.Definitions));
|
|
}
|
|
|
|
[Fact]
|
|
public void Apply_RollbackFailureIsExplicitAndDoesNotStopRecoveryOfOtherPlates()
|
|
{
|
|
var plates = new[] { CreatePlate(), CreatePlate() };
|
|
var originals = plates.Select(p => p.Parts.ToArray()).ToArray();
|
|
plates[1].PartAdded += (_, _) => throw new InvalidOperationException("Apply observer failed");
|
|
plates[1].CutOffs.ItemRemoved += (_, _) => throw new InvalidOperationException("Rollback observer failed");
|
|
|
|
var error = Assert.Throws<InvalidOperationException>(() =>
|
|
AutomaticCutOffBatch.Apply(plates, Options, new CutOffSettings()));
|
|
|
|
Assert.Contains("Restoring cut-offs also failed", error.Message);
|
|
Assert.Contains("Plate 2", error.Message);
|
|
Assert.Contains("may be incomplete", error.Message);
|
|
Assert.IsType<AggregateException>(error.InnerException);
|
|
Assert.Empty(plates[0].CutOffs);
|
|
Assert.Equal(originals[0], plates[0].Parts.ToArray());
|
|
}
|
|
|
|
private static Plate CreatePlate(int quadrant = 1, double width = 36, double length = 120)
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(70, 0));
|
|
program.Codes.Add(new LinearMove(70, 20));
|
|
program.Codes.Add(new LinearMove(0, 20));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
var plate = new Plate(width, length) { Quadrant = quadrant, PartSpacing = 0.5, Quantity = 2 };
|
|
var drawing = new Drawing($"rectangle-q{quadrant}", program);
|
|
var location = new Vector(quadrant is 2 or 3 ? -80 : 10, quadrant is 3 or 4 ? -28 : 8);
|
|
plate.Parts.Add(new Part(drawing, location));
|
|
return plate;
|
|
}
|
|
}
|