feat: add OpenNest.Data machine configuration system
This commit is contained in:
9
OpenNest.Data/CutOffConfig.cs
Normal file
9
OpenNest.Data/CutOffConfig.cs
Normal file
@@ -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";
|
||||
}
|
||||
174
OpenNest.Data/Defaults/CL-980.json
Normal file
174
OpenNest.Data/Defaults/CL-980.json
Normal file
@@ -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" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
9
OpenNest.Data/IDataProvider.cs
Normal file
9
OpenNest.Data/IDataProvider.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
public interface IDataProvider
|
||||
{
|
||||
IReadOnlyList<MachineSummary> GetMachines();
|
||||
MachineConfig? GetMachine(Guid id);
|
||||
void SaveMachine(MachineConfig machine);
|
||||
void DeleteMachine(Guid id);
|
||||
}
|
||||
9
OpenNest.Data/LeadConfig.cs
Normal file
9
OpenNest.Data/LeadConfig.cs
Normal file
@@ -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; }
|
||||
}
|
||||
112
OpenNest.Data/LocalJsonProvider.cs
Normal file
112
OpenNest.Data/LocalJsonProvider.cs
Normal file
@@ -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<MachineSummary> GetMachines()
|
||||
{
|
||||
var summaries = new List<MachineSummary>();
|
||||
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<MachineConfig>(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<MachineConfig>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
OpenNest.Data/MachineConfig.cs
Normal file
26
OpenNest.Data/MachineConfig.cs
Normal file
@@ -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<MaterialConfig> 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));
|
||||
}
|
||||
}
|
||||
3
OpenNest.Data/MachineSummary.cs
Normal file
3
OpenNest.Data/MachineSummary.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
public record MachineSummary(Guid Id, string Name);
|
||||
8
OpenNest.Data/MachineType.cs
Normal file
8
OpenNest.Data/MachineType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
public enum MachineType
|
||||
{
|
||||
Laser,
|
||||
Plasma,
|
||||
Waterjet
|
||||
}
|
||||
9
OpenNest.Data/MaterialConfig.cs
Normal file
9
OpenNest.Data/MaterialConfig.cs
Normal file
@@ -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<ThicknessConfig> Thicknesses { get; set; } = new();
|
||||
}
|
||||
15
OpenNest.Data/OpenNest.Data.csproj
Normal file
15
OpenNest.Data/OpenNest.Data.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>OpenNest.Data</RootNamespace>
|
||||
<AssemblyName>OpenNest.Data</AssemblyName>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Defaults\CL-980.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
14
OpenNest.Data/ThicknessConfig.cs
Normal file
14
OpenNest.Data/ThicknessConfig.cs
Normal file
@@ -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<string> PlateSizes { get; set; } = new();
|
||||
}
|
||||
7
OpenNest.Data/UnitSystem.cs
Normal file
7
OpenNest.Data/UnitSystem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
public enum UnitSystem
|
||||
{
|
||||
Inches,
|
||||
Millimeters
|
||||
}
|
||||
75
OpenNest.Tests/Data/DefaultConfigTests.cs
Normal file
75
OpenNest.Tests/Data/DefaultConfigTests.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
OpenNest.Tests/Data/LocalJsonProviderTests.cs
Normal file
163
OpenNest.Tests/Data/LocalJsonProviderTests.cs
Normal file
@@ -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<MaterialConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Mild Steel",
|
||||
Grade = "A36",
|
||||
Density = 0.2836,
|
||||
Thicknesses = new List<ThicknessConfig>
|
||||
{
|
||||
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<string> { "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<string> { "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);
|
||||
}
|
||||
}
|
||||
131
OpenNest.Tests/Data/MachineConfigTests.cs
Normal file
131
OpenNest.Tests/Data/MachineConfigTests.cs
Normal file
@@ -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<MaterialConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Mild Steel",
|
||||
Grade = "A36",
|
||||
Density = 0.2836,
|
||||
Thicknesses = new List<ThicknessConfig>
|
||||
{
|
||||
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<string> { "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<string> { "60x120" }
|
||||
}
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Stainless Steel",
|
||||
Grade = "304",
|
||||
Density = 0.289,
|
||||
Thicknesses = new List<ThicknessConfig>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Data\OpenNest.Data.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
|
||||
14
OpenNest.sln
14
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
|
||||
|
||||
419
OpenNest/Forms/MachineConfigForm.cs
Normal file
419
OpenNest/Forms/MachineConfigForm.cs
Normal file
@@ -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<string> setter)
|
||||
{
|
||||
var textBox = new TextBox { Text = value };
|
||||
textBox.TextChanged += (s, e) => setter(textBox.Text);
|
||||
return textBox;
|
||||
}
|
||||
|
||||
private static NumericUpDown CreateNumericBox(double value, Action<double> 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>(T currentValue, Action<T> setter) where T : struct, Enum
|
||||
{
|
||||
var combo = new ComboBox
|
||||
{
|
||||
DropDownStyle = ComboBoxStyle.DropDownList
|
||||
};
|
||||
combo.Items.AddRange(Enum.GetNames<T>().Cast<object>().ToArray());
|
||||
combo.SelectedItem = currentValue.ToString();
|
||||
combo.SelectedIndexChanged += (s, e) =>
|
||||
{
|
||||
if (Enum.TryParse<T>(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<MachineConfig>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
OpenNest/Forms/MainForm.Designer.cs
generated
13
OpenNest/Forms/MainForm.Designer.cs
generated
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Data\OpenNest.Data.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user