Files
OpenNest/OpenNest.Tests/Support/NoBedSupportTabTests.cs
T
ajandClaude Opus 5.5 479883e7ac feat(cutting): tab parts by width when the plate has no support bed
With support tabs on and no support bed, nothing was tabbed and nothing
said so. A new no-bed width range (NoBedTabMinWidth..NoBedTabMaxWidth,
the short side of the part's minimum-area bounding rectangle) now picks
the parts to tab instead. SupportTabSettings.Analyze makes the choice
for Plan Cutting, hand-placed lead-ins and the pre-post review; without
a bed any tab clear of inside corners counts. With no bed and no range,
the plan notes that nothing was tabbed. Cutting Settings > Tabs gains
the range fields, and the values persist with the cutting parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:02:31 -04:00

170 lines
6.8 KiB
C#

using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.Support;
using OpenNest.Tests.CuttingPlanning;
namespace OpenNest.Tests.Support;
/// <summary>
/// Support tabs on a plate with no support bed: the parts whose width is in the settings' no-bed
/// range are tabbed instead, any tab clear of inside corners counts, and with no range Plan
/// Cutting says that nothing was tabbed.
/// </summary>
public class NoBedSupportTabTests
{
private const double TabSize = 0.2;
private static CuttingParameters Parameters(double min = 0.5, double max = 2)
{
var parameters = ExplicitContourTests.Parameters();
parameters.SupportTabs = true;
parameters.TabConfig = new NormalTab { Size = TabSize };
parameters.NoBedTabMinWidth = min;
parameters.NoBedTabMaxWidth = max;
return parameters;
}
// A 10 x 1 strip at (2, 2) on a plate with no support bed.
private static (Nest Nest, Plate Plate, Part Part) Strip(CuttingParameters parameters)
{
var nest = new Nest("no bed");
var plate = new Plate(20, 20) { CuttingParameters = parameters };
nest.Plates.Add(plate);
var part = new Part(new Drawing("strip", LeadPathValidationTests.Rectangle(0, 0, 10, 1)), new Vector(2, 2));
plate.Parts.Add(part);
return (nest, plate, part);
}
private static double TabGap(FixedProgramPlacement proposal)
{
var motions = ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, null, default).Motions;
var entry = motions.Last(m => m.Layer == LayerType.Leadin).End;
var cutEnd = motions.Last(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display).End;
return entry.DistanceTo(cutEnd);
}
[Theory]
[InlineData(0, 0, false)] // no range
[InlineData(2, 1, false)] // minimum above maximum
[InlineData(0.5, 2, true)]
[InlineData(1, 1, true)] // the bounds are inclusive
public void TheRangeTabsWidthsInsideIt(double min, double max, bool tabsAWidthOfOne)
{
var parameters = new CuttingParameters { NoBedTabMinWidth = min, NoBedTabMaxWidth = max };
Assert.Equal(tabsAWidthOfOne, parameters.TabsWithoutBedAt(1));
Assert.False(parameters.TabsWithoutBedAt(3));
}
[Fact]
public void WithoutABed_TheBestTabCrossesTheCenterAndAnyClearTabIsAccepted()
{
var (_, _, part) = Strip(Parameters());
var material = SupportMaterial.FromPart(part);
var support = SupportAnalyzer.AnalyzeWithoutBed(material);
Assert.True(support.IsComplete);
Assert.False(support.HasBed);
Assert.False(support.IsSupported);
Assert.Equal(1, support.Width, 6);
Assert.Equal(1, SupportAnalyzer.Width(material), 6);
// The nearest of the outline samples to the center line at x = 7.
Assert.InRange(support.BestTabPoint!.Value.X, 6.9, 7.1);
Assert.True(support.AcceptsTabAt(new Vector(2, 2.5)));
Assert.True(support.IsSteadiedByTabAt(new Vector(12, 2.5)));
}
[Fact]
public void Analyze_UsesTheBedWhenThereIsOneAndTheRangeOnlyWithout()
{
var (_, _, part) = Strip(Parameters());
var material = SupportMaterial.FromPart(part);
var bed = new SupportGrid(2, 4, 0, 2.25, stabilityMargin: 0.1);
Assert.True(SupportTabSettings.Analyze(Parameters(), bed, 1, material).HasBed);
Assert.False(SupportTabSettings.Analyze(Parameters(), null, 1, material).HasBed);
Assert.Null(SupportTabSettings.Analyze(Parameters(2, 5), null, 1, material));
Assert.Null(SupportTabSettings.Analyze(Parameters(0, 0), null, 1, material));
var off = Parameters();
off.SupportTabs = false;
Assert.Null(SupportTabSettings.Analyze(off, null, 1, material));
}
[Fact]
public void PlanCutting_TabsAPartInTheRange()
{
var (_, plate, _) = Strip(Parameters());
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.True(result.Status == CuttingPlanStatus.Ready, string.Join("\n", result.Findings.Select(f => f.Message)));
Assert.Equal(TabSize, TabGap(Assert.Single(result.ProposedOrder)), 6);
Assert.Empty(result.Findings);
}
[Fact]
public void PlanCutting_LeavesAPartOutsideTheRangeUntabbed()
{
var (_, plate, _) = Strip(Parameters(2, 5));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(2, 5)));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.Equal(0, TabGap(Assert.Single(result.ProposedOrder)), 6);
Assert.Empty(result.Findings);
}
[Fact]
public void PlanCutting_NotesThatNothingWasTabbedWithoutABedOrARange()
{
var (_, plate, _) = Strip(Parameters(0, 0));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(0, 0)));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.Equal(0, TabGap(Assert.Single(result.ProposedOrder)), 6);
var note = Assert.Single(result.Findings);
Assert.Null(note.Kind);
Assert.Contains("no support bed", note.Message);
}
[Fact]
public void ManualLeadIn_TabsAPartInTheRange()
{
var parameters = Parameters();
var (_, plate, part) = Strip(parameters);
Assert.True(SupportTabSettings.ForManualLeadIn(parameters, plate, part).TabsEnabled);
Assert.Same(parameters, SupportTabSettings.ForManualLeadIn(parameters, plate, new Part(
new Drawing("wide", LeadPathValidationTests.Rectangle(0, 0, 10, 5)), new Vector(2, 5))));
}
[Fact]
public void PrePost_WarnsAboutAnUntabbedPartInTheRange_AndAcceptsAnyTab()
{
var (untabbed, _, bare) = Strip(Parameters());
bare.ApplySingleLeadIn(Parameters(), new Vector(0, 0.5), new Line(new Vector(0, 0), new Vector(0, 1)),
ContourType.External);
var (tabbed, _, held) = Strip(Parameters());
var withTabs = Parameters();
withTabs.TabsEnabled = true;
held.ApplySingleLeadIn(withTabs, new Vector(0, 0.5), new Line(new Vector(0, 0), new Vector(0, 1)),
ContourType.External);
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(untabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
Assert.Contains("without a support bed", finding.Message);
Assert.Contains("no tab", finding.Message);
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(tabbed).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
}
}