using OpenNest.CNC; using OpenNest.Geometry; namespace OpenNest.Tests.CutOffs; public class AutomaticCutOffShiftTests { internal static AutomaticCutOffOptions Options(double scale = 1) { return new AutomaticCutOffOptions { Spacing = 35 * scale, MinimumStripLength = 17.5 * scale }; } internal static Part Rectangle(double length, double width, double x, double y) { var program = new Program(); program.Codes.Add(new RapidMove(0, 0)); program.Codes.Add(new LinearMove(length, 0)); program.Codes.Add(new LinearMove(length, width)); program.Codes.Add(new LinearMove(0, width)); program.Codes.Add(new LinearMove(0, 0)); return new Part(new Drawing("shift rectangle", program), new Vector(x, y)); } internal static Plate UserCase(int quadrant = 1, double scale = 1) { var plate = new Plate(60 * scale, 120 * scale) { Quadrant = quadrant, PartSpacing = 0.3 * scale }; foreach (var right in new[] { 34.75, 69.75, 80.0 }) plate.Parts.Add(Rectangle((right == 80 ? 5 : 10) * scale, (right == 80 ? 5 : 20) * scale, (quadrant is 2 or 3 ? -right : right - (right == 80 ? 5 : 10)) * scale, (quadrant is 3 or 4 ? -30 : 10) * scale)); return plate; } internal static double CutLength(Program program) => Enumerable.Range(0, program.Codes.Count / 2) .Sum(i => System.Math.Abs(((LinearMove)program.Codes[2 * i + 1]).EndPoint.Y - ((RapidMove)program.Codes[2 * i]).EndPoint.Y)); [Theory] [InlineData(1, 1)] [InlineData(2, 1)] [InlineData(3, 1)] [InlineData(4, 1)] [InlineData(1, 25.4)] [InlineData(2, 25.4)] [InlineData(3, 25.4)] [InlineData(4, 25.4)] public void ParallelEdgeCase_SavesFortyInchesWithSameCountAndSeparator(int quadrant, double scale) { var plate = UserCase(quadrant, scale); var settings = new CutOffSettings { PartClearance = 0.02 * scale }; var legacy = AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 35 * scale }, settings); var shifted = AutomaticCutOffPlanner.Create(plate, Options(scale), settings); Assert.Equal(legacy.TailSeparatorX, shifted.TailSeparatorX); Assert.Equal(3, shifted.Definitions.Count); Assert.True(legacy.PreviewParts.Sum(p => CutLength(p.Program)) - shifted.PreviewParts.Sum(p => CutLength(p.Program)) >= 39.9 * scale); AssertLimits(shifted, 35 * scale, 17.5 * scale); Assert.Empty(plate.CutOffs); } [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(0.00002)] [InlineData(36)] [InlineData(double.NaN)] [InlineData(double.PositiveInfinity)] public void MinimumStrip_RejectsInvalidValues(double minimum) { var options = Options(); options.MinimumStripLength = minimum; Assert.Throws(() => AutomaticCutOffPlanner.Create(UserCase(), options, new CutOffSettings())); } [Fact] public void EqualCosts_SelectNominalPositions_AndLastStripMayBeShort() { var plate = new Plate(60, 120) { PartSpacing = 0.3 }; plate.Parts.Add(Rectangle(40, 20, 0, 10)); var plan = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); Assert.Equal(new[] { 35.0, 40.3 + AutomaticCutOffPlanner.GeometryTolerance }, plan.Definitions.Select(c => c.Position.X)); Assert.DoesNotContain(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ShiftedCut); Assert.Equal(CutLength(plan.PreviewParts[0].Program), plan.StripCutLength, 8); Assert.Equal(plan.StripCutLength, plan.FixedGridCutLength); AssertLimits(plan, 35, 17.5); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] public void NullAndEqualSpacing_AreIdenticalIncludingDiagnosticsAndTotals(int quadrant) { var plate = UserCase(quadrant); plate.CutOffs.Add(new CutOff(new Vector(quadrant is 2 or 3 ? -35 : 35, 0), CutOffAxis.Vertical)); var options = new AutomaticCutOffOptions { Spacing = 35 }; var legacy = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings()); options.MinimumStripLength = 35; var fixedGrid = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings()); Assert.Equal(legacy.Definitions.Select(c => (c.Position, c.Axis, c.StartLimit, c.EndLimit)), fixedGrid.Definitions.Select(c => (c.Position, c.Axis, c.StartLimit, c.EndLimit))); Assert.Equal(legacy.PreviewParts.Select(p => p.Program.ToString()), fixedGrid.PreviewParts.Select(p => p.Program.ToString())); Assert.Equal(legacy.Diagnostics, fixedGrid.Diagnostics); Assert.Equal(legacy.TailSeparatorX, fixedGrid.TailSeparatorX); Assert.Equal(legacy.UsedSpan, fixedGrid.UsedSpan); Assert.Equal(legacy.StripCutLength, fixedGrid.StripCutLength); Assert.Equal(legacy.FixedGridCutLength, fixedGrid.FixedGridCutLength); Assert.Equal(legacy.PreviewParts.Take(legacy.Definitions.Count - (legacy.HasSeparatedTail ? 1 : 0)) .Sum(p => CutLength(p.Program)), legacy.StripCutLength, 8); Assert.Equal(legacy.StripCutLength, legacy.FixedGridCutLength); } [Fact] public void SeededTwoHundredPlates_NeverLonger_RespectLimitsAndAreDeterministic() { var random = new Random(42131); for (var sample = 0; sample < 200; sample++) { var quadrant = sample % 4 + 1; var plate = new Plate(60, 120) { Quadrant = quadrant, PartSpacing = 0.3 }; for (var index = 0; index < 4; index++) { var right = 20 + random.NextDouble() * 90; var width = 2 + random.NextDouble() * 8; var height = 2 + random.NextDouble() * 12; var top = 20 + random.NextDouble() * 20; plate.Parts.Add(Rectangle(width, height, quadrant is 2 or 3 ? -right : right - width, quadrant is 3 or 4 ? -top : top - height)); } var options = Options(); var settings = new CutOffSettings(); var first = AutomaticCutOffPlanner.Create(plate, options, settings); var second = AutomaticCutOffPlanner.Create(plate, options, settings); var legacy = AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 35 }, settings); Assert.Equal(legacy.Definitions.Count, first.Definitions.Count); Assert.Equal(legacy.TailSeparatorX, first.TailSeparatorX); Assert.True(first.StripCutLength <= first.FixedGridCutLength + 0.000001); Assert.Equal(first.Definitions.Select(c => c.Position.X), second.Definitions.Select(c => c.Position.X)); Assert.Equal(first.Diagnostics, second.Diagnostics); Assert.Equal(first.StripCutLength, second.StripCutLength); Assert.Equal(first.PreviewParts.Take(first.PreviewParts.Count - 1).Sum(p => CutLength(p.Program)), first.StripCutLength, 8); AssertLimits(first, 35, 17.5); } } [Theory] [InlineData(1, false)] [InlineData(2, false)] [InlineData(3, false)] [InlineData(4, false)] [InlineData(1, true)] [InlineData(2, true)] [InlineData(3, true)] [InlineData(4, true)] public void FullSpanManualLine_AtTwentyIsBoundary_NotAnExtraObstacle(int quadrant, bool explicitLimits) { var plate = UserCase(quadrant); var sign = quadrant is 2 or 3 ? -1 : 1; var settings = new CutOffSettings { Overtravel = 0.2 }; var manual = new CutOff(new Vector(sign * 20, 0), CutOffAxis.Vertical); if (explicitLimits) { var bounds = plate.BoundingBox(false); manual.StartLimit = bounds.Bottom; manual.EndLimit = bounds.Top + settings.Overtravel; } plate.CutOffs.Add(manual); var plan = AutomaticCutOffPlanner.Create(plate, Options(), settings); Assert.False(plan.HasBlockingDiagnostics); Assert.Equal(2, plan.Definitions.Count); // The feasible window 45.3..55 misses every rectangle, so all costs tie. // One unbroken full-width segment at the gap-relative nominal X wins. Assert.Equal(sign * 55, plan.Definitions[0].Position.X); Assert.Single(plan.PreviewParts[0].Program.Codes.OfType()); Assert.Equal(60.2, CutLength(plan.PreviewParts[0].Program), 8); Assert.Equal(plan.StripCutLength, plan.FixedGridCutLength); Assert.InRange(System.Math.Abs(plan.Definitions[0].Position.X) - 20, 17.5, 35); Assert.InRange(plan.UsedSpan - System.Math.Abs(plan.Definitions[0].Position.X), 0, 35); Assert.Same(manual, Assert.Single(plate.CutOffs)); } [Theory] [InlineData(1, -1)] [InlineData(2, -1)] [InlineData(3, -1)] [InlineData(4, -1)] [InlineData(1, 0)] [InlineData(2, 0)] [InlineData(3, 0)] [InlineData(4, 0)] [InlineData(1, 1)] [InlineData(2, 1)] [InlineData(3, 1)] [InlineData(4, 1)] public void LimitedManualLine_AtBestPosition_IsAvoidedWithoutBlocking(int quadrant, int toleranceOffset) { var plate = UserCase(quadrant); var best = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); var x = best.Definitions[0].Position.X; var manualX = x + toleranceOffset * AutomaticCutOffPlanner.GeometryTolerance; plate.CutOffs.Add(new CutOff(new Vector(manualX, 0), CutOffAxis.Vertical) { EndLimit = plate.BoundingBox(false).Bottom + 2, }); var plan = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); Assert.False(plan.HasBlockingDiagnostics); Assert.Equal(3, plan.Definitions.Count); Assert.DoesNotContain(plan.Definitions, c => c.Position.X == x || System.Math.Abs(c.Position.X - manualX) <= AutomaticCutOffPlanner.GeometryTolerance); Assert.True(plan.StripCutLength < plan.FixedGridCutLength); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] public void LimitedManualLine_AtSeparator_StillBlocksAndEmptiesProposedTotals(int quadrant) { var plate = UserCase(quadrant); var first = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); plate.CutOffs.Add(new CutOff(new Vector(first.TailSeparatorX!.Value, 0), CutOffAxis.Vertical) { EndLimit = plate.BoundingBox(false).Bottom + 2, }); var plan = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); Assert.True(plan.HasBlockingDiagnostics); Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.LimitedCutOffConflict && d.IsBlocking); Assert.False(plan.HasSeparatedTail); Assert.Empty(plan.Definitions); Assert.Empty(plan.PreviewParts); Assert.Equal(0, plan.StripCutLength); Assert.True(plan.FixedGridCutLength > 0); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] public void ApplyAndRerun_FullSpanDefinitionsMakeEveryGapANoOp(int quadrant) { var plate = UserCase(quadrant); var settings = new CutOffSettings(); var plan = AutomaticCutOffPlanner.Create(plate, Options(), settings); plate.CutOffs.AddRange(plan.Definitions); plate.RegenerateCutOffs(settings); var parts = plate.Parts.ToArray(); var rerun = AutomaticCutOffPlanner.Create(plate, Options(), settings); Assert.Empty(rerun.Definitions); Assert.False(rerun.HasBlockingDiagnostics); Assert.Equal(plan.TailSeparatorX, rerun.TailSeparatorX); Assert.Equal(parts, plate.Parts.ToArray()); Assert.Equal(0, rerun.StripCutLength); Assert.Equal(0, rerun.FixedGridCutLength); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] public void FullSpanLineInTail_IsIgnoredAsAStripBoundary(int quadrant) { var plate = UserCase(quadrant); var before = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); plate.CutOffs.Add(new CutOff(new Vector(quadrant is 2 or 3 ? -110 : 110, 0), CutOffAxis.Vertical)); var after = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); Assert.Equal(before.Definitions.Select(c => c.Position.X), after.Definitions.Select(c => c.Position.X)); Assert.Equal(before.StripCutLength, after.StripCutLength); Assert.Equal(before.FixedGridCutLength, after.FixedGridCutLength); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] public void UnsortedDuplicateBoundaries_SpacingSizedGapsNeedNoNewLines(int quadrant) { var plate = UserCase(quadrant); var sign = quadrant is 2 or 3 ? -1 : 1; foreach (var distance in new[] { 55.0, 110, 20, 55, 0 }) plate.CutOffs.Add(new CutOff(new Vector(sign * distance, 0), CutOffAxis.Vertical)); var plan = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings()); Assert.False(plan.HasBlockingDiagnostics); Assert.Equal(plan.TailSeparatorX, Assert.Single(plan.Definitions).Position.X); Assert.Equal(0, plan.StripCutLength); Assert.Equal(0, plan.FixedGridCutLength); Assert.Equal(5, plate.CutOffs.Count); } [Theory] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] public void LimitedManualLine_WhenNoAllowedCandidate_FallbackStillBlocks(int quadrant) { var plate = UserCase(quadrant); var sign = quadrant is 2 or 3 ? -1 : 1; plate.CutOffs.Add(new CutOff(new Vector(sign * 35, 0), CutOffAxis.Vertical) { EndLimit = plate.BoundingBox(false).Bottom + 2, }); var options = Options(); // Only nominal 35 is on the lattice in this first-line window. options.MinimumStripLength = 35 - AutomaticCutOffPlanner.GeometryTolerance / 2; var plan = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings()); Assert.True(plan.HasBlockingDiagnostics); Assert.Contains(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.LimitedCutOffConflict && d.IsBlocking); Assert.Empty(plan.Definitions); Assert.False(plan.HasSeparatedTail); Assert.Equal(0, plan.StripCutLength); Assert.True(plan.FixedGridCutLength > 0); } [Fact] public void LegacyGridReportsActualNewStripCutLengthWithoutChangingItsDefinitions() { var plate = new Plate(60, 120) { PartSpacing = 0.3 }; plate.Parts.Add(Rectangle(5, 5, 75, 10)); var settings = new CutOffSettings(); var options = new AutomaticCutOffOptions { Spacing = 35 }; var legacy = AutomaticCutOffPlanner.Create(plate, options, settings); Assert.Equal(new[] { 35.0, 70.0 }, legacy.Definitions.Take(2).Select(c => c.Position.X)); Assert.Equal(120, legacy.PreviewParts.Take(2).Sum(p => CutLength(p.Program))); Assert.Equal(120, legacy.StripCutLength); Assert.Equal(legacy.StripCutLength, legacy.FixedGridCutLength); options.MinimumStripLength = 35; var equal = AutomaticCutOffPlanner.Create(plate, options, settings); Assert.Equal(legacy.Definitions.Select(c => c.Position.X), equal.Definitions.Select(c => c.Position.X)); Assert.Equal(legacy.Diagnostics, equal.Diagnostics); Assert.Equal(legacy.StripCutLength, equal.StripCutLength); Assert.Equal(legacy.FixedGridCutLength, equal.FixedGridCutLength); } [Fact] public void ShiftedSearchKeepsNominalLinesWithoutRejectingAValidDenseGrid() { var plate = new Plate(60, 120) { PartSpacing = 0 }; plate.Parts.Add(Rectangle(0.01, 0.01, 109.97009, 0.5)); var settings = new CutOffSettings(); var fixedGrid = AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 0.1 }, settings); Assert.Equal(1101, fixedGrid.Definitions.Count); var shifted = AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 0.1, MinimumStripLength = 0.05 }, settings); Assert.Equal(fixedGrid.Definitions.Count, shifted.Definitions.Count); Assert.True(shifted.StripCutLength <= shifted.FixedGridCutLength + 0.000001); } internal static void AssertLimits(AutomaticCutOffPlan plan, double spacing, double minimum) { var positions = plan.Definitions.Where(c => c.Position.X != plan.TailSeparatorX) .Select(c => System.Math.Abs(c.Position.X)).ToArray(); var previous = 0.0; foreach (var position in positions) { Assert.InRange(position - previous, minimum - AutomaticCutOffPlanner.GeometryTolerance, spacing + AutomaticCutOffPlanner.GeometryTolerance); previous = position; } Assert.InRange(plan.UsedSpan - previous, 0, spacing + AutomaticCutOffPlanner.GeometryTolerance); } }