refactor(io): persist best fits through an explicit IBestFitStore

Reading a nest used to fill the engine's static BestFitCache as a side
effect, which is the only reason OpenNest.IO referenced OpenNest.Engine.
Core now defines SavedPairSet/SavedPairResult and IBestFitStore; the
mapping to BestFitResult moved verbatim into Engine's BestFitCacheStore.
NestReader/NestWriter take the store as an optional BestFitStore property
and skip bestfits/ entries without one.

Every production reader and writer opts into BestFitCacheStore.Shared, so
behaviour is unchanged; the golden round-trip test still matches byte for
byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-16 14:54:43 -04:00
1 parent 063362a674
commit 6e66a1efe7
17 files changed
+515 -119

No files matched your search

+3 -2
View File
@@ -5,6 +5,7 @@ using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Jobs;
using OpenNest.IO;
@@ -101,7 +102,7 @@ public class NestResponse
var nestEntry = zip.CreateEntry("nest.nest");
using var nestMs = new MemoryStream();
new NestWriter(Nest).Write(nestMs);
new NestWriter(Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(nestMs);
nestMs.Position = 0;
await using (var stream = nestEntry.Open())
{
@@ -158,7 +159,7 @@ public class NestResponse
await stream.CopyToAsync(nestMs);
}
nestMs.Position = 0;
nest = new NestReader(nestMs).Read();
nest = new NestReader(nestMs) { BestFitStore = BestFitCacheStore.Shared }.Read();
}
return new NestResponse
+3 -2
View File
@@ -5,6 +5,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
@@ -290,7 +291,7 @@ namespace OpenNest.Benchmark
// Manifest jobs have no source nest to copy from, so they keep the job's name.
if (job.SourceFile.EndsWith(".nest", StringComparison.OrdinalIgnoreCase))
{
var source = new OpenNest.IO.NestReader(job.SourceFile).Read();
var source = new OpenNest.IO.NestReader(job.SourceFile) { BestFitStore = BestFitCacheStore.Shared }.Read();
materialized.Nest.Name = source.Name;
materialized.Nest.Units = source.Units;
materialized.Nest.Material = source.Material;
@@ -316,7 +317,7 @@ namespace OpenNest.Benchmark
throw new InvalidOperationException(
"Output must not overwrite the source nest."
);
new OpenNest.IO.NestWriter(materialized.Nest).Write(path);
new OpenNest.IO.NestWriter(materialized.Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(path);
var report = new
{
Source = job.SourceFile,
+2 -1
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.IO;
@@ -39,7 +40,7 @@ namespace OpenNest.Benchmark
try
{
nest = new NestReader(file).Read();
nest = new NestReader(file) { BestFitStore = BestFitCacheStore.Shared }.Read();
}
catch (Exception ex)
{
+4 -3
View File
@@ -9,6 +9,7 @@ using System.Threading;
using OpenNest;
using OpenNest.Diagnostics;
using OpenNest.Engine;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Geometry;
@@ -273,7 +274,7 @@ static class NestConsole
// If we have a nest file, load it and optionally add DXFs.
if (nestFile != null)
{
var nest = new NestReader(nestFile).Read();
var nest = new NestReader(nestFile) { BestFitStore = BestFitCacheStore.Shared }.Read();
if (nest.Plates.Count == 0)
{
@@ -377,7 +378,7 @@ static class NestConsole
return;
}
var templateNest = new NestReader(options.TemplateFile).Read();
var templateNest = new NestReader(options.TemplateFile) { BestFitStore = BestFitCacheStore.Shared }.Read();
var templatePlate = templateNest.PlateDefaults.CreateNew();
plate.Quadrant = templatePlate.Quadrant;
plate.EdgeSpacing = templatePlate.EdgeSpacing;
@@ -599,7 +600,7 @@ static class NestConsole
var outputFile = NestOutputPath(options);
new NestWriter(nest).Write(outputFile);
new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared }.Write(outputFile);
Console.WriteLine($"Saved: {outputFile}");
}
+51
View File
@@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace OpenNest.BestFit
{
/// <summary>One saved best-fit pair result, as persisted in a .nest archive.</summary>
public sealed record SavedPairResult(
double Part1Rotation,
double Part2Rotation,
double Part2OffsetX,
double Part2OffsetY,
int StrategyType,
int TestNumber,
double CandidateSpacing,
double RotatedArea,
double BoundingWidth,
double BoundingHeight,
double OptimalRotation,
bool Keep,
string Reason,
double TrueArea,
IReadOnlyList<double> HullAngles
);
/// <summary>The saved best-fit results for one drawing at one plate size and spacing.</summary>
public sealed record SavedPairSet(
double PlateWidth,
double PlateHeight,
double Spacing,
IReadOnlyList<SavedPairResult> Results
);
/// <summary>
/// Persists best-fit pair caches across .nest round trips. Implementations own their
/// cache; readers and writers never assume one exists, and a file without saved sets is
/// always valid because the data can be recomputed.
/// </summary>
public interface IBestFitStore
{
/// <summary>
/// Sets to save for <paramref name="drawing"/>, limited to the given
/// (plate width, plate length, part spacing) keys.
/// </summary>
IReadOnlyList<SavedPairSet> Export(
Drawing drawing,
IReadOnlySet<(double Width, double Length, double Spacing)> plateKeys
);
/// <summary>Restores saved sets for <paramref name="drawing"/>.</summary>
void Import(Drawing drawing, IReadOnlyList<SavedPairSet> sets);
}
}
@@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.BestFit;
using OpenNest.Geometry;
namespace OpenNest.Engine.BestFit
{
/// <summary>
/// Saves and restores <see cref="BestFitCache"/> entries for .nest archives. Only kept
/// results are exported; imported sets are added without replacing computed ones.
/// </summary>
public sealed class BestFitCacheStore : IBestFitStore
{
/// <summary>The store over the process-wide <see cref="BestFitCache"/>.</summary>
public static BestFitCacheStore Shared { get; } = new();
private BestFitCacheStore() { }
public IReadOnlyList<SavedPairSet> Export(
Drawing drawing,
IReadOnlySet<(double Width, double Length, double Spacing)> plateKeys
)
{
var allBestFits = BestFitCache.GetAllForDrawing(drawing);
var sets = new List<SavedPairSet>();
foreach (var kvp in allBestFits)
{
if (!plateKeys.Contains((kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing)))
continue;
var results = kvp
.Value.Where(r => r.Keep)
.Select(r => new SavedPairResult(
r.Candidate.Part1Rotation,
r.Candidate.Part2Rotation,
r.Candidate.Part2Offset.X,
r.Candidate.Part2Offset.Y,
r.Candidate.StrategyIndex,
r.Candidate.TestNumber,
r.Candidate.Spacing,
r.RotatedArea,
r.BoundingWidth,
r.BoundingHeight,
r.OptimalRotation,
r.Keep,
r.Reason,
r.TrueArea,
r.HullAngles
))
.ToList();
sets.Add(new SavedPairSet(kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing, results));
}
return sets;
}
public void Import(Drawing drawing, IReadOnlyList<SavedPairSet> sets)
{
foreach (var set in sets)
{
var results = set
.Results.Select(r => new BestFitResult
{
Candidate = new PairCandidate
{
Drawing = drawing,
Part1Rotation = r.Part1Rotation,
Part2Rotation = r.Part2Rotation,
Part2Offset = new Vector(r.Part2OffsetX, r.Part2OffsetY),
StrategyIndex = r.StrategyType,
TestNumber = r.TestNumber,
Spacing = r.CandidateSpacing,
},
RotatedArea = r.RotatedArea,
BoundingWidth = r.BoundingWidth,
BoundingHeight = r.BoundingHeight,
OptimalRotation = r.OptimalRotation,
Keep = r.Keep,
Reason = r.Reason,
TrueArea = r.TrueArea,
HullAngles = r.HullAngles?.ToList(),
})
.ToList();
BestFitCache.Populate(drawing, set.PlateWidth, set.PlateHeight, set.Spacing, results);
}
}
}
}
@@ -0,0 +1,136 @@
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])] });
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));
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_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));
}
}
+34 -39
View File
@@ -6,9 +6,9 @@ using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using OpenNest.Bending;
using OpenNest.BestFit;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using static OpenNest.IO.NestFormat;
@@ -24,6 +24,12 @@ namespace OpenNest.IO
/// <summary>Part programs that could not be restored; other parts still load.</summary>
public IReadOnlyList<string> Warnings => warnings.AsReadOnly();
/// <summary>
/// Receives saved best-fit sets. Without a store, <c>bestfits/</c> entries are skipped
/// unread; they are only a cache.
/// </summary>
public IBestFitStore BestFitStore { get; init; }
public NestReader(string file)
{
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
@@ -274,6 +280,9 @@ namespace OpenNest.IO
private void ReadBestFits(Dictionary<int, Drawing> drawingMap)
{
if (BestFitStore == null)
return;
foreach (var kvp in drawingMap)
{
var entry = zipArchive.GetEntry($"bestfits/bestfit-{kvp.Key}");
@@ -288,47 +297,33 @@ namespace OpenNest.IO
if (sets == null)
continue;
PopulateBestFitSets(kvp.Value, sets);
BestFitStore.Import(kvp.Value, sets.Select(FromDto).ToList());
}
}
private void PopulateBestFitSets(Drawing drawing, List<BestFitSetDto> sets)
{
foreach (var set in sets)
{
var results = set
.Results.Select(r => new BestFitResult
{
Candidate = new PairCandidate
{
Drawing = drawing,
Part1Rotation = r.Part1Rotation,
Part2Rotation = r.Part2Rotation,
Part2Offset = new Vector(r.Part2OffsetX, r.Part2OffsetY),
StrategyIndex = r.StrategyType,
TestNumber = r.TestNumber,
Spacing = r.CandidateSpacing,
},
RotatedArea = r.RotatedArea,
BoundingWidth = r.BoundingWidth,
BoundingHeight = r.BoundingHeight,
OptimalRotation = r.OptimalRotation,
Keep = r.Keep,
Reason = r.Reason,
TrueArea = r.TrueArea,
HullAngles = r.HullAngles,
})
.ToList();
BestFitCache.Populate(
drawing,
set.PlateWidth,
set.PlateHeight,
set.Spacing,
results
);
}
}
private static SavedPairSet FromDto(BestFitSetDto set) =>
new(
set.PlateWidth,
set.PlateHeight,
set.Spacing,
set.Results.Select(r => new SavedPairResult(
r.Part1Rotation,
r.Part2Rotation,
r.Part2OffsetX,
r.Part2OffsetY,
r.StrategyType,
r.TestNumber,
r.CandidateSpacing,
r.RotatedArea,
r.BoundingWidth,
r.BoundingHeight,
r.OptimalRotation,
r.Keep,
r.Reason,
r.TrueArea,
r.HullAngles
)).ToList()
);
/// <summary>
/// The saved machine clamp rail, or null when absent, empty or malformed (a bad rail
+44 -55
View File
@@ -7,8 +7,8 @@ using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using OpenNest.BestFit;
using OpenNest.CNC;
using OpenNest.Engine.BestFit;
using static OpenNest.IO.NestFormat;
namespace OpenNest.IO
@@ -23,6 +23,12 @@ namespace OpenNest.IO
private readonly Dictionary<int, string> drawingHashes = new();
private readonly Dictionary<string, Program> partPrograms = new();
/// <summary>
/// Supplies best-fit sets to save. Without a store, no <c>bestfits/</c> entries are
/// written; they are only a cache.
/// </summary>
public IBestFitStore BestFitStore { get; init; }
public NestWriter(Nest nest)
{
this.drawingDict = new Dictionary<int, Drawing>();
@@ -364,64 +370,19 @@ namespace OpenNest.IO
return list;
}
private List<BestFitSetDto> BuildBestFitDtos(Drawing drawing)
{
var allBestFits = BestFitCache.GetAllForDrawing(drawing);
var sets = new List<BestFitSetDto>();
// Only save best-fit sets for plate sizes actually used in this nest.
var plateSizes = new HashSet<(double, double, double)>();
foreach (var plate in nest.Plates)
plateSizes.Add((plate.Size.Width, plate.Size.Length, plate.PartSpacing));
foreach (var kvp in allBestFits)
{
if (
!plateSizes.Contains((kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing))
)
continue;
var results = kvp
.Value.Where(r => r.Keep)
.Select(r => new BestFitResultDto
{
Part1Rotation = r.Candidate.Part1Rotation,
Part2Rotation = r.Candidate.Part2Rotation,
Part2OffsetX = r.Candidate.Part2Offset.X,
Part2OffsetY = r.Candidate.Part2Offset.Y,
StrategyType = r.Candidate.StrategyIndex,
TestNumber = r.Candidate.TestNumber,
CandidateSpacing = r.Candidate.Spacing,
RotatedArea = r.RotatedArea,
BoundingWidth = r.BoundingWidth,
BoundingHeight = r.BoundingHeight,
OptimalRotation = r.OptimalRotation,
Keep = r.Keep,
Reason = r.Reason ?? "",
TrueArea = r.TrueArea,
HullAngles = r.HullAngles ?? new List<double>(),
})
.ToList();
sets.Add(
new BestFitSetDto
{
PlateWidth = kvp.Key.PlateWidth,
PlateHeight = kvp.Key.PlateHeight,
Spacing = kvp.Key.Spacing,
Results = results,
}
);
}
return sets;
}
private void WriteBestFits(ZipArchive zipArchive)
{
if (BestFitStore == null)
return;
// Only save best-fit sets for plate sizes actually used in this nest.
var plateKeys = new HashSet<(double Width, double Length, double Spacing)>();
foreach (var plate in nest.Plates)
plateKeys.Add((plate.Size.Width, plate.Size.Length, plate.PartSpacing));
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
{
var sets = BuildBestFitDtos(kvp.Value);
var sets = BestFitStore.Export(kvp.Value, plateKeys).Select(ToDto).ToList();
if (sets.Count == 0)
continue;
@@ -433,6 +394,34 @@ namespace OpenNest.IO
}
}
private static BestFitSetDto ToDto(SavedPairSet set) =>
new()
{
PlateWidth = set.PlateWidth,
PlateHeight = set.PlateHeight,
Spacing = set.Spacing,
Results = set
.Results.Select(r => new BestFitResultDto
{
Part1Rotation = r.Part1Rotation,
Part2Rotation = r.Part2Rotation,
Part2OffsetX = r.Part2OffsetX,
Part2OffsetY = r.Part2OffsetY,
StrategyType = r.StrategyType,
TestNumber = r.TestNumber,
CandidateSpacing = r.CandidateSpacing,
RotatedArea = r.RotatedArea,
BoundingWidth = r.BoundingWidth,
BoundingHeight = r.BoundingHeight,
OptimalRotation = r.OptimalRotation,
Keep = r.Keep,
Reason = r.Reason ?? "",
TrueArea = r.TrueArea,
HullAngles = r.HullAngles?.ToList() ?? new List<double>(),
})
.ToList(),
};
private void WritePrograms(ZipArchive zipArchive)
{
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
+3 -2
View File
@@ -2,6 +2,7 @@ using System.ComponentModel;
using System.IO;
using System.Text;
using ModelContextProtocol.Server;
using OpenNest.Engine.BestFit;
using OpenNest.IO;
using OpenNest.Shapes;
using CncProgram = OpenNest.CNC.Program;
@@ -27,7 +28,7 @@ namespace OpenNest.Mcp.Tools
if (!File.Exists(path))
return $"Error: file not found: {path}";
var reader = new NestReader(path);
var reader = new NestReader(path) { BestFitStore = BestFitCacheStore.Shared };
var nest = reader.Read();
_session.Nest = nest;
@@ -101,7 +102,7 @@ namespace OpenNest.Mcp.Tools
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var writer = new NestWriter(nest);
var writer = new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared };
if (!writer.Write(path))
return "Error: failed to write nest file";
@@ -0,0 +1,126 @@
using OpenNest.BestFit;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Shapes;
namespace OpenNest.Tests.BestFit;
[Collection(nameof(FillCacheCollection))]
public class BestFitCacheStoreTests
{
private const double Spacing = 0.25;
private static Drawing MakeDrawing() => new TShape { Width = 10, Height = 8 }.GetDrawing();
private static BestFitResult MakeResult(Drawing drawing, bool keep, string reason = "Valid", List<double> hullAngles = null) =>
new()
{
Candidate = new PairCandidate
{
Drawing = drawing,
Part1Rotation = 0.5,
Part2Rotation = System.Math.PI,
Part2Offset = new Vector(3.25, -1.5),
StrategyIndex = 2,
TestNumber = 7,
Spacing = Spacing,
},
RotatedArea = 90,
BoundingWidth = 12,
BoundingHeight = 7.5,
OptimalRotation = 0.1,
Keep = keep,
Reason = reason,
TrueArea = 70,
HullAngles = hullAngles,
};
[Fact]
public void Export_ReturnsOnlyRequestedPlateKeysAndKeptResults()
{
BestFitCache.Clear();
var drawing = MakeDrawing();
BestFitCache.Populate(drawing, 60, 40, Spacing, new()
{
MakeResult(drawing, keep: true, hullAngles: new() { 0.25 }),
MakeResult(drawing, keep: false, reason: "Too wide"),
});
BestFitCache.Populate(drawing, 14, 30, Spacing, new() { MakeResult(drawing, keep: true) });
var sets = BestFitCacheStore.Shared.Export(
drawing, new HashSet<(double, double, double)> { (60, 40, Spacing), (99, 99, Spacing) });
var set = Assert.Single(sets);
Assert.Equal((60.0, 40.0, Spacing), (set.PlateWidth, set.PlateHeight, set.Spacing));
var result = Assert.Single(set.Results);
Assert.Equal(
new SavedPairResult(0.5, System.Math.PI, 3.25, -1.5, 2, 7, Spacing, 90, 12, 7.5, 0.1, true, "Valid", 70, result.HullAngles),
result);
Assert.Equal(new[] { 0.25 }, result.HullAngles);
}
[Fact]
public void Export_DrawingWithoutCacheEntry_ReturnsNoSets()
{
BestFitCache.Clear();
var sets = BestFitCacheStore.Shared.Export(
MakeDrawing(), new HashSet<(double, double, double)> { (60, 40, Spacing) });
Assert.Empty(sets);
}
[Fact]
public void Import_PopulatesCacheWithCandidatesBoundToTheDrawing()
{
BestFitCache.Clear();
var drawing = MakeDrawing();
var saved = new SavedPairResult(0.5, System.Math.PI, 3.25, -1.5, 2, 7, Spacing, 90, 12, 7.5, 0.1, true, "Valid", 70, new[] { 0.25, -0.5 });
BestFitCacheStore.Shared.Import(drawing, new[] { new SavedPairSet(60, 40, Spacing, new[] { saved }) });
var restored = Assert.Single(BestFitCache.GetAllForDrawing(drawing));
Assert.Equal((60.0, 40.0, Spacing), restored.Key);
var result = Assert.Single(restored.Value);
Assert.Same(drawing, result.Candidate.Drawing);
Assert.Equal(0.5, result.Candidate.Part1Rotation);
Assert.Equal(System.Math.PI, result.Candidate.Part2Rotation);
Assert.Equal(new Vector(3.25, -1.5), result.Candidate.Part2Offset);
Assert.Equal(2, result.Candidate.StrategyIndex);
Assert.Equal(7, result.Candidate.TestNumber);
Assert.Equal(Spacing, result.Candidate.Spacing);
Assert.Equal(90, result.RotatedArea);
Assert.Equal(12, result.BoundingWidth);
Assert.Equal(7.5, result.BoundingHeight);
Assert.Equal(0.1, result.OptimalRotation);
Assert.True(result.Keep);
Assert.Equal("Valid", result.Reason);
Assert.Equal(70, result.TrueArea);
Assert.Equal(new[] { 0.25, -0.5 }, result.HullAngles);
}
[Fact]
public void Import_ExistingSetForTheSameKey_IsKept()
{
BestFitCache.Clear();
var drawing = MakeDrawing();
var existing = new List<BestFitResult> { MakeResult(drawing, keep: true) };
BestFitCache.Populate(drawing, 60, 40, Spacing, existing);
var saved = new SavedPairResult(0, 0, 1, 1, 0, 1, Spacing, 5, 1, 5, 0, true, "Valid", 4, new double[0]);
BestFitCacheStore.Shared.Import(drawing, new[] { new SavedPairSet(60, 40, Spacing, new[] { saved }) });
Assert.Same(existing, BestFitCache.GetAllForDrawing(drawing)[(60, 40, Spacing)]);
}
[Fact]
public void Import_EmptySet_AddsNothing()
{
BestFitCache.Clear();
var drawing = MakeDrawing();
BestFitCacheStore.Shared.Import(drawing, new[] { new SavedPairSet(60, 40, Spacing, new SavedPairResult[0]) });
Assert.Empty(BestFitCache.GetAllForDrawing(drawing));
}
}
+2 -2
View File
@@ -290,11 +290,11 @@ public class BestFitPersistenceTests
private static byte[] Save(Nest nest)
{
using var stream = new MemoryStream();
new NestWriter(nest).Write(stream);
new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared }.Write(stream);
return stream.ToArray();
}
private static Nest Load(byte[] bytes) => new NestReader(new MemoryStream(bytes)).Read();
private static Nest Load(byte[] bytes) => new NestReader(new MemoryStream(bytes)) { BestFitStore = BestFitCacheStore.Shared }.Read();
private static System.Collections.Generic.Dictionary<string, byte[]> ReadEntries(byte[] bytes)
{
+2 -2
View File
@@ -75,7 +75,7 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
Console.Error.WriteLine($"Error: Template not found: {template}");
return 1;
}
templateNest = new NestReader(template).Read();
templateNest = new NestReader(template) { BestFitStore = BestFitCacheStore.Shared }.Read();
Console.WriteLine($"Using template: {template}");
}
@@ -289,7 +289,7 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
plateObj.PartSpacing = s;
plateObj.Parts.AddRange(result.PlacedParts);
var writer = new NestWriter(nestObj);
var writer = new NestWriter(nestObj) { BestFitStore = BestFitCacheStore.Shared };
writer.Write(savedFilePath);
}
+3 -2
View File
@@ -3,6 +3,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Data;
using OpenNest.Engine.BestFit;
using OpenNest.IO;
namespace OpenNest
@@ -42,7 +43,7 @@ namespace OpenNest
LastSaveDate = DateTime.Now;
LastSavePath = path;
var writer = new NestWriter(Nest);
var writer = new NestWriter(Nest) { BestFitStore = BestFitCacheStore.Shared };
writer.Write(path);
}
@@ -50,7 +51,7 @@ namespace OpenNest
public byte[] Serialize()
{
using var stream = new MemoryStream();
new NestWriter(Nest).Write(stream);
new NestWriter(Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(stream);
return stream.ToArray();
}
+5 -5
View File
@@ -685,7 +685,7 @@ namespace OpenNest.Forms
{
try
{
var nest = new NestReader(templatePath).Read();
var nest = new NestReader(templatePath) { BestFitStore = BestFitCacheStore.Shared }.Read();
NestDefaults.FromNest(nest).Save(NestDefaults.DefaultPath);
}
catch (Exception ex)
@@ -737,7 +737,7 @@ namespace OpenNest.Forms
if (!string.Equals(Path.GetExtension(path), NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase))
continue;
var reader = new NestReader(path);
var reader = new NestReader(path) { BestFitStore = BestFitCacheStore.Shared };
var nest = reader.Read();
LoadNest(nest);
ShowNestWarnings(reader);
@@ -781,7 +781,7 @@ namespace OpenNest.Forms
using var dlg = new OpenFileDialog { Filter = NestFormat.FileFilter, Multiselect = true };
if (dlg.ShowDialog(this) == DialogResult.OK)
{
var reader = new NestReader(dlg.FileName);
var reader = new NestReader(dlg.FileName) { BestFitStore = BestFitCacheStore.Shared };
var nest = reader.Read();
LoadNest(nest);
ShowNestWarnings(reader);
@@ -802,7 +802,7 @@ namespace OpenNest.Forms
mnuToolsOptions.Enabled = false;
var snapshot = await NestHistorySession.ReadCurrentAsync(repository, dlg.SelectedId, serverUrl);
using var stream = new MemoryStream(snapshot.Archive);
var reader = new NestReader(stream);
var reader = new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared };
var nest = reader.Read();
// Metadata edits create revisions without rewriting the archive. Use their
// current editable fields rather than silently reverting them on the next save.
@@ -957,7 +957,7 @@ namespace OpenNest.Forms
try
{
// Export never changes the document's save path, remote id, or nest name.
new NestWriter(form.Nest).Write(dlg.FileName);
new NestWriter(form.Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(dlg.FileName);
statusLabel1.Text = $"Exported .nest to {dlg.FileName}";
}
catch (Exception ex)
+2 -1
View File
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Data;
using OpenNest.Engine.BestFit;
using OpenNest.IO;
namespace OpenNest.Forms;
@@ -215,7 +216,7 @@ public sealed class NestHistoryForm : Form
var nest = await Task.Run(() =>
{
using var stream = new MemoryStream(bytes);
var reader = new NestReader(stream);
var reader = new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared };
var loaded = reader.Read();
if (reader.Warnings.Count > 0)
throw new InvalidDataException(string.Join(Environment.NewLine, reader.Warnings));
+4 -3
View File
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Data;
using OpenNest.Engine.BestFit;
using OpenNest.IO;
using Timer = System.Windows.Forms.Timer;
@@ -673,7 +674,7 @@ public sealed class SavedNestsForm : Form
() =>
{
using var stream = new MemoryStream(file);
return NestDetails.FromNest(new NestReader(stream).Read());
return NestDetails.FromNest(new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared }.Read());
},
cancellationToken);
}
@@ -814,7 +815,7 @@ public sealed class SavedNestsForm : Form
var nest = await Task.Run(() =>
{
using var source = new MemoryStream(snapshot.Archive);
var loaded = new NestReader(source).Read();
var loaded = new NestReader(source) { BestFitStore = BestFitCacheStore.Shared }.Read();
// Metadata-only revisions can be newer than the archived fields.
NestRecordFactory.ApplyMetadata(loaded, snapshot.Metadata);
return loaded;
@@ -832,7 +833,7 @@ public sealed class SavedNestsForm : Form
var bytes = await Task.Run(() =>
{
using var output = new MemoryStream();
new NestWriter(nest).Write(output);
new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared }.Write(output);
return output.ToArray();
});
var save = new NestSaveSession();