Files
OpenNest/OpenNest.Data/SupportGridSettings.cs
T
ajandClaude Opus 5.5 7ce5e73a82 refactor: share duplicated paging, HTTP and helper code
Roslyn duplicate scans found the same code copied across layers:
- Data: the three browse sessions share BrowseSession<TPage, TSort>;
  the remote clients share NestServerHttp (JSON options, If-Match,
  error text).
- WinForms: five paged lists share BrowsePager for loading text,
  navigation, sort glyphs and failure messages. The Add Drawings to
  Release, Drawing Library and set browser dialogs now dispose their
  browse sessions.
- Server: NestQueryRequest reuses DrawingLibraryRequest's helpers;
  revision history queries share count and paging binds.
- Engine: RemnantPlateNester delegates to DefaultPlateNester;
  CuttingPlanService reuses JointCuttingPlanSearch.Map.
- Core: LineArc and CleanHole lead-ins share LineArcApproach with
  unchanged arithmetic order, so output is bit-identical.
- IO: Dxf.GetGeometry overloads share one reader; .nest JSON test
  helpers move to NestArchiveJson.

Also fixes the production compiler warnings (CS8632, CS8603, CS0108)
and moves a test off an obsolete forwarding API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:45:36 -04:00

174 lines
7.2 KiB
C#

using System.Text.Json;
using OpenNest.Support;
namespace OpenNest.Data;
/// <summary>
/// The machine's laser support grid, persisted to a single JSON file (by default
/// %APPDATA%\OpenNest\support-grid.json). Off by default: until <see cref="Enabled"/> 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.
/// </summary>
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,
};
/// <summary>Whether new nests receive this grid. Default off.</summary>
public bool Enabled { get; set; }
/// <summary>Units of every length below.</summary>
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;
/// <summary>%APPDATA%\OpenNest\support-grid.json.</summary>
public static string DefaultPath =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OpenNest",
"support-grid.json"
);
/// <summary>Settings holding <paramref name="grid"/> (or the defaults when null) in <paramref name="units"/>.</summary>
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;
}
/// <summary>The grid these settings describe in <paramref name="units"/>, or null when it is invalid.</summary>
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;
}
}
/// <summary>A copy with every length expressed in <paramref name="units"/> (inches and millimeters convert at 25.4).</summary>
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,
};
}
/// <summary>Gives a newly created nest this grid when enabled (after its units are set).</summary>
public void ApplyToNewNest(Nest nest)
{
ArgumentNullException.ThrowIfNull(nest);
if (Enabled && GridFor(nest.Units) is { } grid)
nest.SetSupportBed(grid);
}
/// <summary>
/// Loads the settings, never throwing. A missing or unreadable file yields the defaults
/// (off); each invalid value in a readable file keeps its default.
/// </summary>
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<Units>())
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();
}
}
/// <summary>Writes the file, creating its directory.</summary>
public void Save(string path)
{
var dto = new Dictionary<string, object>
{
["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;
}
}