mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 09:21:05 -04:00
Saved best-fit pairs are in the drawing's canonical frame, but the canonical angle is recomputed on load and was not saved with the set. When that computation changes (RotatingCalipers checking every hull edge, marks no longer counted as material), old pairs were replayed in a turned frame and could overlap. Each saved set now records canonicalAngle, and BestFitCacheStore.Import skips sets whose angle no longer matches the recomputed one (1e-9 rad, modulo a full turn). Sets from older files have no angle and are dropped; their pairs are recomputed when next needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
5.5 KiB
C#
154 lines
5.5 KiB
C#
using System.IO.Compression;
|
|
using System.Text;
|
|
using OpenNest.BestFit;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.IO.Tests;
|
|
|
|
public class BestFitStorePersistenceTests
|
|
{
|
|
private static readonly SavedPairResult Result = new(
|
|
0, System.Math.PI, 10.25, -0.5, 1, 4, 0.25, 120, 20, 6, 0.5, true, null!, 100, null!);
|
|
|
|
[Fact]
|
|
public void Write_WithoutStore_WritesNoBestFitEntries()
|
|
{
|
|
var entries = EntryNames(Save(CreateNest(), store: null));
|
|
|
|
Assert.DoesNotContain(entries, IsBestFitEntry);
|
|
}
|
|
|
|
[Fact]
|
|
public void Write_WithStore_PassesUsedPlateKeysAndWritesExportedSets()
|
|
{
|
|
var store = new RecordingStore { ToExport = [new SavedPairSet(60, 40, 0.25, [Result])] };
|
|
|
|
var bytes = Save(CreateNest(), store);
|
|
|
|
var exported = Assert.Single(store.Exported);
|
|
Assert.Equal("Part", exported.Drawing.Name);
|
|
Assert.Equal(
|
|
new[] { (60.0, 40.0, 0.25), (30.0, 20.0, 0.5) }.OrderBy(k => k),
|
|
exported.PlateKeys.OrderBy(k => k));
|
|
Assert.Contains("bestfits/bestfit-1", EntryNames(bytes));
|
|
}
|
|
|
|
[Fact]
|
|
public void Write_StoreExportsNothing_WritesNoBestFitEntry()
|
|
{
|
|
var entries = EntryNames(Save(CreateNest(), new RecordingStore()));
|
|
|
|
Assert.DoesNotContain(entries, IsBestFitEntry);
|
|
}
|
|
|
|
[Fact]
|
|
public void Read_WithStore_ImportsSavedSetsWithWireDefaults()
|
|
{
|
|
var bytes = Save(CreateNest(), new RecordingStore { ToExport = [new SavedPairSet(60, 40, 0.25, [Result], 0.75)] });
|
|
var store = new RecordingStore();
|
|
|
|
var nest = new NestReader(new MemoryStream(bytes)) { BestFitStore = store }.Read();
|
|
|
|
var imported = Assert.Single(store.Imported);
|
|
Assert.Same(nest.Drawings.Single(), imported.Drawing);
|
|
var set = Assert.Single(imported.Sets);
|
|
Assert.Equal((60.0, 40.0, 0.25), (set.PlateWidth, set.PlateHeight, set.Spacing));
|
|
Assert.Equal(0.75, set.CanonicalAngle);
|
|
var result = Assert.Single(set.Results);
|
|
// A null reason and hull list are written as "" and [].
|
|
Assert.Equal(Result with { Reason = "", HullAngles = result.HullAngles }, result);
|
|
Assert.Empty(result.HullAngles);
|
|
}
|
|
|
|
[Fact]
|
|
public void Read_SetWithoutCanonicalAngle_ImportsAnUnknownFrame()
|
|
{
|
|
// Files saved before sets recorded their frame; the store decides whether they are stale.
|
|
const string olderSets = """
|
|
[{ "plateWidth": 60, "plateHeight": 40, "spacing": 0.25, "results": [] }]
|
|
""";
|
|
var bytes = AddEntry(Save(CreateNest(), store: null), "bestfits/bestfit-1", olderSets);
|
|
var store = new RecordingStore();
|
|
|
|
new NestReader(new MemoryStream(bytes)) { BestFitStore = store }.Read();
|
|
|
|
var set = Assert.Single(Assert.Single(store.Imported).Sets);
|
|
Assert.Null(set.CanonicalAngle);
|
|
}
|
|
|
|
[Fact]
|
|
public void Read_WithoutStore_SkipsBestFitEntriesUnparsed()
|
|
{
|
|
var bytes = AddEntry(Save(CreateNest(), store: null), "bestfits/bestfit-1", "not json");
|
|
|
|
var nest = new NestReader(new MemoryStream(bytes)).Read();
|
|
|
|
Assert.Single(nest.Drawings);
|
|
}
|
|
|
|
private static bool IsBestFitEntry(string name) => name.StartsWith("bestfits/", StringComparison.Ordinal);
|
|
|
|
private static Nest CreateNest()
|
|
{
|
|
var program = new CNC.Program();
|
|
program.Codes.Add(new LinearMove(10, 0));
|
|
program.Codes.Add(new LinearMove(10, 5));
|
|
program.Codes.Add(new LinearMove(0, 5));
|
|
program.Codes.Add(new LinearMove(0, 0));
|
|
var drawing = new Drawing("Part", program);
|
|
var nest = new Nest("best fits");
|
|
nest.Drawings.Add(drawing);
|
|
foreach (var (width, length, spacing) in new[] { (60.0, 40.0, 0.25), (30.0, 20.0, 0.5) })
|
|
{
|
|
var plate = new Plate(width, length) { PartSpacing = spacing };
|
|
plate.Parts.Add(new Part(drawing, new Vector(1, 1)));
|
|
nest.Plates.Add(plate);
|
|
}
|
|
return nest;
|
|
}
|
|
|
|
private static byte[] Save(Nest nest, IBestFitStore? store)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
Assert.True(new NestWriter(nest) { BestFitStore = store }.Write(stream));
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static string[] EntryNames(byte[] bytes)
|
|
{
|
|
using var zip = new ZipArchive(new MemoryStream(bytes), ZipArchiveMode.Read);
|
|
return zip.Entries.Select(e => e.FullName).ToArray();
|
|
}
|
|
|
|
private static byte[] AddEntry(byte[] bytes, string name, string text)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
stream.Write(bytes);
|
|
using (var zip = new ZipArchive(stream, ZipArchiveMode.Update, leaveOpen: true))
|
|
{
|
|
using var writer = new StreamWriter(zip.CreateEntry(name).Open(), Encoding.UTF8);
|
|
writer.Write(text);
|
|
}
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private sealed class RecordingStore : IBestFitStore
|
|
{
|
|
public IReadOnlyList<SavedPairSet> ToExport { get; init; } = [];
|
|
|
|
public List<(Drawing Drawing, IReadOnlySet<(double Width, double Length, double Spacing)> PlateKeys)> Exported { get; } = [];
|
|
|
|
public List<(Drawing Drawing, IReadOnlyList<SavedPairSet> Sets)> Imported { get; } = [];
|
|
|
|
public IReadOnlyList<SavedPairSet> Export(
|
|
Drawing drawing, IReadOnlySet<(double Width, double Length, double Spacing)> plateKeys)
|
|
{
|
|
Exported.Add((drawing, plateKeys));
|
|
return ToExport;
|
|
}
|
|
|
|
public void Import(Drawing drawing, IReadOnlyList<SavedPairSet> sets) => Imported.Add((drawing, sets));
|
|
}
|
|
}
|