Files
OpenNest/OpenNest.Tests/Support/SupportReviewTests.cs
T
ajandClaude Opus 5.5 8c7dc41ce8 feat(planning): warn about tipping parts and retire legacy lead-in assignment
Plan Cutting is now the only automatic lead-in assignment, and parts the
support grid cannot hold level are reported wherever they lack a
steadying tab.

- Remove Assign Lead-ins (plate and nest), LeadInAssigner, the lead-in
  tool's Auto-Assign button and the Auto Tab size range; old settings
  with autoTab sizes still load
- Manual Place Lead-in tabs the clicked entry when support tabs are on
  and the grid cannot hold the part (SupportTabSettings)
- New PartSupport finding: Plan Cutting replay and best-effort plans
  warn when a drawing pin, a lead-in that does not fit or no single tab
  leaves a part unsteady; the pre-post review checks each placed
  program's outside tab gap. These warnings replace the advisory
  weak-tab notes; only the inside-corner tab note stays advisory, and
  never for a pin
- Pinned perimeters keep their pin but carry the support analysis
- Draw grid points as pixel-snapped squares without anti-aliasing
- Post fixtures build lead-ins through a test-only TestLeadIns helper
- Docs: support tabs, pre-post verification and planner notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:54:40 -04:00

125 lines
5.2 KiB
C#

using OpenNest.CNC;
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>
/// Parts the support grid cannot hold level: hand-placed lead-ins tab them, Plan Cutting warns
/// when it cannot steady them, and the pre-post review warns about any without a steadying tab.
/// </summary>
public class SupportReviewTests
{
// One row of grid points at y = 2.25 runs under the 10 x 1 strip at (2, 2), below its center.
private static readonly SupportGrid OneRow = new(2, 4, 0, 2.25, stabilityMargin: 0.1);
private static CuttingParameters Parameters(bool tabs = false) => new()
{
ExternalLeadIn = new LineLeadIn { Length = 0.3, ApproachAngle = 90 },
SupportTabs = true,
TabsEnabled = tabs,
TabConfig = new NormalTab { Size = 0.2 },
};
private static (Nest Nest, Plate Plate, Part Part) Strip(SupportBed bed, double height = 1)
{
var nest = new Nest("support");
nest.SetSupportBed(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, height)), new Vector(2, 2));
plate.Parts.Add(part);
return (nest, plate, part);
}
private static void PlaceLeadIn(Part part, CuttingParameters parameters, Vector point, Line edge) =>
part.ApplySingleLeadIn(parameters, point, edge, ContourType.External);
[Fact]
public void ManualLeadIn_TabsAnUnsupportedPartWithoutChangingTheSettings()
{
var (_, plate, part) = Strip(OneRow);
var parameters = Parameters();
var used = SupportTabSettings.ForManualLeadIn(parameters, plate, part);
Assert.NotSame(parameters, used);
Assert.True(used.TabsEnabled);
Assert.False(parameters.TabsEnabled);
}
[Fact]
public void ManualLeadIn_KeepsTheSettingsForASupportedPartOrWithSupportTabsOff()
{
var (_, plate, part) = Strip(new SupportGrid(0.5, 0.5, 0.25, 0.25, stabilityMargin: 0.1));
var parameters = Parameters();
Assert.Same(parameters, SupportTabSettings.ForManualLeadIn(parameters, plate, part));
var (_, tipping, strip) = Strip(OneRow);
parameters.SupportTabs = false;
Assert.Same(parameters, SupportTabSettings.ForManualLeadIn(parameters, tipping, strip));
}
[Fact]
public void PrePost_WarnsAboutAnUnsupportedPartWithoutATab()
{
var (nest, _, part) = Strip(OneRow);
PlaceLeadIn(part, Parameters(), new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1)));
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(nest).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
Assert.Equal(1, finding.PartNumber);
Assert.Contains("no tab", finding.Message);
Assert.Equal(new Vector(7, 2.5), finding.Location);
}
[Fact]
public void PrePost_AcceptsATabThatSteadiesThePart_AndWarnsAboutOneThatDoesNot()
{
var (steadied, _, top) = Strip(OneRow);
PlaceLeadIn(top, Parameters(tabs: true), new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1)));
var (unsteady, _, bottom) = Strip(OneRow);
PlaceLeadIn(bottom, Parameters(tabs: true), new Vector(5, 0), new Line(new Vector(10, 0), new Vector(0, 0)));
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(steadied).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
var finding = Assert.Single(PostVerificationAnalyzer.Analyze(unsteady).Findings,
f => f.Kind == PostVerificationKind.PartSupport);
Assert.Contains("does not steady it", finding.Message);
}
[Fact]
public void PrePost_IgnoresPartsWithoutSupportTabsOrABed()
{
var (nest, _, part) = Strip(OneRow);
var parameters = Parameters();
parameters.SupportTabs = false;
// A placed lead-in keeps its settings on the part, which the review reads first.
PlaceLeadIn(part, parameters, new Vector(5, 1), new Line(new Vector(0, 1), new Vector(10, 1)));
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(nest).Findings, f => f.Kind == PostVerificationKind.PartSupport);
parameters.SupportTabs = true;
nest.SetSupportBed(null);
Assert.DoesNotContain(PostVerificationAnalyzer.Analyze(nest).Findings, f => f.Kind == PostVerificationKind.PartSupport);
}
[Fact]
public void PlanCutting_WarnsWhenNoSingleTabCanSteadyThePart()
{
// The 10 x 0.8 strip lies between grid rows: it rests on nothing, so a tab only hangs it.
var (_, plate, _) = Strip(new SupportGrid(1, 1, 0, 0.9, stabilityMargin: 0.1), height: 0.8);
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.Equal(CuttingPlanStatus.BestEffort, result.Status);
var finding = Assert.Single(result.Findings, f => f.Kind == PostVerificationKind.PartSupport);
Assert.Contains("no single tab can steady it", finding.Message);
}
}