mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 00:21:06 -04:00
Full circles now take InternalLeadIn/InternalLeadOut like every other cutout; the ArcCircle lead settings and the cutting panel's Arc / Circle tab are removed. Circle-specific geometry handling (radius clamp, angle rounding, pierce zone) is unchanged. Older nest files and desktop settings still load: their arcCircleLeadIn/ arcCircleLeadOut keys are ignored and no longer written, so re-planned circles take the internal leads. Saved part programs are untouched. 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 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 };
|
|
}
|
|
return parameters;
|
|
}
|
|
}
|