mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 08:05:07 -05:00
457 lines
21 KiB
C#
457 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.CutOffs;
|
|
using OpenNest.Data;
|
|
using OpenNest.IO;
|
|
|
|
namespace OpenNest.Desktop;
|
|
|
|
/// <summary>
|
|
/// The two per-user settings the machine entry points read: the legacy default unit, used
|
|
/// when the nest defaults give none, and this PC's last cutting settings (saving one persists it).
|
|
/// </summary>
|
|
public interface IMachineUserSettings
|
|
{
|
|
/// <summary>The DefaultUnit setting.</summary>
|
|
Units DefaultUnit { get; }
|
|
|
|
/// <summary>The CuttingParametersJson setting; setting it saves it.</summary>
|
|
string CuttingParametersJson { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The desktop entry points for the persisted machines (with their clamps) and nest defaults.
|
|
/// In Database mode each comes from the nest server once it holds a shared copy
|
|
/// (<see cref="SharedSettingsSource"/>), otherwise from this PC's files. Nothing here shows UI:
|
|
/// the warning texts it returns are shown by the front end, at most once per session where
|
|
/// noted.
|
|
/// </summary>
|
|
public sealed class MachineSettingsSource
|
|
{
|
|
private readonly IMachineUserSettings user;
|
|
private readonly string machinesDirectory;
|
|
private readonly string nestDefaultsPath;
|
|
private readonly string legacyClampsPath;
|
|
private readonly string legacySupportGridPath;
|
|
private bool defaultsWarned;
|
|
private bool clampMigrationAsked;
|
|
|
|
/// <param name="shared">Where shared copies come from.</param>
|
|
/// <param name="user">The per-user settings this reads and saves.</param>
|
|
/// <param name="machinesDirectory">This PC's machines; defaults to %APPDATA%\OpenNest\Machines.</param>
|
|
/// <param name="nestDefaultsPath">This PC's nest defaults; defaults to <see cref="NestDefaults.DefaultPath"/>.</param>
|
|
/// <param name="legacyClampsPath">The pre-machine clamps.json; defaults to <see cref="ClampSettings.DefaultPath"/>.</param>
|
|
/// <param name="legacySupportGridPath">The pre-machine support-grid.json; defaults to <see cref="SupportGridSettings.DefaultPath"/>.</param>
|
|
public MachineSettingsSource(
|
|
SharedSettingsSource shared,
|
|
IMachineUserSettings user,
|
|
string machinesDirectory = null,
|
|
string nestDefaultsPath = null,
|
|
string legacyClampsPath = null,
|
|
string legacySupportGridPath = null)
|
|
{
|
|
Shared = shared ?? throw new ArgumentNullException(nameof(shared));
|
|
this.user = user ?? throw new ArgumentNullException(nameof(user));
|
|
this.machinesDirectory = machinesDirectory ?? DefaultMachinesDirectory;
|
|
this.nestDefaultsPath = nestDefaultsPath ?? NestDefaults.DefaultPath;
|
|
this.legacyClampsPath = legacyClampsPath ?? ClampSettings.DefaultPath;
|
|
this.legacySupportGridPath = legacySupportGridPath ?? SupportGridSettings.DefaultPath;
|
|
}
|
|
|
|
/// <summary>%APPDATA%\OpenNest\Machines.</summary>
|
|
public static string DefaultMachinesDirectory => Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"OpenNest",
|
|
"Machines");
|
|
|
|
/// <summary>Where shared copies come from.</summary>
|
|
public SharedSettingsSource Shared { get; }
|
|
|
|
/// <summary>This PC's nest defaults file.</summary>
|
|
public string NestDefaultsPath => nestDefaultsPath;
|
|
|
|
/// <summary>
|
|
/// The machine configurations edited in Tools > Machine Configuration: the server's when
|
|
/// it holds any, otherwise this PC's. <paramref name="refresh"/> re-reads the server first.
|
|
/// </summary>
|
|
public IDataProvider CreateMachineProvider(bool refresh = false) =>
|
|
Shared.Holding(SharedSettingKinds.Machine, refresh) is { } shared
|
|
? new SharedMachineProvider(shared)
|
|
: CreateLocalMachineProvider();
|
|
|
|
/// <summary>This PC's machine configurations, with the built-in ones written on first use.</summary>
|
|
public LocalJsonProvider CreateLocalMachineProvider()
|
|
{
|
|
var provider = new LocalJsonProvider(machinesDirectory);
|
|
provider.EnsureDefaults();
|
|
return provider;
|
|
}
|
|
|
|
/// <summary>Every readable machine configuration, for choosing acceleration limits.</summary>
|
|
public List<MachineConfig> LoadMachines() => ReadMachines(() => CreateMachineProvider());
|
|
|
|
/// <summary>Every readable machine configuration on this PC, shared or not, for uploading.</summary>
|
|
public List<MachineConfig> LoadLocalMachines() => ReadMachines(CreateLocalMachineProvider);
|
|
|
|
private static List<MachineConfig> ReadMachines(Func<IDataProvider> createProvider)
|
|
{
|
|
var machines = new List<MachineConfig>();
|
|
try
|
|
{
|
|
var provider = createProvider();
|
|
foreach (var summary in provider.GetMachines())
|
|
{
|
|
if (provider.GetMachine(summary.Id) is { } machine)
|
|
machines.Add(machine);
|
|
}
|
|
}
|
|
catch (IOException) { }
|
|
catch (UnauthorizedAccessException) { }
|
|
return machines;
|
|
}
|
|
|
|
/// <summary>Every material and grade on file across the readable machine configurations.</summary>
|
|
public IEnumerable<MaterialConfig> LoadMaterials() =>
|
|
LoadMachines().SelectMany(machine => machine.Materials);
|
|
|
|
/// <summary>
|
|
/// The clamps new nests assigned <paramref name="machine"/> receive (New, BOM import, Load
|
|
/// Machine Defaults); off when there is no machine or it has no clamps.
|
|
/// </summary>
|
|
public static ClampSettings LoadClampSettings(MachineConfig machine) =>
|
|
machine?.GetClampSettings() ?? ClampSettings.Fallback;
|
|
|
|
/// <summary>Saves <paramref name="settings"/> as the clamps of the machine with <paramref name="machineId"/>.</summary>
|
|
public void SaveClampSettings(Guid machineId, ClampSettings settings)
|
|
{
|
|
var provider = CreateMachineProvider();
|
|
var machine = provider.GetMachine(machineId)
|
|
?? throw new IOException("The machine configuration is no longer on file.");
|
|
machine.Clamps = MachineClamps.FromSettings(settings, machine.Units);
|
|
provider.SaveMachine(machine);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the legacy clamps.json (or the server's shared copy) onto a machine once, while
|
|
/// no machine has clamps of its own: the nest defaults' machine, or the only machine. With
|
|
/// several machines and no default <paramref name="askMachine"/> picks one (null for not
|
|
/// now), asked once per session. Returns what to tell the user when the copy failed, or null.
|
|
/// </summary>
|
|
public string MigrateLegacyClamps(Func<IReadOnlyList<MachineConfig>, MachineConfig> askMachine)
|
|
{
|
|
try
|
|
{
|
|
var legacy = LoadLegacyClampSettings();
|
|
var provider = CreateMachineProvider();
|
|
var machines = ReadMachines(() => provider)
|
|
.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase)
|
|
.ToList();
|
|
if (!LegacyClampMigration.IsNeeded(legacy, machines))
|
|
return null;
|
|
|
|
var target = LegacyClampMigration.Target(machines, LoadSavedNestDefaults(out _).MachineId);
|
|
if (target == null)
|
|
{
|
|
if (clampMigrationAsked)
|
|
return null;
|
|
clampMigrationAsked = true;
|
|
target = askMachine?.Invoke(machines);
|
|
if (target == null)
|
|
return null;
|
|
}
|
|
|
|
LegacyClampMigration.Apply(legacy, target);
|
|
provider.SaveMachine(target);
|
|
return null;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
return $"The clamp settings could not be copied to a machine:\n\n{ex.Message}\n\n"
|
|
+ "New nests get no clamps until a machine has them (Tools > Machine Configuration).";
|
|
}
|
|
}
|
|
|
|
// The pre-machine clamp settings: the server's shared copy once it holds one, otherwise
|
|
// this PC's clamps.json; null when neither exists.
|
|
private ClampSettings LoadLegacyClampSettings()
|
|
{
|
|
if (Shared.Find(SharedSettingKinds.Clamps, SharedSettingKinds.DefaultKey) is { } shared)
|
|
return ClampSettings.Parse(shared.Json, out _);
|
|
return File.Exists(legacyClampsPath) ? ClampSettings.Load(legacyClampsPath) : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The machine new nests are assigned: the nest defaults' machine when it is on file,
|
|
/// otherwise the first by name, or null when there are none.
|
|
/// </summary>
|
|
public static MachineConfig DefaultMachine(NestDefaults defaults, IEnumerable<MachineConfig> machines)
|
|
{
|
|
var sorted = machines.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
|
|
return sorted.FirstOrDefault(m => m.Id == defaults?.MachineId) ?? sorted.FirstOrDefault();
|
|
}
|
|
|
|
/// <summary>The configuration of <paramref name="machine"/>, or null when none is assigned or it is not on file.</summary>
|
|
public MachineConfig FindMachine(NestMachine machine)
|
|
{
|
|
if (machine == null)
|
|
return null;
|
|
try
|
|
{
|
|
return CreateMachineProvider().GetMachine(machine.Id);
|
|
}
|
|
catch (IOException) { }
|
|
catch (UnauthorizedAccessException) { }
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns <paramref name="machine"/> to <paramref name="nest"/> (null for none) and gives
|
|
/// every sheet that machine's support grid, or none when the machine gives nests no grid.
|
|
/// </summary>
|
|
public void AssignMachine(Nest nest, MachineConfig machine)
|
|
{
|
|
nest.Machine = machine == null ? null : new NestMachine(machine.Id, machine.Name);
|
|
var grid = LoadSupportGridSettings(machine);
|
|
nest.SetSupportBed(grid.Enabled ? grid.BedFor(nest.Units) : null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cutting parameters a plate of <paramref name="nest"/> without its own starts from:
|
|
/// the last ones applied (or the built-in defaults), with the lead-ins, lead-outs and kerf
|
|
/// of the nest's machine for its material and thickness when the machine has them. Last
|
|
/// parameters already taken from that machine, material and thickness keep the user's
|
|
/// edits to them.
|
|
/// </summary>
|
|
public CuttingParameters DefaultCuttingParameters(Nest nest)
|
|
{
|
|
var parameters = LastCuttingParameters();
|
|
var machine = FindMachine(nest.Machine);
|
|
if (!MachineCuttingDefaults.IsRecordedFor(parameters, machine, nest))
|
|
MachineCuttingDefaults.ApplyTo(parameters, machine, nest);
|
|
return parameters;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cutting parameters to plan or place lead-ins on <paramref name="plate"/> with: the
|
|
/// user's (see <see cref="DefaultCuttingParameters"/>), so an edit reaches nests already
|
|
/// planned. The plate's own are used only when no user settings are saved or readable.
|
|
/// </summary>
|
|
public CuttingParameters CuttingParametersFor(Plate plate, Nest nest) =>
|
|
plate?.CuttingParameters != null && ReadLastCuttingParameters() == null
|
|
? plate.CuttingParameters
|
|
: DefaultCuttingParameters(nest);
|
|
|
|
/// <summary>Whether the cutting settings of <paramref name="nest"/> are kept on its machine's thickness.</summary>
|
|
public bool HasMachineThickness(Nest nest) =>
|
|
nest != null && MachineCuttingDefaults.Find(FindMachine(nest.Machine), nest) != null;
|
|
|
|
/// <summary>
|
|
/// Where the cutting settings of <paramref name="nest"/> are kept, for the settings
|
|
/// dialogs: the machine's thickness, or why this PC's settings are used instead.
|
|
/// </summary>
|
|
public string DescribeCuttingSource(Nest nest)
|
|
{
|
|
var machine = FindMachine(nest?.Machine);
|
|
if (nest == null || machine == null)
|
|
return "This PC's settings: the nest has no machine.";
|
|
var material = nest.Material?.Name;
|
|
if (string.IsNullOrWhiteSpace(material) || !double.IsFinite(nest.Thickness) || nest.Thickness <= 0)
|
|
return "This PC's settings: the nest has no material and thickness.";
|
|
var job = $"{material}{(string.IsNullOrWhiteSpace(nest.Material.Grade) ? "" : " " + nest.Material.Grade)} "
|
|
+ $"{nest.Thickness:0.####} {(nest.Units == Units.Millimeters ? "mm" : "in")}";
|
|
return MachineCuttingDefaults.Find(machine, nest) == null
|
|
? $"This PC's settings: {machine.Name} has no {job}."
|
|
: $"Saved for {machine.Name} · {job}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves cutting settings edited for <paramref name="nest"/>: on the machine's thickness
|
|
/// when it has one, otherwise as this PC's settings. When the machine cannot be saved
|
|
/// (the nest server is offline, or someone else changed the machine), the settings are
|
|
/// kept on this PC for this job and <paramref name="warn"/> is told why. Returns whether
|
|
/// they reached the machine.
|
|
/// </summary>
|
|
public bool SaveCuttingParameters(Nest nest, CuttingParameters parameters, Action<string> warn) =>
|
|
SaveCuttingParameters(nest, parameters, CreateMachineProvider(), warn);
|
|
|
|
/// <summary>
|
|
/// <see cref="SaveCuttingParameters(Nest, CuttingParameters, Action{string})"/> with the
|
|
/// machines of <paramref name="provider"/>.
|
|
/// </summary>
|
|
public bool SaveCuttingParameters(Nest nest, CuttingParameters parameters, IDataProvider provider, Action<string> warn)
|
|
{
|
|
MachineConfig machine = null;
|
|
string error;
|
|
try
|
|
{
|
|
machine = nest?.Machine == null ? null : provider.GetMachine(nest.Machine.Id);
|
|
if (machine == null || !MachineCuttingDefaults.WriteTo(machine, nest, parameters))
|
|
{
|
|
SaveLastCuttingParameters(parameters);
|
|
return false;
|
|
}
|
|
provider.SaveMachine(machine);
|
|
// A copy kept on this PC for this job would hide the machine's from now on.
|
|
var last = ReadLastCuttingParameters();
|
|
if (last != null && MachineCuttingDefaults.IsRecordedFor(last, machine, nest))
|
|
{
|
|
last.MachineName = null;
|
|
SaveLastCuttingParameters(last);
|
|
}
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException)
|
|
{
|
|
error = ex.Message;
|
|
}
|
|
|
|
// Stamped for this job, so the edit sticks on this PC until the machine takes it.
|
|
var kept = CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(parameters));
|
|
kept.MachineName = machine?.Name ?? nest.Machine.Name;
|
|
kept.MaterialName = nest.Material?.Name;
|
|
kept.Grade = nest.Material?.Grade;
|
|
kept.Thickness = nest.Thickness;
|
|
SaveLastCuttingParameters(kept);
|
|
warn?.Invoke($"The cutting settings could not be saved to {kept.MachineName}:\n\n{error}\n\n"
|
|
+ "They are kept on this PC for this material and thickness.");
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Saves <paramref name="parameters"/> as this PC's last cutting settings.</summary>
|
|
public void SaveLastCuttingParameters(CuttingParameters parameters) =>
|
|
user.CuttingParametersJson = CuttingParametersSerializer.Serialize(parameters);
|
|
|
|
/// <summary>The cutting parameters last applied, or the built-in defaults when none are saved or readable.</summary>
|
|
public CuttingParameters LastCuttingParameters() =>
|
|
ReadLastCuttingParameters() ?? new CuttingParameters();
|
|
|
|
private CuttingParameters ReadLastCuttingParameters()
|
|
{
|
|
var json = user.CuttingParametersJson;
|
|
if (!string.IsNullOrEmpty(json))
|
|
{
|
|
try
|
|
{
|
|
// Settings saved with the retired "Tab every part" read as tabs by size over
|
|
// every width. A plate's recorded parameters are never migrated.
|
|
var parameters = CuttingParametersSerializer.Deserialize(json);
|
|
Support.SupportTabSettings.MigrateTabEveryPart(parameters);
|
|
return parameters;
|
|
}
|
|
catch
|
|
{ /* fall through */
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gives <paramref name="settings"/> the cut-off clearance, overtravel, minimum segment and
|
|
/// direction of the nest's machine for its material and thickness, when the machine has
|
|
/// them; otherwise they are left as they are.
|
|
/// </summary>
|
|
public void ApplyCutOffDefaults(CutOffSettings settings, Nest nest) =>
|
|
MachineCuttingDefaults.ApplyTo(settings, FindMachine(nest.Machine), nest);
|
|
|
|
/// <summary>
|
|
/// The support grid of <paramref name="machine"/>; a machine without one saved yet, or no
|
|
/// machine, uses the legacy shared support-grid.json. Never throws, and loads as off when unreadable.
|
|
/// </summary>
|
|
public SupportGridSettings LoadSupportGridSettings(MachineConfig machine) =>
|
|
machine?.SupportGrid ?? SupportGridSettings.Load(legacySupportGridPath);
|
|
|
|
/// <summary>Saves <paramref name="settings"/> as the support grid of the machine with <paramref name="machineId"/>.</summary>
|
|
public void SaveSupportGridSettings(Guid machineId, SupportGridSettings settings)
|
|
{
|
|
var provider = CreateMachineProvider();
|
|
var machine = provider.GetMachine(machineId)
|
|
?? throw new IOException("The machine configuration is no longer on file.");
|
|
machine.SupportGrid = settings;
|
|
provider.SaveMachine(machine);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the persisted nest defaults (see <see cref="LoadSavedNestDefaults"/>).
|
|
/// <paramref name="warning"/> says, the first time in a session, that they could not be
|
|
/// read and the built-in defaults are used; it is null otherwise.
|
|
/// </summary>
|
|
public NestDefaults LoadNestDefaults(out string warning)
|
|
{
|
|
var defaults = LoadSavedNestDefaults(out var status);
|
|
warning = null;
|
|
if (status == NestDefaultsStatus.Invalid && !defaultsWarned)
|
|
{
|
|
defaultsWarned = true;
|
|
var source = SharedNestDefaults() != null ? "on the nest server" : nestDefaultsPath;
|
|
warning = $"The nest defaults could not be read:\n{source}\n\nBuilt-in defaults will be used. Re-save your defaults from Tools > Nest Defaults.";
|
|
}
|
|
return defaults;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The single desktop entry point for persisted nest defaults. Units
|
|
/// fall back to the legacy DefaultUnit setting whenever the file is
|
|
/// missing or unusable or has no valid unit, so New, BOM import and
|
|
/// the defaults dialog always agree.
|
|
/// </summary>
|
|
public NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) =>
|
|
SharedNestDefaults() is { } shared
|
|
? NestDefaults.Parse(shared.Json, user.DefaultUnit, out status)
|
|
: NestDefaults.Load(nestDefaultsPath, user.DefaultUnit, out status);
|
|
|
|
/// <summary>This PC's own nest defaults, shared or not, for uploading.</summary>
|
|
public NestDefaults LoadLocalNestDefaults() =>
|
|
NestDefaults.Load(nestDefaultsPath, user.DefaultUnit, out _);
|
|
|
|
/// <summary>
|
|
/// Saves the nest defaults where they are read: the server once it holds them, otherwise
|
|
/// this PC's file. Throws <see cref="IOException"/> when the server cannot take them.
|
|
/// </summary>
|
|
public void SaveNestDefaults(NestDefaults defaults)
|
|
{
|
|
if (Shared.Current() is { } current
|
|
&& current.Get(SharedSettingKinds.NestDefaults, SharedSettingKinds.DefaultKey) != null)
|
|
current.Save(SharedSettingKinds.NestDefaults, SharedSettingKinds.DefaultKey, defaults.ToJson());
|
|
else
|
|
defaults.Save(nestDefaultsPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This PC's machines, <paramref name="postProcessors"/>' settings and nest defaults that
|
|
/// <paramref name="current"/> does not hold yet (see <see cref="SharedSettingsSource.PendingUploads"/>).
|
|
/// </summary>
|
|
public IReadOnlyList<SharedSettingUpload> PendingUploads(SharedSettingsStore current, IEnumerable<IPostProcessor> postProcessors) =>
|
|
SharedSettingsSource.PendingUploads(current, LoadLocalMachines, postProcessors,
|
|
() => File.Exists(nestDefaultsPath) ? LoadLocalNestDefaults().ToJson() : null);
|
|
|
|
private SharedSettingRecord SharedNestDefaults() =>
|
|
Shared.Find(SharedSettingKinds.NestDefaults, SharedSettingKinds.DefaultKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="IMachineUserSettings"/> kept in the desktop user-settings file
|
|
/// (<see cref="UserSettingsFile"/>) under the WinForms names and text: DefaultUnit as the unit's
|
|
/// name ("Inches" when missing or unreadable) and CuttingParametersJson as JSON text.
|
|
/// </summary>
|
|
public sealed class MachineUserSettingsFile(string path) : IMachineUserSettings
|
|
{
|
|
public const string DefaultUnitName = "DefaultUnit";
|
|
public const string CuttingParametersName = "CuttingParametersJson";
|
|
|
|
public Units DefaultUnit =>
|
|
UserSettingsFile.Read(path).TryGetValue(DefaultUnitName, out var text)
|
|
&& Enum.TryParse<Units>(text?.Trim(), ignoreCase: false, out var units)
|
|
&& Enum.IsDefined(units)
|
|
? units
|
|
: Units.Inches;
|
|
|
|
public string CuttingParametersJson
|
|
{
|
|
get => UserSettingText.ReadString(UserSettingsFile.Read(path), CuttingParametersName, "");
|
|
set => UserSettingsFile.Update(path, values => values[CuttingParametersName] = value ?? "");
|
|
}
|
|
}
|