diff --git a/OpenNest.Data/CutOffConfig.cs b/OpenNest.Data/CutOffConfig.cs new file mode 100644 index 0000000..2cc6060 --- /dev/null +++ b/OpenNest.Data/CutOffConfig.cs @@ -0,0 +1,9 @@ +namespace OpenNest.Data; + +public class CutOffConfig +{ + public double PartClearance { get; set; } = 0.02; + public double Overtravel { get; set; } + public double MinSegmentLength { get; set; } = 0.05; + public string Direction { get; set; } = "AwayFromOrigin"; +} 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/IDataProvider.cs b/OpenNest.Data/IDataProvider.cs new file mode 100644 index 0000000..f499848 --- /dev/null +++ b/OpenNest.Data/IDataProvider.cs @@ -0,0 +1,9 @@ +namespace OpenNest.Data; + +public interface IDataProvider +{ + IReadOnlyList GetMachines(); + MachineConfig? GetMachine(Guid id); + void SaveMachine(MachineConfig machine); + void DeleteMachine(Guid id); +} diff --git a/OpenNest.Data/LeadConfig.cs b/OpenNest.Data/LeadConfig.cs new file mode 100644 index 0000000..6e02411 --- /dev/null +++ b/OpenNest.Data/LeadConfig.cs @@ -0,0 +1,9 @@ +namespace OpenNest.Data; + +public class LeadConfig +{ + public string Type { get; set; } = "Line"; + public double Length { get; set; } + public double Angle { get; set; } = 90.0; + public double Radius { get; set; } +} diff --git a/OpenNest.Data/LocalJsonProvider.cs b/OpenNest.Data/LocalJsonProvider.cs new file mode 100644 index 0000000..2f2432a --- /dev/null +++ b/OpenNest.Data/LocalJsonProvider.cs @@ -0,0 +1,112 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenNest.Data; + +public class LocalJsonProvider : IDataProvider +{ + private readonly string _directory; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + + public LocalJsonProvider(string directory) + { + _directory = directory; + Directory.CreateDirectory(_directory); + } + + public IReadOnlyList GetMachines() + { + var summaries = new List(); + foreach (var file in Directory.GetFiles(_directory, "*.json")) + { + var machine = ReadFile(file); + if (machine is not null) + summaries.Add(new MachineSummary(machine.Id, machine.Name)); + } + return summaries; + } + + public MachineConfig? GetMachine(Guid id) + { + var path = GetPath(id); + return File.Exists(path) ? ReadFile(path) : null; + } + + public void SaveMachine(MachineConfig machine) + { + var json = JsonSerializer.Serialize(machine, JsonOptions); + var path = GetPath(machine.Id); + WriteWithRetry(path, json); + } + + public void DeleteMachine(Guid id) + { + var path = GetPath(id); + if (File.Exists(path)) + File.Delete(path); + } + + private string GetPath(Guid id) => Path.Combine(_directory, $"{id}.json"); + + private static MachineConfig? ReadFile(string path) + { + try + { + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json, JsonOptions); + } + catch (JsonException) + { + return null; + } + catch (IOException) + { + return null; + } + } + + 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++) + { + try + { + File.WriteAllText(path, json); + return; + } + catch (IOException) when (attempt < maxRetries - 1) + { + Thread.Sleep(100); + } + } + } +} diff --git a/OpenNest.Data/MachineConfig.cs b/OpenNest.Data/MachineConfig.cs new file mode 100644 index 0000000..77abe8a --- /dev/null +++ b/OpenNest.Data/MachineConfig.cs @@ -0,0 +1,26 @@ +using OpenNest.Math; + +namespace OpenNest.Data; + +public class MachineConfig +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public int SchemaVersion { get; set; } = 1; + public string Name { get; set; } = ""; + public MachineType Type { get; set; } = MachineType.Laser; + public UnitSystem Units { get; set; } = UnitSystem.Inches; + public List Materials { get; set; } = new(); + + public ThicknessConfig? GetParameters(string material, double thickness) + { + var mat = GetMaterial(material); + if (mat is null) return null; + return mat.Thicknesses.FirstOrDefault(t => t.Value.IsEqualTo(thickness)); + } + + public MaterialConfig? GetMaterial(string name) + { + return Materials.FirstOrDefault(m => + string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/OpenNest.Data/MachineSummary.cs b/OpenNest.Data/MachineSummary.cs new file mode 100644 index 0000000..f980bb0 --- /dev/null +++ b/OpenNest.Data/MachineSummary.cs @@ -0,0 +1,3 @@ +namespace OpenNest.Data; + +public record MachineSummary(Guid Id, string Name); diff --git a/OpenNest.Data/MachineType.cs b/OpenNest.Data/MachineType.cs new file mode 100644 index 0000000..c59a8c5 --- /dev/null +++ b/OpenNest.Data/MachineType.cs @@ -0,0 +1,8 @@ +namespace OpenNest.Data; + +public enum MachineType +{ + Laser, + Plasma, + Waterjet +} diff --git a/OpenNest.Data/MaterialConfig.cs b/OpenNest.Data/MaterialConfig.cs new file mode 100644 index 0000000..cd0d960 --- /dev/null +++ b/OpenNest.Data/MaterialConfig.cs @@ -0,0 +1,9 @@ +namespace OpenNest.Data; + +public class MaterialConfig +{ + public string Name { get; set; } = ""; + public string Grade { get; set; } = ""; + public double Density { get; set; } + public List Thicknesses { get; set; } = new(); +} diff --git a/OpenNest.Data/OpenNest.Data.csproj b/OpenNest.Data/OpenNest.Data.csproj new file mode 100644 index 0000000..9103a22 --- /dev/null +++ b/OpenNest.Data/OpenNest.Data.csproj @@ -0,0 +1,15 @@ + + + net8.0-windows + OpenNest.Data + OpenNest.Data + enable + enable + + + + + + + + diff --git a/OpenNest.Data/ThicknessConfig.cs b/OpenNest.Data/ThicknessConfig.cs new file mode 100644 index 0000000..715cb2b --- /dev/null +++ b/OpenNest.Data/ThicknessConfig.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace OpenNest.Data; + +public class ThicknessConfig +{ + public double Value { get; set; } + public double Kerf { get; set; } + public string AssistGas { get; set; } = ""; + public LeadConfig LeadIn { get; set; } = new(); + public LeadConfig LeadOut { get; set; } = new(); + public CutOffConfig CutOff { get; set; } = new(); + public List PlateSizes { get; set; } = new(); +} diff --git a/OpenNest.Data/UnitSystem.cs b/OpenNest.Data/UnitSystem.cs new file mode 100644 index 0000000..c4b500f --- /dev/null +++ b/OpenNest.Data/UnitSystem.cs @@ -0,0 +1,7 @@ +namespace OpenNest.Data; + +public enum UnitSystem +{ + Inches, + Millimeters +} 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); + } + } + } +} diff --git a/OpenNest.Tests/Data/LocalJsonProviderTests.cs b/OpenNest.Tests/Data/LocalJsonProviderTests.cs new file mode 100644 index 0000000..9225147 --- /dev/null +++ b/OpenNest.Tests/Data/LocalJsonProviderTests.cs @@ -0,0 +1,163 @@ +using OpenNest.Data; + +namespace OpenNest.Tests.Data; + +public class LocalJsonProviderTests : IDisposable +{ + private readonly string _testDir; + + public LocalJsonProviderTests() + { + _testDir = Path.Combine(Path.GetTempPath(), "OpenNestTests", Guid.NewGuid().ToString()); + Directory.CreateDirectory(_testDir); + } + + public void Dispose() + { + if (Directory.Exists(_testDir)) + Directory.Delete(_testDir, true); + } + + private LocalJsonProvider CreateProvider() => new(_testDir); + + [Fact] + public void GetMachines_EmptyDirectory_ReturnsEmpty() + { + var provider = CreateProvider(); + var result = provider.GetMachines(); + Assert.Empty(result); + } + + [Fact] + public void SaveMachine_ThenGetMachine_RoundTrips() + { + var provider = CreateProvider(); + var machine = new MachineConfig + { + Name = "Test Laser", + Type = MachineType.Laser, + Units = UnitSystem.Inches, + Materials = new List + { + new() + { + Name = "Mild Steel", + Grade = "A36", + Density = 0.2836, + Thicknesses = new List + { + new() + { + Value = 0.250, + Kerf = 0.012, + AssistGas = "O2", + LeadIn = new LeadConfig { Type = "Arc", Length = 0.25, Angle = 90.0, Radius = 0.125 }, + LeadOut = new LeadConfig { Type = "Line", Length = 0.125 }, + CutOff = new CutOffConfig + { + PartClearance = 0.5, + Overtravel = 0.25, + Direction = "AwayFromOrigin", + MinSegmentLength = 1.0 + }, + PlateSizes = new List { "60x120", "48x96" } + } + } + } + } + }; + + provider.SaveMachine(machine); + var loaded = provider.GetMachine(machine.Id); + + Assert.NotNull(loaded); + Assert.Equal("Test Laser", loaded.Name); + Assert.Equal(MachineType.Laser, loaded.Type); + Assert.Equal(UnitSystem.Inches, loaded.Units); + Assert.Single(loaded.Materials); + + var mat = loaded.Materials[0]; + Assert.Equal("Mild Steel", mat.Name); + Assert.Equal("A36", mat.Grade); + Assert.Equal(0.2836, mat.Density); + Assert.Single(mat.Thicknesses); + + var thick = mat.Thicknesses[0]; + Assert.Equal(0.250, thick.Value); + Assert.Equal(0.012, thick.Kerf); + Assert.Equal("O2", thick.AssistGas); + Assert.Equal("Arc", thick.LeadIn.Type); + Assert.Equal(0.25, thick.LeadIn.Length); + Assert.Equal("Line", thick.LeadOut.Type); + Assert.Equal(0.125, thick.LeadOut.Length); + Assert.Equal(0.5, thick.CutOff.PartClearance); + Assert.Equal("AwayFromOrigin", thick.CutOff.Direction); + Assert.Equal(new List { "60x120", "48x96" }, thick.PlateSizes); + } + + [Fact] + public void GetMachines_ReturnsSummaries() + { + var provider = CreateProvider(); + var m1 = new MachineConfig { Name = "Laser A" }; + var m2 = new MachineConfig { Name = "Plasma B" }; + + provider.SaveMachine(m1); + provider.SaveMachine(m2); + var summaries = provider.GetMachines(); + + Assert.Equal(2, summaries.Count); + Assert.Contains(summaries, s => s.Name == "Laser A" && s.Id == m1.Id); + Assert.Contains(summaries, s => s.Name == "Plasma B" && s.Id == m2.Id); + } + + [Fact] + public void GetMachine_NotFound_ReturnsNull() + { + var provider = CreateProvider(); + var result = provider.GetMachine(Guid.NewGuid()); + Assert.Null(result); + } + + [Fact] + public void DeleteMachine_RemovesFile() + { + var provider = CreateProvider(); + var machine = new MachineConfig { Name = "To Delete" }; + provider.SaveMachine(machine); + + provider.DeleteMachine(machine.Id); + + Assert.Null(provider.GetMachine(machine.Id)); + Assert.Empty(provider.GetMachines()); + } + + [Fact] + public void SaveMachine_OverwritesExisting() + { + var provider = CreateProvider(); + var machine = new MachineConfig { Name = "Original" }; + provider.SaveMachine(machine); + + machine.Name = "Updated"; + provider.SaveMachine(machine); + + var loaded = provider.GetMachine(machine.Id); + Assert.NotNull(loaded); + Assert.Equal("Updated", loaded.Name); + Assert.Single(provider.GetMachines()); + } + + [Fact] + public void SaveMachine_PreservesSchemaVersion() + { + var provider = CreateProvider(); + var machine = new MachineConfig { Name = "Versioned", SchemaVersion = 1 }; + + provider.SaveMachine(machine); + var loaded = provider.GetMachine(machine.Id); + + Assert.NotNull(loaded); + Assert.Equal(1, loaded.SchemaVersion); + } +} diff --git a/OpenNest.Tests/Data/MachineConfigTests.cs b/OpenNest.Tests/Data/MachineConfigTests.cs new file mode 100644 index 0000000..58194bd --- /dev/null +++ b/OpenNest.Tests/Data/MachineConfigTests.cs @@ -0,0 +1,131 @@ +using OpenNest.Data; + +namespace OpenNest.Tests.Data; + +public class MachineConfigTests +{ + private static MachineConfig CreateTestMachine() + { + return new MachineConfig + { + Id = Guid.NewGuid(), + Name = "Test Laser", + Type = MachineType.Laser, + Units = UnitSystem.Inches, + Materials = new List + { + new() + { + Name = "Mild Steel", + Grade = "A36", + Density = 0.2836, + Thicknesses = new List + { + new() + { + Value = 0.250, + Kerf = 0.012, + AssistGas = "O2", + LeadIn = new LeadConfig { Type = "Arc", Radius = 0.25 }, + LeadOut = new LeadConfig { Type = "Line", Length = 0.125 }, + PlateSizes = new List { "60x120", "48x96" } + }, + new() + { + Value = 0.500, + Kerf = 0.020, + AssistGas = "O2", + LeadIn = new LeadConfig { Type = "Arc", Radius = 0.375 }, + LeadOut = new LeadConfig { Type = "Line", Length = 0.25 }, + PlateSizes = new List { "60x120" } + } + } + }, + new() + { + Name = "Stainless Steel", + Grade = "304", + Density = 0.289, + Thicknesses = new List + { + new() + { + Value = 0.250, + Kerf = 0.014, + AssistGas = "N2" + } + } + } + } + }; + } + + [Fact] + public void GetParameters_ExactMatch_ReturnsThickness() + { + var machine = CreateTestMachine(); + var result = machine.GetParameters("Mild Steel", 0.250); + Assert.NotNull(result); + Assert.Equal(0.012, result.Kerf); + Assert.Equal("O2", result.AssistGas); + } + + [Fact] + public void GetParameters_WithinTolerance_ReturnsThickness() + { + var machine = CreateTestMachine(); + var result = machine.GetParameters("Mild Steel", 0.250001); + Assert.NotNull(result); + Assert.Equal(0.012, result.Kerf); + } + + [Fact] + public void GetParameters_NoMatch_ReturnsNull() + { + var machine = CreateTestMachine(); + var result = machine.GetParameters("Mild Steel", 0.375); + Assert.Null(result); + } + + [Fact] + public void GetParameters_CaseInsensitiveMaterial() + { + var machine = CreateTestMachine(); + var result = machine.GetParameters("mild steel", 0.250); + Assert.NotNull(result); + Assert.Equal(0.012, result.Kerf); + } + + [Fact] + public void GetParameters_UnknownMaterial_ReturnsNull() + { + var machine = CreateTestMachine(); + var result = machine.GetParameters("Titanium", 0.250); + Assert.Null(result); + } + + [Fact] + public void GetMaterial_ReturnsMaterialByName() + { + var machine = CreateTestMachine(); + var result = machine.GetMaterial("Stainless Steel"); + Assert.NotNull(result); + Assert.Equal("304", result.Grade); + } + + [Fact] + public void GetMaterial_CaseInsensitive() + { + var machine = CreateTestMachine(); + var result = machine.GetMaterial("stainless steel"); + Assert.NotNull(result); + } + + [Fact] + public void GetMaterial_NotFound_ReturnsNull() + { + var machine = CreateTestMachine(); + var result = machine.GetMaterial("Titanium"); + Assert.Null(result); + } +} diff --git a/OpenNest.Tests/OpenNest.Tests.csproj b/OpenNest.Tests/OpenNest.Tests.csproj index 6ffd66d..4b2f7fa 100644 --- a/OpenNest.Tests/OpenNest.Tests.csproj +++ b/OpenNest.Tests/OpenNest.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/OpenNest.sln b/OpenNest.sln index 3c0540f..20c64be 100644 --- a/OpenNest.sln +++ b/OpenNest.sln @@ -30,6 +30,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostProcessors", "PostProce EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Posts.Cincinnati", "OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj", "{FB1B2EB2-9D80-4499-BA93-B4E2F295A532}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Data", "OpenNest.Data\OpenNest.Data.csproj", "{A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -172,6 +174,18 @@ Global {FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x64.Build.0 = Release|Any CPU {FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.ActiveCfg = Release|Any CPU {FB1B2EB2-9D80-4499-BA93-B4E2F295A532}.Release|x86.Build.0 = Release|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x64.Build.0 = Debug|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Debug|x86.Build.0 = Debug|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|Any CPU.Build.0 = Release|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x64.ActiveCfg = Release|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x64.Build.0 = Release|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.ActiveCfg = Release|Any CPU + {A0B4B48E-1DF0-4DD3-B42C-B9B7779EA8B0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/OpenNest/Forms/MachineConfigForm.cs b/OpenNest/Forms/MachineConfigForm.cs new file mode 100644 index 0000000..02b1b4a --- /dev/null +++ b/OpenNest/Forms/MachineConfigForm.cs @@ -0,0 +1,419 @@ +using OpenNest.Data; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Windows.Forms; + +namespace OpenNest.Forms +{ + public class MachineConfigForm : Form + { + private readonly IDataProvider _provider; + private readonly TreeView _tree; + private readonly Panel _detailPanel; + + private MachineConfig _currentMachine; + + public MachineConfigForm(IDataProvider provider) + { + _provider = provider; + + Text = "Machine Configuration"; + Size = new Size(900, 600); + StartPosition = FormStartPosition.CenterParent; + MinimumSize = new Size(700, 400); + + var splitContainer = new SplitContainer + { + Dock = DockStyle.Fill, + SplitterDistance = 250, + FixedPanel = FixedPanel.Panel1 + }; + + _tree = new TreeView + { + Dock = DockStyle.Fill, + HideSelection = false + }; + _tree.AfterSelect += Tree_AfterSelect; + + var treeButtonPanel = new FlowLayoutPanel + { + Dock = DockStyle.Bottom, + AutoSize = true, + FlowDirection = FlowDirection.LeftToRight, + WrapContents = true, + Padding = new Padding(2) + }; + + var addMachineButton = new Button { Text = "+ Machine", AutoSize = true }; + addMachineButton.Click += AddMachine_Click; + var removeMachineButton = new Button { Text = "- Machine", AutoSize = true }; + removeMachineButton.Click += RemoveMachine_Click; + var addMaterialButton = new Button { Text = "+ Material", AutoSize = true }; + addMaterialButton.Click += AddMaterial_Click; + var removeMaterialButton = new Button { Text = "- Material", AutoSize = true }; + removeMaterialButton.Click += RemoveMaterial_Click; + var addThicknessButton = new Button { Text = "+ Thickness", AutoSize = true }; + addThicknessButton.Click += AddThickness_Click; + var removeThicknessButton = new Button { Text = "- Thickness", AutoSize = true }; + removeThicknessButton.Click += RemoveThickness_Click; + + treeButtonPanel.Controls.AddRange(new Control[] + { + addMachineButton, removeMachineButton, + addMaterialButton, removeMaterialButton, + addThicknessButton, removeThicknessButton + }); + + splitContainer.Panel1.Controls.Add(_tree); + splitContainer.Panel1.Controls.Add(treeButtonPanel); + + _detailPanel = new Panel { Dock = DockStyle.Fill, AutoScroll = true }; + splitContainer.Panel2.Controls.Add(_detailPanel); + + var bottomPanel = new FlowLayoutPanel + { + Dock = DockStyle.Bottom, + AutoSize = true, + FlowDirection = FlowDirection.RightToLeft, + Padding = new Padding(4) + }; + + var saveButton = new Button { Text = "Save", AutoSize = true }; + saveButton.Click += Save_Click; + var importButton = new Button { Text = "Import...", AutoSize = true }; + importButton.Click += Import_Click; + var exportButton = new Button { Text = "Export...", AutoSize = true }; + exportButton.Click += Export_Click; + + bottomPanel.Controls.AddRange(new Control[] { saveButton, exportButton, importButton }); + + Controls.Add(splitContainer); + Controls.Add(bottomPanel); + + LoadTree(); + } + + private void LoadTree() + { + _tree.Nodes.Clear(); + foreach (var summary in _provider.GetMachines()) + { + var machine = _provider.GetMachine(summary.Id); + if (machine is null) continue; + + var machineNode = new TreeNode(machine.Name) { Tag = machine }; + foreach (var material in machine.Materials) + { + var matNode = new TreeNode(material.Name) { Tag = material }; + foreach (var thickness in material.Thicknesses) + { + var thickNode = new TreeNode(thickness.Value.ToString("0.####")) { Tag = thickness }; + matNode.Nodes.Add(thickNode); + } + machineNode.Nodes.Add(matNode); + } + _tree.Nodes.Add(machineNode); + } + + if (_tree.Nodes.Count > 0) + _tree.SelectedNode = _tree.Nodes[0]; + } + + private void Tree_AfterSelect(object sender, TreeViewEventArgs e) + { + _detailPanel.Controls.Clear(); + if (e.Node?.Tag is null) return; + + switch (e.Node.Tag) + { + case MachineConfig machine: + _currentMachine = machine; + ShowMachineDetails(machine); + break; + case MaterialConfig material: + _currentMachine = e.Node.Parent?.Tag as MachineConfig; + ShowMaterialDetails(material); + break; + case ThicknessConfig thickness: + _currentMachine = e.Node.Parent?.Parent?.Tag as MachineConfig; + ShowThicknessDetails(thickness); + break; + } + } + + private void ShowMachineDetails(MachineConfig machine) + { + var layout = CreateDetailLayout(); + var row = 0; + + AddField(layout, ref row, "Name:", CreateTextBox(machine.Name, v => machine.Name = v)); + AddField(layout, ref row, "Type:", CreateEnumCombo(machine.Type, v => machine.Type = v)); + AddField(layout, ref row, "Units:", CreateEnumCombo(machine.Units, v => machine.Units = v)); + + _detailPanel.Controls.Add(layout); + } + + private void ShowMaterialDetails(MaterialConfig material) + { + var layout = CreateDetailLayout(); + var row = 0; + + AddField(layout, ref row, "Name:", CreateTextBox(material.Name, v => material.Name = v)); + AddField(layout, ref row, "Grade:", CreateTextBox(material.Grade, v => material.Grade = v)); + AddField(layout, ref row, "Density:", CreateNumericBox(material.Density, v => material.Density = v, 4)); + + _detailPanel.Controls.Add(layout); + } + + private void ShowThicknessDetails(ThicknessConfig thickness) + { + var layout = CreateDetailLayout(); + var row = 0; + + AddField(layout, ref row, "Thickness:", CreateNumericBox(thickness.Value, v => thickness.Value = v, 4)); + AddField(layout, ref row, "Kerf:", CreateNumericBox(thickness.Kerf, v => thickness.Kerf = v, 4)); + AddField(layout, ref row, "Assist Gas:", CreateTextBox(thickness.AssistGas, v => thickness.AssistGas = v)); + + AddSectionHeader(layout, ref row, "Lead In"); + AddField(layout, ref row, "Type:", CreateTextBox(thickness.LeadIn.Type, v => thickness.LeadIn.Type = v)); + AddField(layout, ref row, "Length:", CreateNumericBox(thickness.LeadIn.Length, v => thickness.LeadIn.Length = v, 4)); + AddField(layout, ref row, "Angle:", CreateNumericBox(thickness.LeadIn.Angle, v => thickness.LeadIn.Angle = v, 1)); + AddField(layout, ref row, "Radius:", CreateNumericBox(thickness.LeadIn.Radius, v => thickness.LeadIn.Radius = v, 4)); + + AddSectionHeader(layout, ref row, "Lead Out"); + AddField(layout, ref row, "Type:", CreateTextBox(thickness.LeadOut.Type, v => thickness.LeadOut.Type = v)); + AddField(layout, ref row, "Length:", CreateNumericBox(thickness.LeadOut.Length, v => thickness.LeadOut.Length = v, 4)); + AddField(layout, ref row, "Angle:", CreateNumericBox(thickness.LeadOut.Angle, v => thickness.LeadOut.Angle = v, 1)); + AddField(layout, ref row, "Radius:", CreateNumericBox(thickness.LeadOut.Radius, v => thickness.LeadOut.Radius = v, 4)); + + AddSectionHeader(layout, ref row, "Cut Off"); + AddField(layout, ref row, "Part Clearance:", CreateNumericBox(thickness.CutOff.PartClearance, v => thickness.CutOff.PartClearance = v, 4)); + AddField(layout, ref row, "Overtravel:", CreateNumericBox(thickness.CutOff.Overtravel, v => thickness.CutOff.Overtravel = v, 4)); + AddField(layout, ref row, "Min Segment:", CreateNumericBox(thickness.CutOff.MinSegmentLength, v => thickness.CutOff.MinSegmentLength = v, 4)); + AddField(layout, ref row, "Direction:", CreateTextBox(thickness.CutOff.Direction, v => thickness.CutOff.Direction = v)); + + AddSectionHeader(layout, ref row, "Plate Sizes"); + var sizesText = string.Join(", ", thickness.PlateSizes); + AddField(layout, ref row, "Sizes:", CreateTextBox(sizesText, v => + { + thickness.PlateSizes = v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); + })); + + _detailPanel.Controls.Add(layout); + } + + private static TableLayoutPanel CreateDetailLayout() + { + var layout = new TableLayoutPanel + { + Dock = DockStyle.Top, + AutoSize = true, + ColumnCount = 2, + Padding = new Padding(8) + }; + layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + return layout; + } + + private static void AddField(TableLayoutPanel layout, ref int row, string label, Control control) + { + layout.RowCount = row + 1; + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + layout.Controls.Add(new Label { Text = label, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 6, 8, 0) }, 0, row); + control.Dock = DockStyle.Fill; + layout.Controls.Add(control, 1, row); + row++; + } + + private static void AddSectionHeader(TableLayoutPanel layout, ref int row, string text) + { + layout.RowCount = row + 1; + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + var label = new Label + { + Text = text, + AutoSize = true, + Font = new Font(Control.DefaultFont, FontStyle.Bold), + Margin = new Padding(0, 12, 0, 4) + }; + layout.SetColumnSpan(label, 2); + layout.Controls.Add(label, 0, row); + row++; + } + + private static TextBox CreateTextBox(string value, Action setter) + { + var textBox = new TextBox { Text = value }; + textBox.TextChanged += (s, e) => setter(textBox.Text); + return textBox; + } + + private static NumericUpDown CreateNumericBox(double value, Action setter, int decimals) + { + var numeric = new NumericUpDown + { + DecimalPlaces = decimals, + Minimum = 0, + Maximum = 10000, + Increment = (decimal)System.Math.Pow(10, -decimals), + Value = (decimal)value + }; + numeric.ValueChanged += (s, e) => setter((double)numeric.Value); + return numeric; + } + + private static ComboBox CreateEnumCombo(T currentValue, Action setter) where T : struct, Enum + { + var combo = new ComboBox + { + DropDownStyle = ComboBoxStyle.DropDownList + }; + combo.Items.AddRange(Enum.GetNames().Cast().ToArray()); + combo.SelectedItem = currentValue.ToString(); + combo.SelectedIndexChanged += (s, e) => + { + if (Enum.TryParse(combo.SelectedItem?.ToString(), out var val)) + setter(val); + }; + return combo; + } + + private void Save_Click(object sender, EventArgs e) + { + foreach (TreeNode machineNode in _tree.Nodes) + { + if (machineNode.Tag is MachineConfig machine) + _provider.SaveMachine(machine); + } + MessageBox.Show("Machine configurations saved.", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + private void AddMachine_Click(object sender, EventArgs e) + { + var machine = new MachineConfig { Name = "New Machine" }; + _provider.SaveMachine(machine); + LoadTree(); + } + + private void RemoveMachine_Click(object sender, EventArgs e) + { + if (_tree.SelectedNode?.Tag is not MachineConfig machine) return; + if (MessageBox.Show($"Delete machine '{machine.Name}'?", "Confirm", MessageBoxButtons.YesNo) != DialogResult.Yes) return; + + _provider.DeleteMachine(machine.Id); + LoadTree(); + } + + private void AddMaterial_Click(object sender, EventArgs e) + { + if (_currentMachine is null) return; + + _currentMachine.Materials.Add(new MaterialConfig { Name = "New Material" }); + _provider.SaveMachine(_currentMachine); + LoadTree(); + } + + private void RemoveMaterial_Click(object sender, EventArgs e) + { + if (_tree.SelectedNode?.Tag is not MaterialConfig material) return; + if (_currentMachine is null) return; + + _currentMachine.Materials.Remove(material); + _provider.SaveMachine(_currentMachine); + LoadTree(); + } + + private void AddThickness_Click(object sender, EventArgs e) + { + var material = _tree.SelectedNode?.Tag as MaterialConfig; + if (material is null && _tree.SelectedNode?.Tag is ThicknessConfig) + material = _tree.SelectedNode.Parent?.Tag as MaterialConfig; + if (material is null || _currentMachine is null) return; + + material.Thicknesses.Add(new ThicknessConfig { Value = 0.250 }); + _provider.SaveMachine(_currentMachine); + LoadTree(); + } + + private void RemoveThickness_Click(object sender, EventArgs e) + { + if (_tree.SelectedNode?.Tag is not ThicknessConfig thickness) return; + var material = _tree.SelectedNode.Parent?.Tag as MaterialConfig; + if (material is null || _currentMachine is null) return; + + material.Thicknesses.Remove(thickness); + _provider.SaveMachine(_currentMachine); + LoadTree(); + } + + private void Import_Click(object sender, EventArgs e) + { + using (var dialog = new OpenFileDialog + { + Filter = "JSON files (*.json)|*.json", + Title = "Import Machine Configuration" + }) + { + if (dialog.ShowDialog() != DialogResult.OK) return; + + try + { + var json = File.ReadAllText(dialog.FileName); + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + var machine = JsonSerializer.Deserialize(json, options); + if (machine is null) return; + + machine.Id = Guid.NewGuid(); + _provider.SaveMachine(machine); + LoadTree(); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to import: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + + private void Export_Click(object sender, EventArgs e) + { + if (_currentMachine is null) return; + + using (var dialog = new SaveFileDialog + { + Filter = "JSON files (*.json)|*.json", + FileName = $"{_currentMachine.Name}.json", + Title = "Export Machine Configuration" + }) + { + if (dialog.ShowDialog() != DialogResult.OK) return; + + try + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + var json = JsonSerializer.Serialize(_currentMachine, options); + File.WriteAllText(dialog.FileName, json); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to export: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } + } +} diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index 4990472..81a8744 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -79,6 +79,7 @@ mnuSetOffsetIncrement = new System.Windows.Forms.ToolStripMenuItem(); mnuSetRotationIncrement = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem15 = new System.Windows.Forms.ToolStripSeparator(); + mnuToolsMachineConfig = new System.Windows.Forms.ToolStripMenuItem(); mnuToolsOptions = new System.Windows.Forms.ToolStripMenuItem(); mnuNest = new System.Windows.Forms.ToolStripMenuItem(); mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem(); @@ -403,7 +404,7 @@ // // mnuTools // - mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuToolsMeasureArea, mnuToolsBestFitViewer, mnuToolsPatternTile, mnuToolsAlign, toolStripMenuItem14, mnuSetOffsetIncrement, mnuSetRotationIncrement, toolStripMenuItem15, mnuToolsOptions }); + mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuToolsMeasureArea, mnuToolsBestFitViewer, mnuToolsPatternTile, mnuToolsAlign, toolStripMenuItem14, mnuSetOffsetIncrement, mnuSetRotationIncrement, toolStripMenuItem15, mnuToolsMachineConfig, mnuToolsOptions }); mnuTools.Name = "mnuTools"; mnuTools.Size = new System.Drawing.Size(47, 20); mnuTools.Text = "&Tools"; @@ -528,8 +529,15 @@ toolStripMenuItem15.Name = "toolStripMenuItem15"; toolStripMenuItem15.Size = new System.Drawing.Size(211, 6); // + // mnuToolsMachineConfig + // + mnuToolsMachineConfig.Name = "mnuToolsMachineConfig"; + mnuToolsMachineConfig.Size = new System.Drawing.Size(214, 22); + mnuToolsMachineConfig.Text = "Machine Configuration..."; + mnuToolsMachineConfig.Click += MachineConfig_Click; + // // mnuToolsOptions - // + // mnuToolsOptions.Name = "mnuToolsOptions"; mnuToolsOptions.Size = new System.Drawing.Size(214, 22); mnuToolsOptions.Text = "Options"; @@ -1154,6 +1162,7 @@ private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5; private System.Windows.Forms.ToolStripMenuItem mnuTools; + private System.Windows.Forms.ToolStripMenuItem mnuToolsMachineConfig; private System.Windows.Forms.ToolStripMenuItem mnuToolsOptions; private System.Windows.Forms.ToolStripMenuItem mnuNest; private System.Windows.Forms.ToolStripMenuItem mnuNestEdit; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 8314ce3..e2e5151 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -1,5 +1,6 @@ using OpenNest.Actions; using OpenNest.Collections; +using OpenNest.Data; using OpenNest.Engine.BestFit; using OpenNest.Engine.Fill; using OpenNest.Geometry; @@ -739,6 +740,17 @@ namespace OpenNest.Forms form.ShowDialog(); } + private void MachineConfig_Click(object sender, EventArgs e) + { + var appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OpenNest", "Machines"); + var provider = new LocalJsonProvider(appDataPath); + provider.EnsureDefaults(); + using (var form = new MachineConfigForm(provider)) + { + form.ShowDialog(this); + } + } + private void AlignLeft_Click(object sender, EventArgs e) { if (activeForm == null) return; diff --git a/OpenNest/OpenNest.csproj b/OpenNest/OpenNest.csproj index 6b4fcd5..3f0613d 100644 --- a/OpenNest/OpenNest.csproj +++ b/OpenNest/OpenNest.csproj @@ -13,6 +13,7 @@ +