mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-09 17:55:44 -05:00
Nobody tabs every part, but tabbing by size is useful even on a plate that has a support grid. Cutting Settings > Tabs now offers "Only tab parts the support grid can't hold level" or "Tab parts by size" (CuttingParameters.TabBySize), which ignores the bed and tabs each part whose minimum-area bounding rectangle's narrow side is in the range. The support grid choice still falls back to that range on plates with no bed, as before. The range fields become TabSizeMinWidth/TabSizeMaxWidth; their JSON keys stay noBedTabMinWidth/noBedTabMaxWidth so saved settings and nests read unchanged. TabsEnabled stays as the per-contour flag Plan Cutting, Place Lead-in and drawing pins set on the parts they tab. Saved user settings with "Tab every part" read as tabs by size from 0 to 9999, which tabs the same parts. Only the user's settings migrate; a plate's recorded parameters and pins replay as planned. Plan Cutting, Place Lead-in and the pre-post review all go through SupportTabSettings, so they agree on which parts need a tab. A size-tabbed part is held by any clear tab, so it is never a rapid obstacle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.8 KiB
C#
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.TabSizeMinWidth = min;
|
|
parameters.TabSizeMaxWidth = 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 { TabSizeMinWidth = min, TabSizeMaxWidth = max };
|
|
|
|
Assert.Equal(tabsAWidthOfOne, parameters.TabsBySizeAt(1));
|
|
Assert.False(parameters.TabsBySizeAt(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("tabbed by size", finding.Message);
|
|
Assert.Contains("no tab", finding.Message);
|
|
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(tabbed).Findings,
|
|
f => f.Kind == PostVerificationKind.PartSupport);
|
|
}
|
|
}
|