using System.Text.Json; using OpenNest.Support; namespace OpenNest.Data; /// /// The machine's laser support grid, persisted to a single JSON file (by default /// %APPDATA%\OpenNest\support-grid.json). Off by default: until is set, /// new nests get no support bed. Loading never throws; an invalid value falls back to its /// default and a file that cannot be read loads as off. /// public sealed class SupportGridSettings { public const int CurrentVersion = 1; private const double MillimetersPerInch = 25.4; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; /// Whether new nests receive this grid. Default off. public bool Enabled { get; set; } /// Units of every length below. public Units Units { get; set; } = Units.Inches; public double PitchX { get; set; } = 2; public double PitchY { get; set; } = 2; public double OffsetX { get; set; } public double OffsetY { get; set; } public double RegistrationTolerance { get; set; } = 0.125; public double StabilityMargin { get; set; } = 0.25; /// %APPDATA%\OpenNest\support-grid.json. public static string DefaultPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OpenNest", "support-grid.json" ); /// Settings holding (or the defaults when null) in . public static SupportGridSettings FromGrid(SupportGrid grid, Units units, bool enabled) { var settings = new SupportGridSettings().ConvertedTo(units); settings.Enabled = enabled; if (grid != null) { settings.PitchX = grid.PitchX; settings.PitchY = grid.PitchY; settings.OffsetX = grid.OffsetX; settings.OffsetY = grid.OffsetY; settings.RegistrationTolerance = grid.RegistrationTolerance; settings.StabilityMargin = grid.StabilityMargin; } return settings; } /// The grid these settings describe in , or null when it is invalid. public SupportGrid GridFor(Units units) { var converted = ConvertedTo(units); try { return new SupportGrid(converted.PitchX, converted.PitchY, converted.OffsetX, converted.OffsetY, converted.RegistrationTolerance, converted.StabilityMargin); } catch (ArgumentOutOfRangeException) { return null; } } /// A copy with every length expressed in (inches and millimeters convert at 25.4). public SupportGridSettings ConvertedTo(Units units) { var scale = Units == units ? 1 : Units == Units.Inches ? MillimetersPerInch : 1 / MillimetersPerInch; return new SupportGridSettings { Enabled = Enabled, Units = units, PitchX = PitchX * scale, PitchY = PitchY * scale, OffsetX = OffsetX * scale, OffsetY = OffsetY * scale, RegistrationTolerance = RegistrationTolerance * scale, StabilityMargin = StabilityMargin * scale, }; } /// Gives a newly created nest this grid when enabled (after its units are set). public void ApplyToNewNest(Nest nest) { ArgumentNullException.ThrowIfNull(nest); if (Enabled && GridFor(nest.Units) is { } grid) nest.SetSupportBed(grid); } /// /// Loads the settings, never throwing. A missing or unreadable file yields the defaults /// (off); each invalid value in a readable file keeps its default. /// public static SupportGridSettings Load(string path) { var settings = new SupportGridSettings(); if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return settings; try { using var document = JsonDocument.Parse(File.ReadAllText(path)); var root = document.RootElement; if (root.ValueKind != JsonValueKind.Object) return settings; if (root.TryGetProperty("enabled", out var enabled) && enabled.ValueKind is JsonValueKind.True or JsonValueKind.False) settings.Enabled = enabled.GetBoolean(); // Lengths read below are in the file's units; the defaults are converted to them first. if (root.TryGetProperty("units", out var units) && units.ValueKind == JsonValueKind.String) foreach (var unit in Enum.GetValues()) if (string.Equals(unit.ToString(), units.GetString(), StringComparison.OrdinalIgnoreCase)) settings = settings.ConvertedTo(unit); settings.PitchX = Length(root, "pitchX", settings.PitchX, positive: true); settings.PitchY = Length(root, "pitchY", settings.PitchY, positive: true); settings.OffsetX = Length(root, "offsetX", settings.OffsetX, positive: false, signed: true); settings.OffsetY = Length(root, "offsetY", settings.OffsetY, positive: false, signed: true); settings.RegistrationTolerance = Length(root, "registrationTolerance", settings.RegistrationTolerance, positive: false); settings.StabilityMargin = Length(root, "stabilityMargin", settings.StabilityMargin, positive: false); return settings; } catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException or NotSupportedException or System.Security.SecurityException) { return new SupportGridSettings(); } } /// Writes the file, creating its directory. public void Save(string path) { var dto = new Dictionary { ["version"] = CurrentVersion, ["enabled"] = Enabled, ["units"] = Units.ToString().ToLowerInvariant(), ["pitchX"] = PitchX, ["pitchY"] = PitchY, ["offsetX"] = OffsetX, ["offsetY"] = OffsetY, ["registrationTolerance"] = RegistrationTolerance, ["stabilityMargin"] = StabilityMargin, }; var directory = Path.GetDirectoryName(Path.GetFullPath(path)); if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); File.WriteAllText(path, JsonSerializer.Serialize(dto, JsonOptions)); } // A finite number that stays finite in either unit; otherwise the fallback. private static double Length(JsonElement root, string name, double fallback, bool positive, bool signed = false) { if (!root.TryGetProperty(name, out var element) || element.ValueKind != JsonValueKind.Number || !element.TryGetDouble(out var value) || !double.IsFinite(value) || !double.IsFinite(value * MillimetersPerInch)) return fallback; if (positive ? value <= 0 : !signed && value < 0) return fallback; return value; } }