mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 03:14:14 -04:00
fix(cutting): explain missing leads and suggest clearance remedies
This commit is contained in:
1 parent
f4d5cfe98e
commit
1c923976a8
5 files changed
+175
-4
No files matched your search
@@ -90,7 +90,9 @@ public static class LeadPathValidator
|
||||
if (ReferenceEquals(material, target))
|
||||
continue;
|
||||
if (CheckMaterial(material, null) != null)
|
||||
return new(true, false, "Lead contacts or enters another placed material.");
|
||||
return new(true, false, "Lead contacts or enters another placed material. "
|
||||
+ "Try spacing the parts farther apart or reducing the lead-in/lead-out length, then replan. "
|
||||
+ "For locked parts, edit the leads or unlock the part before replanning.");
|
||||
}
|
||||
|
||||
string CheckMaterial(LeadMaterialSnapshot material, Vector? permittedJoint)
|
||||
|
||||
@@ -307,14 +307,22 @@ public sealed class CuttingPlanProposal
|
||||
yield break;
|
||||
}
|
||||
|
||||
var missingLead = !plate.IsRouteReady && result.Status != CuttingPlanStatus.Cancelled
|
||||
&& result.Findings.Any(f => f.Kind == PostVerificationKind.MissingLeadIn);
|
||||
yield return heading + (plate.IsRouteReady
|
||||
? "blocked: parts overlap or could not be checked for overlap."
|
||||
: StatusText(result.Status));
|
||||
: missingLead ? "blocked: missing or zero-length lead-in." : StatusText(result.Status));
|
||||
foreach (var line in Limit(DescribeOverlap(plate.Overlap)))
|
||||
yield return line;
|
||||
if (plate.IsRouteReady)
|
||||
yield break;
|
||||
if (plate.KeptCurrentOrder)
|
||||
if (missingLead)
|
||||
{
|
||||
yield return " Open Cutting Settings... and select a lead-in type other than None with "
|
||||
+ "nonzero length for the affected contour (External, Internal, or Arc / Circle), then replan.";
|
||||
yield return " If the affected part is locked, edit its lead-ins or unlock it before replanning.";
|
||||
}
|
||||
else if (plate.KeptCurrentOrder)
|
||||
yield return " No new part order was found within the search limit, and planning with the "
|
||||
+ "current order was refused:";
|
||||
foreach (var line in Limit(result.Findings.Select(DescribeFinding)))
|
||||
|
||||
@@ -451,7 +451,9 @@ internal static class JointCuttingPlanSearch
|
||||
else if (selection.Choices.Count == 0 && selection.UncertainChoices.Count == 0
|
||||
&& reportedNoFit.Add(source.SourceOrdinal * 1000 + contour))
|
||||
rejected.Add(Finding(source, null,
|
||||
$"No tested lead-in fits on part {source.SourceOrdinal}, contour {contour}: {selection.Reason}"));
|
||||
$"No tested lead-in fits on cutting contour {contour + 1}: {selection.Reason} "
|
||||
+ "Try reducing the lead-in length in Cutting Settings...; if nearby parts obstruct the lead-in, "
|
||||
+ "space the parts farther apart. Replan to check the changes."));
|
||||
// Uncertain candidates are NOT refused by the precheck: they reach the emitted-
|
||||
// prefix Check and complete replay, which remain the authority on them.
|
||||
return selection.Choices.Concat(selection.UncertainChoices).Take(snapshot.MaxEntries).ToArray();
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class CuttingPlanMessageTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Describe_MissingOrZeroLengthLead_ExplainsSettingsWithoutBlamingSearch(bool keepOrder, bool zeroLength)
|
||||
{
|
||||
var part = new Part(new Drawing("sample", ExplicitContourTests.Square(false)), new Vector(1, 1));
|
||||
var plate = new Nest().CreatePlate();
|
||||
plate.Size = new Size(100, 100);
|
||||
plate.Parts.Add(part);
|
||||
var program = part.Program;
|
||||
var original = OwnedProgramCopy.Copy(program);
|
||||
var settings = new CuttingParameters();
|
||||
if (zeroLength)
|
||||
settings.ExternalLeadIn = new LineLeadIn { Length = 0, ApproachAngle = 90 };
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], settings, keepOrder).Plan();
|
||||
|
||||
Assert.False(proposal.CanApply);
|
||||
var result = Assert.Single(proposal.Plates).Result;
|
||||
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, result.Status);
|
||||
Assert.Contains(result.Findings, f => f.Kind == PostVerificationKind.MissingLeadIn);
|
||||
var text = string.Join("\n", proposal.Describe("in"));
|
||||
Assert.Contains("Plate 1: blocked: missing or zero-length lead-in.", text);
|
||||
Assert.Contains("Open Cutting Settings...", text);
|
||||
Assert.Contains("other than None", text);
|
||||
Assert.Contains("nonzero length", text);
|
||||
Assert.Contains("Part 1 (sample):", text);
|
||||
Assert.Contains("Cutting contour 1", text);
|
||||
Assert.DoesNotContain("search limit", text);
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply().Status);
|
||||
Assert.Same(program, part.Program);
|
||||
Assert.True(ProgramContent.Equal(original, part.Program));
|
||||
Assert.Null(plate.CuttingParameters);
|
||||
Assert.False(part.HasManualLeadIns);
|
||||
|
||||
// Choosing valid settings fixes the refusal; reporting never changes settings or bypasses checks.
|
||||
var ready = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), keepOrder).Plan();
|
||||
Assert.True(ready.CanApply, string.Join("\n", ready.Describe("in")));
|
||||
Assert.True(Assert.Single(ready.Plates).Result.IndependentlyReplayed);
|
||||
Assert.DoesNotContain("missing or zero-length", string.Join("\n", ready.Describe("in")));
|
||||
Assert.Same(program, part.Program);
|
||||
Assert.True(ProgramContent.Equal(original, part.Program));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Describe_LockedProgramWithoutLead_ExplainsThatSettingsCannotRegenerateIt()
|
||||
{
|
||||
var part = new Part(new Drawing("locked sample", ExplicitContourTests.Square(false)), new Vector(1, 1))
|
||||
{
|
||||
LeadInsLocked = true,
|
||||
};
|
||||
var plate = new Nest().CreatePlate();
|
||||
plate.Size = new Size(100, 100);
|
||||
plate.Parts.Add(part);
|
||||
var program = part.Program;
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.ConstraintConflict, Assert.Single(proposal.Plates).Result.Status);
|
||||
Assert.False(proposal.CanApply);
|
||||
var text = string.Join("\n", proposal.Describe("in"));
|
||||
Assert.Contains("missing or zero-length lead-in", text);
|
||||
Assert.Contains("If the affected part is locked, edit its lead-ins or unlock it before replanning.", text);
|
||||
Assert.Contains("Part 1 (locked sample):", text);
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply().Status);
|
||||
Assert.True(part.LeadInsLocked);
|
||||
Assert.Same(program, part.Program);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Describe_LeadHitsNeighbour_SuggestsSpacingOrShorterLeadWithoutAllowingApply()
|
||||
{
|
||||
var settings = ExplicitContourTests.Parameters();
|
||||
var clean = LeadPathValidationTests.Rectangle(0, 0, 10, 10);
|
||||
var prepared = PreparedContours.Capture(clean, settings);
|
||||
var part = new Part(new Drawing("lead blocked", clean));
|
||||
Assert.True(part.RestoreLeadInProgram(prepared.Emit([prepared.ClosestEntry(0, new Vector(-1, 5))]), true));
|
||||
var obstacle = new Part(new Drawing("neighbour", LeadPathValidationTests.Rectangle(-0.3, 4.5, -0.1, 5.5)));
|
||||
var plate = new Nest().CreatePlate();
|
||||
plate.Parts.Add(part);
|
||||
plate.Parts.Add(obstacle);
|
||||
var original = OwnedProgramCopy.Copy(part.Program);
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], settings, true).Plan();
|
||||
var text = string.Join("\n", proposal.Describe("in"));
|
||||
|
||||
Assert.False(proposal.CanApply);
|
||||
Assert.Contains("another placed material", text);
|
||||
Assert.Contains("spacing the parts farther apart", text);
|
||||
Assert.Contains("reducing the lead-in", text);
|
||||
Assert.Contains("then replan", text);
|
||||
Assert.DoesNotContain("other than None", text);
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply().Status);
|
||||
Assert.True(ProgramContent.Equal(original, part.Program));
|
||||
Assert.True(part.LeadInsLocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Describe_NoTestedLeadFits_SuggestsChangesButDoesNotClaimTheyWillWork()
|
||||
{
|
||||
var settings = ExplicitContourTests.Parameters();
|
||||
var part = new Part(new Drawing("inside", LeadPathValidationTests.Rectangle(0, 0, 10, 10)));
|
||||
var wrap = new Part(new Drawing("wrap", LeadPathValidationTests.Rectangle(-0.05, -0.05, 10.05, 10.05)));
|
||||
var plate = new Nest().CreatePlate();
|
||||
plate.Parts.Add(part);
|
||||
plate.Parts.Add(wrap);
|
||||
|
||||
var proposal = CuttingPlanBatch.Capture([plate], settings, false).Plan();
|
||||
var text = string.Join("\n", proposal.Describe("in"));
|
||||
|
||||
Assert.Contains("No tested lead-in fits", text);
|
||||
Assert.Contains("Try reducing the lead-in length", text);
|
||||
Assert.Contains("if nearby parts obstruct the lead-in, space the parts farther apart", text);
|
||||
Assert.Contains("Replan to check the changes.", text);
|
||||
Assert.False(proposal.CanApply);
|
||||
Assert.Equal(CuttingCommitStatus.InvalidInput, proposal.Apply().Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData(PostVerificationKind.RapidCrossing)]
|
||||
public void Describe_OtherRefusals_KeepTheirStatusAndFindings(PostVerificationKind? kind)
|
||||
{
|
||||
// The presentation uses the typed finding, not matching words in a diagnostic.
|
||||
var finding = new CuttingPlanFinding(null, null, null, null, kind, "MissingLeadIn lookalike text");
|
||||
var result = new CuttingPlanResult(CuttingPlanStatus.NoSolutionWithinBudget, findings: [finding]);
|
||||
var proposal = new CuttingPlanProposal([new(new Nest().CreatePlate(), 7, result, result, null)],
|
||||
ExplicitContourTests.Parameters());
|
||||
|
||||
var text = string.Join("\n", proposal.Describe("in"));
|
||||
|
||||
Assert.Contains("Plate 7: no complete plan was found within the search limit.", text);
|
||||
Assert.Contains("planning with the current order was refused:", text);
|
||||
Assert.Contains(finding.Message, text);
|
||||
Assert.DoesNotContain("Open Cutting Settings...", text);
|
||||
Assert.DoesNotContain("missing or zero-length lead-in", text);
|
||||
Assert.False(proposal.CanApply);
|
||||
}
|
||||
}
|
||||
@@ -279,6 +279,14 @@ plans every plate that has parts. Both open one dialog built on
|
||||
whole-part order; either change replans. The settings are confirmed parameters: every
|
||||
unlocked part's lead-ins are regenerated, and locked parts keep programs that must
|
||||
already pass the checks.
|
||||
- A missing or zero-length lead-in is reported directly, rather than as a search-limit
|
||||
failure. Open `Cutting Settings...`, choose a lead-in other than `None` with a nonzero
|
||||
length on the affected `External`, `Internal`, or `Arc / Circle` tab, then replan.
|
||||
Locked programs require manual lead editing or unlocking before regeneration.
|
||||
When a lead hits another part, the finding suggests more spacing or a shorter lead;
|
||||
when no tested entry fits, it suggests reducing lead-in length and, if neighbours
|
||||
obstruct it, spacing the parts farther apart. These are suggestions, not guaranteed
|
||||
fixes: replanning runs the same checks, and Apply stays blocked until every plate is ready.
|
||||
- Every plate is captured on the UI thread and checked and planned on a worker. Clean part
|
||||
material is checked for overlaps with the pre-post overlap analyzer; overlapping parts or
|
||||
an incomplete check (see [pre-post verification](post-verification.md)) block that plate
|
||||
|
||||
Reference in new issue
Block a user