mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-05 23:05:47 -05:00
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:
1 parent
063362a674
commit
6e66a1efe7
17 files changed
+515
-119
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user