Files
OpenNest/OpenNest.Data/SupportGridSettings.cs
T
ajandClaude Opus 5.5 a7c0747e11 feat(planning): tab parts the laser support grid cannot hold level
Model the machine's support bed as a grid of contact points measured
from the plate origin, and check each part's center of gravity against
the convex hull of the grid points under its material. The bed is
extensible so straight and CAD-imported curved slats can follow.

- Core: SupportBed/SupportGrid, SupportMaterial and SupportAnalyzer
  (margin with registration tolerance, best single tab point)
- Nest-wide support bed that every plate follows, saved in
  plateDefaults.supportBed; machine defaults in support-grid.json
- CuttingParameters.SupportTabs: Plan Cutting tabs only unsupported
  parts and limits their perimeter entries to points where the tab
  steadies the part, falling back to any fitting entry
- Desktop: Nest > Support Grid..., View > Draw Support overlay and a
  "Tab parts the support grid cannot hold level" tab option
- docs/support-tabs.md, nest file format and planner notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:55:24 -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;
}
}