mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 15:45:44 -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>
197 lines
10 KiB
C#
197 lines
10 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Engine.Jobs.Remote;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Tests.Jobs;
|
|
|
|
/// <summary>
|
|
/// A nest solved elsewhere (the nest server's worker) comes back as the engine's recorded
|
|
/// answers and is replayed here: the proposal must be the local run's, validated and bound
|
|
/// to the caller's own drawings, and a replay that does not fit these drawings must refuse.
|
|
/// </summary>
|
|
public class RemoteNestReplayTests
|
|
{
|
|
private static NestPipelineRequest Request(Drawing a, Drawing b, string engine = "Rectangles")
|
|
{
|
|
var template = new Plate(48, 96) { EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25), PartSpacing = 0.25 };
|
|
template.SetClampRail(new ClampRail(1.5, 0.625, new[] { 6.0, 30.0 }));
|
|
return new NestPipelineRequest(engine,
|
|
[new NestItem { Drawing = a, Quantity = 12 }, new NestItem { Drawing = b, Quantity = 7, Priority = -1 }],
|
|
NestStockBuilder.FromTemplate(template,
|
|
[new PlateOption { Width = 48, Length = 96, Cost = 100 }, new PlateOption { Width = 60, Length = 120, Quantity = 2, Cost = 160 }]),
|
|
new NestJobOptions(maxPlates: 10, salvageRate: 0.5, minimumSalvageDimension: 6));
|
|
}
|
|
|
|
private static NestSolveTranscript Solve(NestPipelineRequest request)
|
|
{
|
|
var recorder = new RecordingNestingEngine(NestingEngineRegistry.Create(request.EngineName));
|
|
NestPipeline.Run(recorder, request.EngineName, request);
|
|
// Over the wire and back, as the server sends it.
|
|
return NestSolveTranscript.FromJson(recorder.Transcript.ToJson());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Rectangles")]
|
|
[InlineData("Default")]
|
|
public void ReplayGivesTheLocalProposalBoundToTheCallersDrawings(string engine)
|
|
{
|
|
var a = new Drawing("a", TestDrawingFactory.Rectangle(10, 20));
|
|
var b = new Drawing("b", TestDrawingFactory.LShape());
|
|
var request = Request(a, b, engine);
|
|
var submission = new RemoteNestSubmission(request);
|
|
var transcript = Solve(request);
|
|
|
|
var local = NestPipeline.Run(request);
|
|
var replayed = submission.Complete(transcript);
|
|
|
|
Assert.True(replayed.IsValid, string.Join("; ", replayed.Violations));
|
|
Assert.True(replayed.CanKeep);
|
|
Assert.Equal(local.Plates.Count, replayed.Plates.Count);
|
|
for (var i = 0; i < local.Plates.Count; i++)
|
|
{
|
|
Assert.Equal(local.Plates[i].Quantity, replayed.Plates[i].Quantity);
|
|
Assert.Equal(local.Plates[i].Stock.Id, replayed.Plates[i].Stock.Id);
|
|
Assert.Same(request.Stock.Single(s => s.Id == replayed.Plates[i].Stock.Id), replayed.Plates[i].Stock);
|
|
Assert.Equal(
|
|
local.Plates[i].Parts.Select(p => (p.BaseDrawing.Name, p.Location.X, p.Location.Y, p.Rotation)),
|
|
replayed.Plates[i].Parts.Select(p => (p.BaseDrawing.Name, p.Location.X, p.Location.Y, p.Rotation)));
|
|
Assert.All(replayed.Plates[i].Parts, p => Assert.True(ReferenceEquals(p.BaseDrawing, a) || ReferenceEquals(p.BaseDrawing, b)));
|
|
}
|
|
Assert.Equal(local.Unplaced.Count, replayed.Unplaced.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void CutoutTrialAndOrdinarySolveReplayInOrder()
|
|
{
|
|
// A 20 x 20 frame with a 12 x 14 window, a strap that fits the window, and a square
|
|
// that fits nowhere on the one sheet: the bundled trial is incomplete, so the pipeline
|
|
// also runs the ordinary solve, and the replay must answer both in order.
|
|
var frame = TestDrawingFactory.Rectangle(20, 20);
|
|
frame.Codes.Add(new RapidMove(4, 3));
|
|
frame.Codes.Add(new LinearMove(16, 3));
|
|
frame.Codes.Add(new LinearMove(16, 17));
|
|
frame.Codes.Add(new LinearMove(4, 17));
|
|
frame.Codes.Add(new LinearMove(4, 3));
|
|
var request = new NestPipelineRequest("Default",
|
|
[
|
|
new NestItem { Drawing = new Drawing("frame", frame), Quantity = 1 },
|
|
new NestItem { Drawing = new Drawing("strap", TestDrawingFactory.Rectangle(13, 1.25)), Quantity = 1 },
|
|
new NestItem { Drawing = new Drawing("square", TestDrawingFactory.Rectangle(15, 15)), Quantity = 1 },
|
|
],
|
|
new[] { new NestPlateStock("sheet", new Size(20.5, 20.5), 1, 0.25) });
|
|
var submission = new RemoteNestSubmission(request);
|
|
var transcript = Solve(request);
|
|
Assert.True(transcript.Solves.Count >= 2, $"Expected the trial and the ordinary solve, got {transcript.Solves.Count}.");
|
|
|
|
var local = NestPipeline.Run(request);
|
|
var replayed = submission.Complete(transcript);
|
|
|
|
Assert.Equal(local.IsValid, replayed.IsValid);
|
|
Assert.Equal(local.Plates.SelectMany(p => p.Parts).Select(p => (p.BaseDrawing.Name, p.Location.X, p.Location.Y)),
|
|
replayed.Plates.SelectMany(p => p.Parts).Select(p => (p.BaseDrawing.Name, p.Location.X, p.Location.Y)));
|
|
Assert.Equal(local.Unplaced.Select(u => u.ToString()), replayed.Unplaced.Select(u => u.ToString()));
|
|
}
|
|
|
|
[Fact]
|
|
public void ADrawingEditedAfterSendingRefusesTheResult()
|
|
{
|
|
var a = new Drawing("a", TestDrawingFactory.Rectangle(10, 20));
|
|
var b = new Drawing("b", TestDrawingFactory.LShape());
|
|
var request = Request(a, b);
|
|
var submission = new RemoteNestSubmission(request);
|
|
var transcript = Solve(request);
|
|
|
|
a.Program = TestDrawingFactory.Rectangle(11, 20);
|
|
|
|
var refused = Assert.Throws<NestReplayMismatchException>(() => submission.Complete(transcript));
|
|
Assert.Contains("'a'", refused.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnswersForADifferentJobDoNotReplay()
|
|
{
|
|
var a = new Drawing("a", TestDrawingFactory.Rectangle(10, 20));
|
|
var b = new Drawing("b", TestDrawingFactory.LShape());
|
|
var transcript = Solve(Request(a, b));
|
|
var other = Request(a, b);
|
|
other.Items[0].Quantity = 13;
|
|
|
|
Assert.Throws<NestReplayMismatchException>(() => new RemoteNestSubmission(other).Complete(transcript));
|
|
}
|
|
|
|
[Fact]
|
|
public void StockSurvivesTheWireBitForBit()
|
|
{
|
|
var outline = new SheetOutline(
|
|
new[] { new Vector(0, 0), new Vector(96, 0), new Vector(96, 48), new Vector(0, 48) },
|
|
new[] { new[] { new Vector(10, 10), new Vector(12, 10), new Vector(12, 12.125), new Vector(10, 12.125) } });
|
|
var template = new Plate(48, 96) { Quadrant = 3, EdgeSpacing = new Spacing(0.1, 0.2, 0.3, 0.4), PartSpacing = 0.1 + 0.2 };
|
|
template.SetClampRail(new ClampRail(1.5, 0.625, new[] { new RailClamp(ClampEdge.OriginSide, 6), new RailClamp(ClampEdge.FarSide, 40.1) }, 7, 0.3));
|
|
var stock = NestStockBuilder.FromTemplate(template, [new PlateOption { Width = 48, Length = 96, Cost = 1.1, Quantity = 3, Id = "ml-7" }])
|
|
.Append(new NestPlateStock("odd", new Size(48, 96), null, 0.25, new Spacing(0, 0, 0, 0), 1, null, null, outline))
|
|
.ToList();
|
|
|
|
foreach (var sheet in stock)
|
|
{
|
|
var json = System.Text.Json.JsonSerializer.Serialize(NestStockJson.From(sheet));
|
|
var back = System.Text.Json.JsonSerializer.Deserialize<NestStockJson>(json)!.ToStock();
|
|
Assert.True(RepeatedPlateGrouping.SameStock(sheet, back));
|
|
Assert.Equal(sheet.ClampRail, back.ClampRail);
|
|
Assert.Equal(sheet.SheetClampZones.Select(z => (z.Id, z.Bounds.X, z.Bounds.Y, z.Bounds.Length, z.Bounds.Width)),
|
|
back.SheetClampZones.Select(z => (z.Id, z.Bounds.X, z.Bounds.Y, z.Bounds.Length, z.Bounds.Width)));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OnlyShippedEnginesAreBuiltIn()
|
|
{
|
|
Assert.True(NestingEngineRegistry.IsBuiltIn("Default"));
|
|
Assert.True(NestingEngineRegistry.IsBuiltIn("rectangles"));
|
|
Assert.True(NestingEngineRegistry.IsBuiltIn("Opus55NestingEngine"));
|
|
Assert.False(NestingEngineRegistry.IsBuiltIn("SomePlugIn"));
|
|
Assert.False(NestingEngineRegistry.IsBuiltIn(null));
|
|
}
|
|
|
|
[Fact]
|
|
public void NonfinitePosesSurviveTheWire()
|
|
{
|
|
var transcript = new NestSolveTranscript();
|
|
var stock = new NestPlateStock("s", new Size(10, 10));
|
|
var job = new NestJob([], [stock]);
|
|
transcript.Add(job, new NestJobResult(NestJobStatus.Incomplete, NestJobStopReason.NoPlacementFound,
|
|
[new NestJobPlateResult(0, stock, [new NestJobPlacement("p", 0, double.NaN, double.PositiveInfinity, 0)])], [], []));
|
|
|
|
var back = NestSolveTranscript.FromJson(transcript.ToJson()).Solves.Single().Result.ToResult(job);
|
|
|
|
var pose = back.Plates.Single().Placements.Single();
|
|
Assert.True(double.IsNaN(pose.X));
|
|
Assert.True(double.IsPositiveInfinity(pose.Y));
|
|
Assert.Same(stock, back.Plates.Single().Stock);
|
|
}
|
|
|
|
[Fact]
|
|
public void ADrawingsFingerprintFollowsItsExactGeometryAndTravelsWithTheJob()
|
|
{
|
|
var a = new Drawing("a", TestDrawingFactory.Rectangle(10, 20));
|
|
var same = new Drawing("other name", TestDrawingFactory.Rectangle(10, 20));
|
|
var moved = new Drawing("a", TestDrawingFactory.Rectangle(10, 20.000000001));
|
|
Assert.Equal(64, RemoteNestSubmission.Fingerprint(a).Length);
|
|
Assert.Equal(RemoteNestSubmission.Fingerprint(a), RemoteNestSubmission.Fingerprint(same));
|
|
Assert.NotEqual(RemoteNestSubmission.Fingerprint(a), RemoteNestSubmission.Fingerprint(moved));
|
|
|
|
var b = new Drawing("b", TestDrawingFactory.LShape());
|
|
var request = Request(a, b);
|
|
var document = NestJobDocument.FromJson(NestJobDocument.From(request, [a, b]).ToJson());
|
|
Assert.Equal([RemoteNestSubmission.Fingerprint(a), RemoteNestSubmission.Fingerprint(b)], document.DrawingFingerprints);
|
|
|
|
// Reopened against new copies of the same drawings, the job replays to the local proposal.
|
|
var copyA = new Drawing("a", TestDrawingFactory.Rectangle(10, 20));
|
|
var copyB = new Drawing("b", TestDrawingFactory.LShape());
|
|
var reopened = RemoteNestSubmission.Reopen(document, [copyB, copyA]).Complete(Solve(request));
|
|
var local = NestPipeline.Run(Request(copyA, copyB));
|
|
Assert.Equal(local.Plates.SelectMany(p => p.Parts).Select(p => (p.BaseDrawing, p.Location)),
|
|
reopened.Plates.SelectMany(p => p.Parts).Select(p => (p.BaseDrawing, p.Location)));
|
|
}
|
|
}
|