From 7081c7b4d08f9190d9ed277202a6b44cefc5a769 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 27 Mar 2026 20:23:00 -0400 Subject: [PATCH] feat: add embedded CL-980 default config with first-run EnsureDefaults Embeds CL-980.json as a resource in OpenNest.Data and adds EnsureDefaults() to LocalJsonProvider, which seeds the machines directory on first run when empty. Co-Authored-By: Claude Sonnet 4.6 --- OpenNest.Data/Defaults/CL-980.json | 174 ++++++++++++++++++++++ OpenNest.Data/LocalJsonProvider.cs | 23 +++ OpenNest.Data/OpenNest.Data.csproj | 3 + OpenNest.Tests/Data/DefaultConfigTests.cs | 75 ++++++++++ 4 files changed, 275 insertions(+) create mode 100644 OpenNest.Data/Defaults/CL-980.json create mode 100644 OpenNest.Tests/Data/DefaultConfigTests.cs diff --git a/OpenNest.Data/Defaults/CL-980.json b/OpenNest.Data/Defaults/CL-980.json new file mode 100644 index 0000000..88ad46e --- /dev/null +++ b/OpenNest.Data/Defaults/CL-980.json @@ -0,0 +1,174 @@ +{ + "id": "00000000-0000-0000-0000-000000980001", + "schemaVersion": 1, + "name": "CL-980", + "type": "laser", + "units": "inches", + "materials": [ + { + "name": "Mild Steel", + "grade": "A36", + "density": 0.2836, + "thicknesses": [ + { + "value": 0.060, + "kerf": 0.008, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 }, + "leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x120", "60x120" ] + }, + { + "value": 0.075, + "kerf": 0.008, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 }, + "leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x120", "60x120" ] + }, + { + "value": 0.105, + "kerf": 0.010, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 }, + "leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x120", "60x120" ] + }, + { + "value": 0.135, + "kerf": 0.010, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 }, + "leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x120", "60x120", "60x144" ] + }, + { + "value": 0.1875, + "kerf": 0.012, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 }, + "leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x120", "60x120", "60x144" ] + }, + { + "value": 0.250, + "kerf": 0.012, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 }, + "leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x120", "60x120", "60x144" ] + }, + { + "value": 0.375, + "kerf": 0.016, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.375, "angle": 90.0, "radius": 0.1875 }, + "leadOut": { "type": "Line", "length": 0.1875, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.625, "overtravel": 0.3125, "minSegmentLength": 1.25, "direction": "AwayFromOrigin" }, + "plateSizes": [ "60x120", "60x144", "72x120" ] + }, + { + "value": 0.500, + "kerf": 0.020, + "assistGas": "O2", + "leadIn": { "type": "Arc", "length": 0.5, "angle": 90.0, "radius": 0.25 }, + "leadOut": { "type": "Line", "length": 0.25, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.75, "overtravel": 0.375, "minSegmentLength": 1.5, "direction": "AwayFromOrigin" }, + "plateSizes": [ "60x120", "60x144", "72x120" ] + } + ] + }, + { + "name": "Stainless Steel", + "grade": "304", + "density": 0.289, + "thicknesses": [ + { + "value": 0.060, + "kerf": 0.008, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 }, + "leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + }, + { + "value": 0.075, + "kerf": 0.008, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 }, + "leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + }, + { + "value": 0.105, + "kerf": 0.010, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 }, + "leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + }, + { + "value": 0.250, + "kerf": 0.014, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 }, + "leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + } + ] + }, + { + "name": "Aluminum", + "grade": "5052", + "density": 0.097, + "thicknesses": [ + { + "value": 0.060, + "kerf": 0.008, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 }, + "leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + }, + { + "value": 0.080, + "kerf": 0.008, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.125, "angle": 90.0, "radius": 0.0625 }, + "leadOut": { "type": "Line", "length": 0.0625, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.25, "overtravel": 0.125, "minSegmentLength": 0.5, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + }, + { + "value": 0.125, + "kerf": 0.010, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.1875, "angle": 90.0, "radius": 0.09375 }, + "leadOut": { "type": "Line", "length": 0.09375, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.375, "overtravel": 0.1875, "minSegmentLength": 0.75, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + }, + { + "value": 0.250, + "kerf": 0.014, + "assistGas": "N2", + "leadIn": { "type": "Arc", "length": 0.25, "angle": 90.0, "radius": 0.125 }, + "leadOut": { "type": "Line", "length": 0.125, "angle": 90.0, "radius": 0.0 }, + "cutOff": { "partClearance": 0.5, "overtravel": 0.25, "minSegmentLength": 1.0, "direction": "AwayFromOrigin" }, + "plateSizes": [ "48x96", "48x120", "60x120" ] + } + ] + } + ] +} diff --git a/OpenNest.Data/LocalJsonProvider.cs b/OpenNest.Data/LocalJsonProvider.cs index b618bd7..2f2432a 100644 --- a/OpenNest.Data/LocalJsonProvider.cs +++ b/OpenNest.Data/LocalJsonProvider.cs @@ -71,6 +71,29 @@ public class LocalJsonProvider : IDataProvider } } + public void EnsureDefaults() + { + if (Directory.GetFiles(_directory, "*.json").Length > 0) + return; + + var assembly = typeof(LocalJsonProvider).Assembly; + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("CL-980.json")); + + if (resourceName is null) return; + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream is null) return; + + using var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + var config = JsonSerializer.Deserialize(json, JsonOptions); + if (config is null) return; + + SaveMachine(config); + } + private static void WriteWithRetry(string path, string json, int maxRetries = 3) { for (var attempt = 0; attempt < maxRetries; attempt++) diff --git a/OpenNest.Data/OpenNest.Data.csproj b/OpenNest.Data/OpenNest.Data.csproj index 89538a0..9103a22 100644 --- a/OpenNest.Data/OpenNest.Data.csproj +++ b/OpenNest.Data/OpenNest.Data.csproj @@ -9,4 +9,7 @@ + + + diff --git a/OpenNest.Tests/Data/DefaultConfigTests.cs b/OpenNest.Tests/Data/DefaultConfigTests.cs new file mode 100644 index 0000000..954c7b1 --- /dev/null +++ b/OpenNest.Tests/Data/DefaultConfigTests.cs @@ -0,0 +1,75 @@ +using OpenNest.Data; + +namespace OpenNest.Tests.Data; + +public class DefaultConfigTests : IDisposable +{ + private readonly string _testDir; + + public DefaultConfigTests() + { + _testDir = Path.Combine(Path.GetTempPath(), "OpenNestTests", Guid.NewGuid().ToString()); + } + + public void Dispose() + { + if (Directory.Exists(_testDir)) + Directory.Delete(_testDir, true); + } + + [Fact] + public void EnsureDefaults_EmptyDirectory_CopiesDefaultConfig() + { + var provider = new LocalJsonProvider(_testDir); + provider.EnsureDefaults(); + var machines = provider.GetMachines(); + Assert.Single(machines); + Assert.Equal("CL-980", machines[0].Name); + } + + [Fact] + public void EnsureDefaults_ExistingFiles_DoesNotCopy() + { + var provider = new LocalJsonProvider(_testDir); + var existing = new MachineConfig { Name = "My Machine" }; + provider.SaveMachine(existing); + + provider.EnsureDefaults(); + + var machines = provider.GetMachines(); + Assert.Single(machines); + Assert.Equal("My Machine", machines[0].Name); + } + + [Fact] + public void DefaultConfig_HasValidStructure() + { + var provider = new LocalJsonProvider(_testDir); + provider.EnsureDefaults(); + + var machines = provider.GetMachines(); + var config = provider.GetMachine(machines[0].Id); + + Assert.NotNull(config); + Assert.Equal(1, config.SchemaVersion); + Assert.Equal(MachineType.Laser, config.Type); + Assert.Equal(UnitSystem.Inches, config.Units); + Assert.NotEmpty(config.Materials); + + foreach (var material in config.Materials) + { + Assert.False(string.IsNullOrWhiteSpace(material.Name)); + Assert.NotEmpty(material.Thicknesses); + + foreach (var thickness in material.Thicknesses) + { + Assert.True(thickness.Value > 0); + Assert.True(thickness.Kerf > 0); + Assert.False(string.IsNullOrWhiteSpace(thickness.AssistGas)); + Assert.NotNull(thickness.LeadIn); + Assert.NotNull(thickness.LeadOut); + Assert.NotNull(thickness.CutOff); + } + } + } +}