mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 13:41:37 -05:00
Priority worked like a rank (lower first) with a default of 0, and the Edit Drawing form stopped at 0, so moving one part ahead of the rest meant renumbering every other drawing. Higher-first lets a user raise a single part to 1 and leave everything else at the default. Every engine, filler and candidate comparer now serves the highest tier first. Stored values keep their order: - .nest files add `nestPriority` and keep writing the negated value under `priority`, so older readers nest in the same order; files without `nestPriority` are negated on read. Drawing packages strip both, so package hashes are unchanged. - Library settings send both keys the same way for older clients. - Server schema v18 negates stored drawing priorities (an older server refuses the upgraded database). - Job documents move to version 2; version 1 jobs are converted, and an older worker refuses version 2 instead of reversing the order. The Edit Drawing form now accepts negative priorities and says higher numbers nest first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
6.3 KiB
C#
151 lines
6.3 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Geometry;
|
|
using Xunit;
|
|
using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
|
|
using static OpenNest.Engine.Tests.NestingEngines.Shapes;
|
|
|
|
namespace OpenNest.Engine.Tests.NestingEngines;
|
|
|
|
/// <summary>Host contract only; engines retain their own packing-quality regressions.</summary>
|
|
public abstract class EngineContractTests<TEngine> where TEngine : INestingEngine, new()
|
|
{
|
|
/// <summary>A part about <paramref name="size"/> across; an engine limited to one shape class
|
|
/// overrides the shapes below with ones it bids on.</summary>
|
|
protected virtual Program Block(double size) => Rectangle(size, size);
|
|
|
|
/// <summary>Three distinct shapes of decreasing size for mixed jobs.</summary>
|
|
protected virtual Program Mixed(int which) => which switch
|
|
{
|
|
0 => LShape(9, 7, 3),
|
|
1 => Triangle(7, 7),
|
|
_ => LShape(6, 5, 2),
|
|
};
|
|
|
|
/// <summary>A part that fits a 10.4 square sheet unrotated; scribe marks are added to it.</summary>
|
|
protected virtual Program Etchable() => NotchedPartWithEtch();
|
|
|
|
[Fact]
|
|
public void ContractPublicConstructor() => Assert.IsAssignableFrom<INestingEngine>(Activator.CreateInstance(typeof(TEngine)));
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
[InlineData(3)]
|
|
[InlineData(4)]
|
|
public void ContractQuadrants(int quadrant)
|
|
{
|
|
var job = Job([Part("disc", Disc(2), 3), Part("ell", Mixed(2), 3)],
|
|
[Stock("s", 20, 30, 0.2, new Spacing(0.2, 0.3, 0.4, 0.5), quadrant)]);
|
|
var result = new TEngine().Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractOverflowIndicesAndProgress()
|
|
{
|
|
var job = Job([Part("p", Block(8), 3)], [Stock("s", 10, 10)]);
|
|
var commits = new List<NestJobProgress>();
|
|
var result = new TEngine().Solve(job, new Capture(p => { if (p.Stage == NestJobStage.PlateCommitted) commits.Add(p); }));
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
|
Assert.Equal(3, result.Plates.Count);
|
|
Assert.Equal(Enumerable.Range(0, 3), result.Plates.Select(p => p.PlateIndex));
|
|
Assert.Equal(3, commits.Count);
|
|
Assert.Equal(Enumerable.Range(0, 3), commits.Select(p => p.PlateIndex));
|
|
Assert.Equal(Enumerable.Range(1, 3), commits.Select(p => p.CommittedPlates));
|
|
Assert.Equal(Enumerable.Range(1, 3), commits.Select(p => p.CommittedParts));
|
|
Assert.All(result.StockUsage, s => Assert.Null(s.Remaining));
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractOversize()
|
|
{
|
|
var job = Job([Part("huge", Block(50), 1), Part("small", Block(2), 2)], [Stock("s", 10, 10)]);
|
|
var result = new TEngine().Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(1, result.Fulfillment.Single(f => f.PartId == "huge").Unplaced);
|
|
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
|
Assert.Equal(NestJobStopReason.NoPlacementFound, result.StopReason);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractHigherNumberPriorityWins()
|
|
{
|
|
var job = Job([Part("low", Block(8), 1, priority: 0), Part("high", Block(8), 1, priority: 9)],
|
|
[Stock("s", 10, 10, quantity: 1)]);
|
|
var result = new TEngine().Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal("high", Assert.Single(Assert.Single(result.Plates).Placements).PartId);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractEtchOutsideSheetIsIgnored()
|
|
{
|
|
var etched = Etchable();
|
|
etched.Codes.Add(new RapidMove(5, 5));
|
|
etched.Codes.Add(new LinearMove(100, 100) { Layer = LayerType.Scribe });
|
|
var job = Job([Part("p", etched, 1, RotationPolicy.Fixed(0))], [Stock("s", 10.4, 10.4, quantity: 1)]);
|
|
var result = new TEngine().Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractDeterminism()
|
|
{
|
|
NestJob Build() => Job([Part("disc", Disc(2.5), 12), Part("ell", Mixed(0), 12), Part("tri", Mixed(1), 12)],
|
|
[Stock("a", 30, 45, 0.3), Stock("b", 40, 40, 0.3)]);
|
|
var engine = new TEngine();
|
|
var job = Build();
|
|
var first = engine.Solve(job);
|
|
var second = engine.Solve(job);
|
|
var third = new TEngine().Solve(Build());
|
|
foreach (var result in new[] { first, second, third }) LayoutAssert.Valid(job, result);
|
|
Assert.Equal(Describe(first), Describe(second));
|
|
Assert.Equal(Describe(first), Describe(third));
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractCancellationThrows()
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
var job = Job([Part("p", Block(2), 5)], [Stock("s", 10, 10)]);
|
|
Assert.ThrowsAny<OperationCanceledException>(() => new TEngine().Solve(job, token: cancellation.Token));
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractCancellationDuringSolveThrows()
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var job = Job([Part("p", Block(2), 20)], [Stock("s", 10, 10)]);
|
|
var progress = new Capture(p =>
|
|
{
|
|
if (p.Stage == NestJobStage.EvaluatingCandidate) cancellation.Cancel();
|
|
});
|
|
Assert.ThrowsAny<OperationCanceledException>(() => new TEngine().Solve(job, progress, cancellation.Token));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void ContractStockAndPlateLimits(bool plateLimit)
|
|
{
|
|
var job = Job([Part("p", Block(8), 3)],
|
|
[Stock("s", 10, 10, quantity: plateLimit ? null : 1)],
|
|
new NestJobOptions(maxPlates: plateLimit ? 1 : null));
|
|
var result = new TEngine().Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Single(result.Plates);
|
|
Assert.Equal(2, Assert.Single(result.Fulfillment).Unplaced);
|
|
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
|
Assert.Equal(plateLimit ? NestJobStopReason.PlateLimitReached : NestJobStopReason.StockExhausted, result.StopReason);
|
|
}
|
|
|
|
private static string Describe(NestJobResult result) => System.Text.Json.JsonSerializer.Serialize(result);
|
|
private sealed class Capture(Action<NestJobProgress> action) : IProgress<NestJobProgress>
|
|
{ public void Report(NestJobProgress value) => action(value); }
|
|
}
|