using System; using System.Collections.Generic; using System.IO; using System.Linq; using OpenNest.CNC.CuttingStrategy; using OpenNest.Data; using OpenNest.IO; namespace OpenNest.Desktop; /// /// 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). /// public interface IMachineUserSettings { /// The DefaultUnit setting. Units DefaultUnit { get; } /// The CuttingParametersJson setting; setting it saves it. string CuttingParametersJson { get; set; } } /// /// 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 /// (), 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. /// 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; /// Where shared copies come from. /// The per-user settings this reads and saves. /// This PC's machines; defaults to %APPDATA%\OpenNest\Machines. /// This PC's nest defaults; defaults to . /// The pre-machine clamps.json; defaults to . /// The pre-machine support-grid.json; defaults to . 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; } /// %APPDATA%\OpenNest\Machines. public static string DefaultMachinesDirectory => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OpenNest", "Machines"); /// Where shared copies come from. public SharedSettingsSource Shared { get; } /// This PC's nest defaults file. public string NestDefaultsPath => nestDefaultsPath; /// /// The machine configurations edited in Tools > Machine Configuration: the server's when /// it holds any, otherwise this PC's. re-reads the server first. /// public IDataProvider CreateMachineProvider(bool refresh = false) => Shared.Holding(SharedSettingKinds.Machine, refresh) is { } shared ? new SharedMachineProvider(shared) : CreateLocalMachineProvider(); /// This PC's machine configurations, with the built-in ones written on first use. public LocalJsonProvider CreateLocalMachineProvider() { var provider = new LocalJsonProvider(machinesDirectory); provider.EnsureDefaults(); return provider; } /// Every readable machine configuration, for choosing acceleration limits. public List LoadMachines() => ReadMachines(() => CreateMachineProvider()); /// Every readable machine configuration on this PC, shared or not, for uploading. public List LoadLocalMachines() => ReadMachines(CreateLocalMachineProvider); private static List ReadMachines(Func createProvider) { var machines = new List(); 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; } /// Every material and grade on file across the readable machine configurations. public IEnumerable LoadMaterials() => LoadMachines().SelectMany(machine => machine.Materials); /// /// The clamps new nests assigned receive (New, BOM import, Load /// Machine Defaults); off when there is no machine or it has no clamps. /// public static ClampSettings LoadClampSettings(MachineConfig machine) => machine?.GetClampSettings() ?? ClampSettings.Fallback; /// Saves as the clamps of the machine with . 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); } /// /// 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 picks one (null for not /// now), asked once per session. Returns what to tell the user when the copy failed, or null. /// public string MigrateLegacyClamps(Func, 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; } /// /// 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. /// public static MachineConfig DefaultMachine(NestDefaults defaults, IEnumerable machines) { var sorted = machines.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); return sorted.FirstOrDefault(m => m.Id == defaults?.MachineId) ?? sorted.FirstOrDefault(); } /// The configuration of , or null when none is assigned or it is not on file. public MachineConfig FindMachine(NestMachine machine) { if (machine == null) return null; try { return CreateMachineProvider().GetMachine(machine.Id); } catch (IOException) { } catch (UnauthorizedAccessException) { } return null; } /// /// Assigns to (null for none) and gives /// every sheet that machine's support grid, or none when the machine gives nests no grid. /// 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); } /// /// The cutting parameters a plate of 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. /// 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; } /// /// The cutting parameters to plan or place lead-ins on with: the /// user's (see ), so an edit reaches nests already /// planned. The plate's own are used only when no user settings are saved or readable. /// public CuttingParameters CuttingParametersFor(Plate plate, Nest nest) => plate?.CuttingParameters != null && ReadLastCuttingParameters() == null ? plate.CuttingParameters : DefaultCuttingParameters(nest); /// Whether the cutting settings of are kept on its machine's thickness. public bool HasMachineThickness(Nest nest) => nest != null && MachineCuttingDefaults.Find(FindMachine(nest.Machine), nest) != null; /// /// Where the cutting settings of are kept, for the settings /// dialogs: the machine's thickness, or why this PC's settings are used instead. /// 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}"; } /// /// Saves cutting settings edited for : 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 is told why. Returns whether /// they reached the machine. /// public bool SaveCuttingParameters(Nest nest, CuttingParameters parameters, Action warn) => SaveCuttingParameters(nest, parameters, CreateMachineProvider(), warn); /// /// with the /// machines of . /// public bool SaveCuttingParameters(Nest nest, CuttingParameters parameters, IDataProvider provider, Action 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; } /// Saves as this PC's last cutting settings. public void SaveLastCuttingParameters(CuttingParameters parameters) => user.CuttingParametersJson = CuttingParametersSerializer.Serialize(parameters); /// The cutting parameters last applied, or the built-in defaults when none are saved or readable. 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; } /// /// Gives 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. /// public void ApplyCutOffDefaults(CutOffSettings settings, Nest nest) => MachineCuttingDefaults.ApplyTo(settings, FindMachine(nest.Machine), nest); /// /// The support grid of ; a machine without one saved yet, or no /// machine, uses the legacy shared support-grid.json. Never throws, and loads as off when unreadable. /// public SupportGridSettings LoadSupportGridSettings(MachineConfig machine) => machine?.SupportGrid ?? SupportGridSettings.Load(legacySupportGridPath); /// Saves as the support grid of the machine with . 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); } /// /// Loads the persisted nest defaults (see ). /// says, the first time in a session, that they could not be /// read and the built-in defaults are used; it is null otherwise. /// 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; } /// /// 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. /// public NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) => SharedNestDefaults() is { } shared ? NestDefaults.Parse(shared.Json, user.DefaultUnit, out status) : NestDefaults.Load(nestDefaultsPath, user.DefaultUnit, out status); /// This PC's own nest defaults, shared or not, for uploading. public NestDefaults LoadLocalNestDefaults() => NestDefaults.Load(nestDefaultsPath, user.DefaultUnit, out _); /// /// Saves the nest defaults where they are read: the server once it holds them, otherwise /// this PC's file. Throws when the server cannot take them. /// 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); } /// /// This PC's machines, ' settings and nest defaults that /// does not hold yet (see ). /// public IReadOnlyList PendingUploads(SharedSettingsStore current, IEnumerable postProcessors) => SharedSettingsSource.PendingUploads(current, LoadLocalMachines, postProcessors, () => File.Exists(nestDefaultsPath) ? LoadLocalNestDefaults().ToJson() : null); private SharedSettingRecord SharedNestDefaults() => Shared.Find(SharedSettingKinds.NestDefaults, SharedSettingKinds.DefaultKey); } /// /// kept in the desktop user-settings file /// () under the WinForms names and text: DefaultUnit as the unit's /// name ("Inches" when missing or unreadable) and CuttingParametersJson as JSON text. /// 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(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 ?? ""); } }