Files
OpenNest/OpenNest.Tests/CuttingPlanning/ContourRecoveryFileTests.cs
T
ajandClaude Opus 5.5 04a6e0eb50 fix(engine): let contour recovery plan scribed and boxed-in holed parts
The cut planner's directed-contour recovery pass gave up on plates the
full search could solve, so they ended "no plan found within the search
limit":

- Each hole was checked as an isolated probe that repeats the part's
  scribes, so every hole-to-hole hop was checked as a detour through
  the scribe start. Scribes and other open cuts are now checked once
  from the arrival, and each hole as a direct hop from the one before.
- Hole entries were ranked only by pierce distance, so the departure
  often ran back across the hole just cut. The nearest hole is still
  tried first, but its entries that leave toward the holes still to
  cut come first.
- The first-ranked outside entry could spend the whole recovery
  allowance. Each outside entry now gets a share of what is left.

The optional private-fixture test now takes its config key, adding
RecoveryCuttingNestPath; it fails on the old search and passes now.

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

110 lines
5.8 KiB
C#

using System.Security.Cryptography;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.IO;
namespace OpenNest.Tests.CuttingPlanning;
public class ContourRecoveryFileTests
{
// Optional private inputs: never embed customer geometry or names in the repository.
// RecoveryCuttingNestPath: scribed parts with aligned slot pairs, and a bolted frame boxed
// in by parts cut before it, that the free search only finishes through contour recovery.
[SkippableTheory]
[InlineData("InterlockedCuttingNestPath", false)]
[InlineData("InterlockedCuttingNestPath", true)]
[InlineData("RecoveryCuttingNestPath", true)]
public void PlateFinishesThroughDesktopBatchWithoutChangingInput(string key, bool lineLeads)
{
var path = TestConfig.GetExistingPath(key);
Skip.If(path == null, $"Configure {key} in ignored test-config.json.");
var bytes = SHA256.HashData(File.ReadAllBytes(path!));
var nest = new NestReader(path).Read();
var plates = nest.Plates.Where(p => p.Parts.Count > 0).ToArray();
Assert.NotEmpty(plates);
var parts = plates.SelectMany(p => p.Parts).ToArray();
var programs = parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)).ToArray();
var poses = parts.Select(p => (p.Location, p.Rotation, p.LeadInsLocked)).ToArray();
var parameters = Parameters(lineLeads);
var proposal = CuttingPlanBatch.Capture(plates, parameters, preservePartOrder: false).Plan();
Assert.True(proposal.CanApply, string.Join(Environment.NewLine, proposal.Describe("in")));
Assert.False(proposal.RequiresWarningAcceptance);
Assert.Equal(plates.Length, proposal.Plates.Count);
foreach (var planned in proposal.Plates)
{
Assert.True(planned.IsOverlapClear);
Assert.True(planned.Result.IndependentlyReplayed);
Assert.Equal(CuttingPlanStatus.Ready, planned.Result.Status);
Assert.InRange(planned.Result.Expansions, 1, CuttingPlanBatch.PlateBudget(planned.Plate.Parts.Count));
Assert.Equal(planned.Plate.Parts.Count, planned.PartCount);
var snapshot = planned.Result.Snapshot;
foreach (var part in planned.Result.ProposedOrder)
{
Assert.Equal(snapshot.Placements[part.SourceOrdinal].Prepared.Count, part.ContourChoices.Count);
Assert.Equal(part.ContourChoices.Count, part.ContourChoices.Select(c => c.ContourOrdinal).Distinct().Count());
}
var replay = CuttingPlanService.ReplayPrograms(snapshot, planned.Result.ProposedOrder, 0, default);
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
Assert.True(replay.IndependentlyReplayed);
Assert.Empty(replay.Findings);
var bounded = CuttingPlanService.Plan(snapshot.WithSearchLimits(1, 16));
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, bounded.Status);
Assert.Equal(1, bounded.Expansions);
Assert.Empty(bounded.ProposedOrder);
Assert.False(bounded.IndependentlyReplayed);
}
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
var cancelled = CuttingPlanBatch.Capture(plates, parameters, false).Plan(token: cancellation.Token);
Assert.True(cancelled.IsCancelled);
Assert.False(cancelled.CanApplyWithWarnings);
Assert.Equal(parts, plates.SelectMany(p => p.Parts));
Assert.Equal(programs, parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
Assert.Equal(poses, parts.Select(p => (p.Location, p.Rotation, p.LeadInsLocked)));
Assert.Equal(bytes, SHA256.HashData(File.ReadAllBytes(path)));
}
// Optional private input: a plate of holed parts whose preferred hole chains used to fail on
// cutouts left back across themselves, spending the whole default budget before a new order.
[SkippableFact]
public void HoledPlatesFindANewOrderWithinTheDefaultBudget()
{
var path = TestConfig.GetExistingPath("HoledReorderNestPath");
Skip.If(path == null, "Configure HoledReorderNestPath in ignored test-config.json.");
var bytes = SHA256.HashData(File.ReadAllBytes(path!));
var nest = new NestReader(path).Read();
// Locked programs are fixed emissions: those plates yield unverified proposals by design.
var plates = nest.Plates.Where(p => p.Parts.Count > 0 && p.Parts.All(part => !part.LeadInsLocked)).ToArray();
Skip.If(plates.Length == 0, "The configured nest has no plate of unlocked parts.");
var parts = plates.SelectMany(p => p.Parts).ToArray();
var programs = parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)).ToArray();
var proposal = CuttingPlanBatch.Capture(plates, Parameters(lineLeads: true), preservePartOrder: false).Plan();
Assert.True(proposal.CanApply, string.Join(Environment.NewLine, proposal.Describe("in")));
foreach (var planned in proposal.Plates)
{
Assert.False(planned.KeptCurrentOrder);
Assert.True(planned.IsReady);
Assert.InRange(planned.Result.Expansions, 1, CuttingPlanBatch.PlateBudget(planned.Plate.Parts.Count));
}
Assert.Equal(programs, parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
Assert.Equal(bytes, SHA256.HashData(File.ReadAllBytes(path)));
}
private static CuttingParameters Parameters(bool lineLeads)
{
var parameters = new CuttingParameters();
if (lineLeads)
{
parameters.ExternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
parameters.InternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
parameters.ArcCircleLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
}
return parameters;
}
}