mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 01:11:35 -05:00
Small parts on a coarse support grid (the help tutorial's 5 x 5 gussets on a 2 inch grid) rest on one or two points, so no single tab could steady them and every plan came back unverified. New cutting settings now start "Tab alone holds parts up to" at 6; settings saved without the field still read as 0, so existing settings keep their meaning. 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 bed for parts short enough for their tab alone to hold
|
|
/// (<see cref="CuttingParameters.TabHeldMaxLength"/>): a part the bed cannot hold level is still
|
|
/// tabbed, but any tab clear of inside corners steadies it, so it is not reported.
|
|
/// </summary>
|
|
public class TabHeldSupportTabTests
|
|
{
|
|
private const double TabSize = 0.2;
|
|
|
|
// A 2 inch laser grid; a 1.5 inch square rests on one of its points at most.
|
|
private static readonly SupportGrid Bed = new(2, 2, 0, 0, registrationTolerance: 0.125, stabilityMargin: 0.25);
|
|
|
|
private static CuttingParameters Parameters(double tabHeldMaxLength)
|
|
{
|
|
var parameters = ExplicitContourTests.Parameters();
|
|
parameters.SupportTabs = true;
|
|
parameters.TabConfig = new NormalTab { Size = TabSize };
|
|
parameters.TabHeldMaxLength = tabHeldMaxLength;
|
|
return parameters;
|
|
}
|
|
|
|
// A square `size` on a side at (3, 3) on the 2 inch grid.
|
|
private static (Nest Nest, Plate Plate, Part Part) Square(CuttingParameters parameters, double size = 1.5)
|
|
{
|
|
var nest = new Nest("tab held");
|
|
var plate = new Plate(20, 20) { CuttingParameters = parameters };
|
|
nest.Plates.Add(plate);
|
|
nest.SetSupportBed(Bed);
|
|
var part = new Part(new Drawing("square", LeadPathValidationTests.Rectangle(0, 0, size, size)), new Vector(3, 3));
|
|
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, false)] // off
|
|
[InlineData(1, false)]
|
|
[InlineData(1.5, true)] // the bound is inclusive
|
|
[InlineData(2, true)]
|
|
public void TheLimitHoldsPartsUpToIt(double max, bool holdsALengthOfOneAndAHalf)
|
|
{
|
|
Assert.Equal(holdsALengthOfOneAndAHalf, new CuttingParameters { TabHeldMaxLength = max }.TabHoldsAt(1.5));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(45)]
|
|
[InlineData(90)]
|
|
[InlineData(135)]
|
|
[InlineData(180)]
|
|
[InlineData(270)]
|
|
[InlineData(315)]
|
|
public void LengthOfATriangleWithTwoLeastBoxes_DoesNotDependOnItsRotation(double degrees)
|
|
{
|
|
// A 5 x 5 right triangle fits its legs' square and its hypotenuse's 7.07 x 3.54 box, both
|
|
// of area 25: the squarer box is the one measured, whatever way the part is turned.
|
|
var triangle = new Program();
|
|
triangle.MoveTo(0, 0); triangle.LineTo(5, 0); triangle.LineTo(0, 5); triangle.LineTo(0, 0);
|
|
var part = new Part(new Drawing("gusset", triangle), new Vector(10, 10));
|
|
part.Rotate(degrees * System.Math.PI / 180);
|
|
var material = SupportMaterial.FromPart(part);
|
|
|
|
Assert.Equal(5, SupportAnalyzer.Length(material), 6);
|
|
Assert.Equal(5, SupportAnalyzer.Width(material), 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void NewSettingsHoldPartsUpToSixLong()
|
|
{
|
|
Assert.Equal(6, new CuttingParameters().TabHeldMaxLength);
|
|
Assert.True(new CuttingParameters().TabHoldsAt(5));
|
|
}
|
|
|
|
[Fact]
|
|
public void Analyze_HoldsAShortPartTheBedCannotByAnyClearTab()
|
|
{
|
|
var (_, _, part) = Square(Parameters(2));
|
|
var material = SupportMaterial.FromPart(part);
|
|
|
|
var held = SupportTabSettings.Analyze(Parameters(2), Bed, 1, material);
|
|
var bedOnly = SupportTabSettings.Analyze(Parameters(0), Bed, 1, material);
|
|
|
|
Assert.Equal(1.5, SupportAnalyzer.Length(material), 6);
|
|
Assert.False(bedOnly.IsHeldByTab);
|
|
Assert.False(bedOnly.TabCanSupport);
|
|
Assert.True(held.IsHeldByTab);
|
|
Assert.True(held.HasBed);
|
|
Assert.False(held.IsSupported);
|
|
Assert.Equal(bedOnly.Margin, held.Margin);
|
|
Assert.True(held.TabCanSupport);
|
|
Assert.True(held.AcceptsTabAt(new Vector(3, 3.75)));
|
|
Assert.True(held.IsSteadiedByTabAt(new Vector(4.5, 3.75)));
|
|
Assert.False(SupportTabSettings.Analyze(Parameters(1), Bed, 1, material).IsHeldByTab);
|
|
}
|
|
|
|
[Fact]
|
|
public void Analyze_LeavesAPartTheBedHoldsAlone()
|
|
{
|
|
// A 5 inch square rests on a 2 x 2 block of grid points around its center.
|
|
var (_, _, part) = Square(Parameters(6), 5);
|
|
|
|
var support = SupportTabSettings.Analyze(Parameters(6), Bed, 1, SupportMaterial.FromPart(part));
|
|
|
|
Assert.True(support.IsSupported);
|
|
Assert.False(support.IsHeldByTab);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlanCutting_WarnsAboutAShortPartNoTabCanSteady_WithoutTheLimit()
|
|
{
|
|
var (_, plate, _) = Square(Parameters(0));
|
|
|
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
|
|
confirmedParameters: Parameters(0)));
|
|
|
|
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
|
|
Assert.Contains("no single tab can steady it", Assert.Single(result.Findings).Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlanCutting_TabsAShortPartWithoutAWarning()
|
|
{
|
|
var (_, plate, _) = Square(Parameters(2));
|
|
|
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
|
|
confirmedParameters: Parameters(2)));
|
|
|
|
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 PrePost_WarnsOnlyAboutAnUntabbedShortPart()
|
|
{
|
|
var lead = new Line(new Vector(0, 0), new Vector(0, 1.5));
|
|
var (untabbed, _, bare) = Square(Parameters(2));
|
|
bare.ApplySingleLeadIn(Parameters(2), new Vector(0, 0.75), lead, ContourType.External);
|
|
var (tabbed, _, held) = Square(Parameters(2));
|
|
var withTabs = Parameters(2);
|
|
withTabs.TabsEnabled = true;
|
|
held.ApplySingleLeadIn(withTabs, new Vector(0, 0.75), lead, ContourType.External);
|
|
|
|
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(untabbed).Findings,
|
|
f => f.Kind == PostVerificationKind.PartSupport);
|
|
Assert.Contains("short enough for its tab alone to hold", finding.Message);
|
|
Assert.Contains("no tab", finding.Message);
|
|
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(tabbed).Findings,
|
|
f => f.Kind == PostVerificationKind.PartSupport);
|
|
}
|
|
}
|