mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-03 12:21:39 -05:00
fix(cutting): let a part no tab can steady leave from another entry
A support-tabbed part that no single tab can hold level was limited to its one steadiest entry. Two strips stacked in a frame's cutout then blocked the plate: from the upper strip's only entry, every route to the lower one crossed holes already cut. Its other entries clear of inside corners now follow the steadiest, in the search and in contour recovery; the part is still tabbed and still warned about. Parts a tab can hold keep their steadying entries. The unverified headline also named fixed lead-ins when the only warning was a part that may tip; it now says what needs review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0bc97a719
commit
b2cc796e4a
5 files changed
+125
-3
No files matched your search
@@ -491,7 +491,7 @@ public sealed class CuttingPlanProposal
|
||||
+ $"{Count(Plates.Sum(p => p.RegeneratedCount), "part program")} regenerated.");
|
||||
else if (RequiresWarningAcceptance)
|
||||
lines.Add((Plates.All(p => p.IsReady || p.HasOnlyFixedLeadWarnings)
|
||||
? "Unverified plan available: fixed lead-ins could not be certified. "
|
||||
? "Unverified plan available: " + Unverified(Plates.Where(p => !p.IsReady)).Headline + " "
|
||||
: Plates.Any(p => p.Result.IsSafestRouteFallback)
|
||||
? "Safest-route plan available: no route passed every check within the search limit. "
|
||||
: "Best-effort plan available. ")
|
||||
@@ -509,6 +509,19 @@ public sealed class CuttingPlanProposal
|
||||
return lines;
|
||||
}
|
||||
|
||||
// What an unverified plan whose motions all passed asks the operator to review: its fixed
|
||||
// lead-ins, parts its support bed cannot hold level, or both.
|
||||
private static (string Headline, string Plate) Unverified(IEnumerable<CuttingPlanPlateResult> plates)
|
||||
{
|
||||
var kinds = plates.SelectMany(p => p.Result.Findings).Select(f => f.Kind).Where(k => k != null).ToHashSet();
|
||||
return kinds.Count == 1 && kinds.Contains(PostVerificationKind.PartSupport)
|
||||
? ("a part may tip once cut free.", "a part may tip once cut free.")
|
||||
: kinds.Contains(PostVerificationKind.PartSupport)
|
||||
? ("fixed lead-ins could not be certified, and a part may tip once cut free.",
|
||||
"fixed lead-ins need review, and a part may tip once cut free.")
|
||||
: ("fixed lead-ins could not be certified.", "fixed lead-ins need review.");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> DescribePlate(CuttingPlanPlateResult plate, string unit, string changeSettings)
|
||||
{
|
||||
var heading = $"Plate {plate.PlateNumber}: ";
|
||||
@@ -541,7 +554,7 @@ public sealed class CuttingPlanProposal
|
||||
if (plate.CanApplyWithWarnings)
|
||||
{
|
||||
yield return heading + (plate.HasOnlyFixedLeadWarnings
|
||||
? $"unverified: fixed lead-ins need review. {Count(plate.PartCount, "part")}, "
|
||||
? $"unverified: {Unverified([plate]).Plate} {Count(plate.PartCount, "part")}, "
|
||||
+ $"{plate.RegeneratedCount} regenerated; every planned motion passed its checks."
|
||||
: result.SafestRoute is { } severity
|
||||
? $"safest route found, unverified. No route passed every check within the search limit; "
|
||||
|
||||
@@ -739,8 +739,16 @@ internal static class JointCuttingPlanSearch
|
||||
{
|
||||
// Placement origins are authored program coordinates, not a downstream cut.
|
||||
// Use the same material-center look-ahead as the retained entry search.
|
||||
var toward = target ?? node.RoutePosition - source.Location;
|
||||
var ranked = prepared.AutomaticEntryCandidatesWithFallbacks(prepared.PerimeterOrdinal, token: token)
|
||||
.OrderBy(c => c.Choice.Point.DistanceTo(target ?? node.RoutePosition - source.Location)).ToArray();
|
||||
.OrderBy(c => c.Choice.Point.DistanceTo(toward)).ToArray();
|
||||
// As in SelectEntries: when no single tab can steady the part, its other entries
|
||||
// clear of inside corners follow the steadiest.
|
||||
if (prepared.HasSupportTab && prepared.Support is { TabCanSupport: false })
|
||||
ranked = [.. ranked, .. prepared.AutomaticEntryCandidatesWithFallbacks(prepared.PerimeterOrdinal,
|
||||
token: token, supportEntries: SupportEntries.ClearOfInsideCorners)
|
||||
.Where(c => !ranked.Any(r => r.Choice.Point.DistanceTo(c.Choice.Point) <= OpenNest.Math.Tolerance.Epsilon))
|
||||
.OrderBy(c => c.Choice.Point.DistanceTo(toward))];
|
||||
perimeters = ranked.Where(c => Evaluate(source, c).Status == ContourFeasibilityStatus.Clear)
|
||||
.Select(c => c.Choice).ToArray();
|
||||
if (perimeters.Length == 0)
|
||||
@@ -1260,6 +1268,25 @@ internal static class JointCuttingPlanSearch
|
||||
selection = Steadiest(source.Prepared, ContourEntrySelection.Select(ordered,
|
||||
candidate => Evaluate(source, candidate), snapshot.MaxEntries, token));
|
||||
}
|
||||
// When no single tab can steady the part, its steadiest entry is only a preference:
|
||||
// the other entries clear of inside corners follow it, steadiest first, so a part
|
||||
// boxed in by its neighbors can still be left toward the next cut.
|
||||
if (contour == source.Prepared.PerimeterOrdinal && source.Prepared.HasSupportTab
|
||||
&& source.Prepared.Support is { TabCanSupport: false }
|
||||
&& (selection.Choices.Count != 0 || selection.UncertainChoices.Count != 0))
|
||||
{
|
||||
var others = ContourEntrySelection.Select(
|
||||
RankedEntries(source, contour, target, arrival, sightTargets, SupportEntries.ClearOfInsideCorners),
|
||||
candidate => Evaluate(source, candidate), snapshot.MaxEntries, token);
|
||||
var kept = selection.Choices.Concat(selection.UncertainChoices).ToArray();
|
||||
bool IsNew(ContourChoice choice) =>
|
||||
!kept.Any(k => k.Point.DistanceTo(choice.Point) <= OpenNest.Math.Tolerance.Epsilon);
|
||||
selection = selection with
|
||||
{
|
||||
Choices = selection.Choices.Concat(others.Choices.Where(IsNew)).ToArray(),
|
||||
UncertainChoices = selection.UncertainChoices.Concat(others.UncertainChoices.Where(IsNew)).ToArray(),
|
||||
};
|
||||
}
|
||||
// No configured lead-in fits a cutout: pierce where eroding it leaves room.
|
||||
string erodedReason = null;
|
||||
// Entries the check could not settle are no fit either: a clear pierce outranks them.
|
||||
|
||||
@@ -93,6 +93,25 @@ public class CuttingPlanMessageTests
|
||||
Assert.True(part.LeadInsLocked);
|
||||
Assert.Same(program, part.Program);
|
||||
}
|
||||
[Fact]
|
||||
public void Describe_OnlyAnUnsteadyPart_SaysItMayTipNotThatLeadsNeedReview()
|
||||
{
|
||||
// Nothing of the grid runs under the strip, so no single tab can hold it level.
|
||||
var plate = new Plate(20, 20) { SupportBed = new OpenNest.Support.SupportGrid(1, 1, 0, 0.9, stabilityMargin: 0.1) };
|
||||
plate.Parts.Add(new Part(new Drawing("strip", LeadPathValidationTests.Rectangle(0, 0, 10, 0.8)), new Vector(2, 2)));
|
||||
var settings = ExplicitContourTests.Parameters();
|
||||
settings.SupportTabs = true;
|
||||
settings.TabConfig = new NormalTab { Size = 0.2 };
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], settings, false).Plan();
|
||||
var text = string.Join("\n", proposal.Describe("in"));
|
||||
|
||||
Assert.True(proposal.RequiresWarningAcceptance, text);
|
||||
Assert.StartsWith("Unverified plan available: a part may tip once cut free.", text);
|
||||
Assert.Contains("Plate 1: unverified: a part may tip once cut free.", text);
|
||||
Assert.DoesNotContain("fixed lead-ins", text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Describe_LockedLeadHitsNeighbor_WarnsWithAdviceAndNeedsAcceptance()
|
||||
{
|
||||
|
||||
@@ -141,6 +141,62 @@ public class SupportTabPlanningTests
|
||||
Assert.Contains("no single tab can steady it", warning.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripWithALeadOut_IsTabbedAndPlanned()
|
||||
{
|
||||
// The lead-out leaves the tabbed perimeter from its trimmed end, away from the tab: the
|
||||
// open-cut proof shows it cuts no gap, as pre-post review does.
|
||||
var bed = new SupportGrid(2, 4, 0, 2.25, stabilityMargin: 0.1);
|
||||
var (plate, _) = Strip(bed);
|
||||
var parameters = Parameters();
|
||||
parameters.ExternalLeadOut = new LineLeadOut { Length = 0.1, ApproachAngle = 60 };
|
||||
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, new Vector(0, 0),
|
||||
confirmedParameters: parameters));
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, string.Join("\n", result.Findings.Select(f => f.Message)));
|
||||
var proposal = Assert.Single(result.ProposedOrder);
|
||||
var (entry, cutEnd) = EntryAndCutEnd(proposal);
|
||||
Assert.Equal(TabSize, entry.DistanceTo(cutEnd), 6);
|
||||
var motions = ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, null, default).Motions;
|
||||
Assert.Equal(LayerType.Leadout, motions[^1].Layer);
|
||||
}
|
||||
|
||||
// A 10 x 0.8 strip with a 9 x 0.4 slot near its bottom edge: no rapid from its top edge
|
||||
// reaches the plate below it without crossing the slot once that is cut.
|
||||
private static Program SlottedStrip()
|
||||
{
|
||||
var program = new Program();
|
||||
program.Merge(LeadPathValidationTests.Rectangle(0.5, 0.1, 9.5, 0.5));
|
||||
program.Merge(LeadPathValidationTests.Rectangle(0, 0, 10, 0.8));
|
||||
return program;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartNoSingleTabCanHold_LeavesFromAnotherEntryWhenItsSteadiestIsBoxedIn()
|
||||
{
|
||||
// No grid row runs under the slotted strip, so no tab can hold it; its steadiest tab
|
||||
// point is on its top edge, across the slot from the part cut after it.
|
||||
var bed = new SupportGrid(1, 1, 0, 0.9, stabilityMargin: 0.1);
|
||||
var plate = new Plate(20, 20) { SupportBed = bed };
|
||||
var slotted = new Part(new Drawing("slotted", SlottedStrip()), new Vector(2, 2));
|
||||
plate.Parts.Add(slotted);
|
||||
plate.Parts.Add(new Part(new Drawing("below", LeadPathValidationTests.Rectangle(0, 0, 10, 0.8)), new Vector(2, 0.6)));
|
||||
var support = SupportAnalyzer.Analyze(bed, plate.Quadrant, slotted);
|
||||
Assert.False(support.TabCanSupport);
|
||||
Assert.True(support.BestTabPoint!.Value.Y > 2.4, $"best tab {support.BestTabPoint}");
|
||||
|
||||
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, new Vector(0, 0),
|
||||
confirmedParameters: Parameters(), preservePartOrder: true));
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.BestEffort, string.Join("\n", result.Findings.Select(f => f.Message)));
|
||||
var (entry, cutEnd) = EntryAndCutEnd(result.ProposedOrder.Single(p => ReferenceEquals(p.SourcePart, slotted)));
|
||||
Assert.Equal(TabSize, entry.DistanceTo(cutEnd), 6);
|
||||
Assert.True(entry.Y < 2.4, $"entry {entry}");
|
||||
Assert.Contains(result.Findings, f => ReferenceEquals(f.SourcePart, slotted)
|
||||
&& f.Kind == PostVerificationKind.PartSupport && f.Message.Contains("no single tab can steady it"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FallbackCatalog_KeepsClearOfInsideCornersBeforeOfferingEveryEntry()
|
||||
{
|
||||
|
||||
@@ -131,6 +131,13 @@ back first to every catalog entry clear of inside corners, then to every entry
|
||||
lead-in fits, it keeps every one that holds the part level, or else only the steadiest. The
|
||||
open-scrap pierce fallback then tries the last of those catalogs.
|
||||
|
||||
When no single tab can steady the part (it rests on no contacts, or too few), the steadiest
|
||||
entry is only a preference: its other fitting entries clear of inside corners follow it,
|
||||
steadiest first, in the search and in contour recovery. A part boxed in by its neighbors, such as
|
||||
the upper of two strips nested in a cutout, can then be left toward the next cut instead of
|
||||
blocking the plan. It is still tabbed and still warned about as one that may tip. A part a tab
|
||||
can hold level keeps to its steadying entries.
|
||||
|
||||
A drawing pin on the perimeter wins: its entry, leads and tab are kept as pinned, and the
|
||||
analysis is only carried for review. Replay and best-effort plans (`SupportTabCapture.Review`)
|
||||
warn with `PostVerificationKind.PartSupport` whenever an unsupported part's emitted perimeter is
|
||||
|
||||
Reference in new issue
Block a user