mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-09 09:55:44 -05:00
The repo mixed variants such as center/centre and color/colour, so a name could be spelled either way and searches missed half the hits. Standardize on the US spelling, matching .NET's own APIs (Color, Center). Wire values owned by other systems keep their spelling: the material ledger's "Cancelled" hold status and GitHub's "cancelled" job status. No behavior change beyond the spelling of a few status messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
6.0 KiB
C#
113 lines
6.0 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.
|
|
// One of its plates also holds a many-holed part that once set every stall, so the plan
|
|
// must finish with most of the budget to spare, not just within it.
|
|
[SkippableTheory]
|
|
[InlineData("InterlockedCuttingNestPath", false, 100)]
|
|
[InlineData("InterlockedCuttingNestPath", true, 100)]
|
|
[InlineData("RecoveryCuttingNestPath", false, 60)]
|
|
[InlineData("RecoveryCuttingNestPath", true, 60)]
|
|
public void PlateFinishesThroughDesktopBatchWithoutChangingInput(string key, bool lineLeads, int budgetPercent)
|
|
{
|
|
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) * budgetPercent / 100);
|
|
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 canceled = CuttingPlanBatch.Capture(plates, parameters, false).Plan(token: cancellation.Token);
|
|
Assert.True(canceled.IsCanceled);
|
|
Assert.False(canceled.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 };
|
|
}
|
|
return parameters;
|
|
}
|
|
}
|