Files
OpenNest/OpenNest/Forms/EditLibraryDrawingForm.cs
T
ajandClaude Opus 5.5 5a374aa3f4 feat(library): content-addressed packages and editable nesting settings
Schema v5 stores each distinct drawing package once in drawing_packages,
keyed by SHA-256; revisions point at it, so details, settings, archive and
restore revisions add no blob. The v4 drawing tables are replaced outright
(no production library existed); nest tables are untouched.

Nesting settings (cut first, priority, rotation constraints, color) become
structured record fields (DrawingLibraryRecord.Settings), returned on every
read, filterable by cutFirst, editable through the metadata endpoint and
applied by LibraryDrawingInsert. Canonical packages now leave every
record-owned field at its default, so renaming or changing settings never
changes the package hash. Saving a nest drawing to the library carries its
settings into the record.

The desktop library browser gains a Cut first column and an Edit Settings
dialog that saves notes and settings as a new revision with the same package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:47:18 -04:00

148 lines
6.1 KiB
C#

using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenNest.Data;
using OpenNest.Math;
namespace OpenNest.Forms
{
/// <summary>
/// Drawing library &gt; Edit Settings. Edits one library drawing's notes and nesting
/// settings (Cut first, priority, rotation constraints, color) through
/// <see cref="DrawingLibraryEditSession"/>. Saving adds a revision on the server that
/// keeps the same package; nests that already hold the drawing are not changed.
/// </summary>
public partial class EditLibraryDrawingForm : Form
{
private readonly DrawingLibraryEditSession session;
private readonly decimal initialStep;
private readonly decimal initialStart;
private readonly decimal initialEnd;
private Color color;
private bool saving;
/// <summary>Shows a message; replaced by tests.</summary>
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
/// <summary>The saved revision once the dialog closes with OK; otherwise null.</summary>
public DrawingLibraryRecord Saved { get; private set; }
public EditLibraryDrawingForm(DrawingLibraryEditSession session)
{
InitializeComponent();
this.session = session ?? throw new ArgumentNullException(nameof(session));
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
var listed = session.Listed;
var settings = listed.Settings ?? new DrawingNestingSettings();
cutFirstCheckBox.Checked = settings.CutFirst;
priorityBox.Value = settings.Priority;
initialStep = SetDegrees(stepBox, settings.StepAngle);
initialStart = SetDegrees(startBox, settings.StartAngle);
initialEnd = SetDegrees(endBox, settings.EndAngle);
allow180CheckBox.Checked = settings.Allow180Equivalent;
color = Color.FromArgb(settings.ColorArgb);
colorButton.BackColor = color.A == 0 ? SystemColors.Control : color;
notesBox.Text = listed.Notes;
Text = $"Edit '{listed.Name}' — library revision {listed.Revision}";
UpdateCommands();
}
/// <summary>The settings as currently entered; unchanged angles keep their exact stored radians.</summary>
internal DrawingNestingSettings ReadSettings()
{
var listed = session.Listed.Settings ?? new DrawingNestingSettings();
return new DrawingNestingSettings
{
CutFirst = cutFirstCheckBox.Checked,
Priority = (int)priorityBox.Value,
StepAngle = ReadDegrees(stepBox, initialStep, listed.StepAngle),
StartAngle = ReadDegrees(startBox, initialStart, listed.StartAngle),
EndAngle = ReadDegrees(endBox, initialEnd, listed.EndAngle),
Allow180Equivalent = allow180CheckBox.Checked,
ColorArgb = color.ToArgb(),
};
}
private void Value_Changed(object sender, EventArgs e) => UpdateCommands();
private void ColorButton_Click(object sender, EventArgs e)
{
using var dialog = new ColorDialog { Color = color, FullOpen = true };
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
// Opaque, as nest drawings are.
color = Color.FromArgb(255, dialog.Color);
colorButton.BackColor = color;
UpdateCommands();
}
private async void SaveButton_Click(object sender, EventArgs e) => await SaveAsync();
/// <summary>Saves the edit as a new library revision; closes with OK on success.</summary>
internal async Task SaveAsync()
{
if (saving || !CanSave())
return;
saving = true;
UpdateCommands();
statusLabel.Text = "Saving...";
try
{
Saved = await session.SaveAsync(notesBox.Text, ReadSettings());
if (IsDisposed)
return;
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex) when (ex is IOException || ex is HttpRequestException || ex is TaskCanceledException
|| ex is ArgumentException)
{
// A conflict (someone else saved first) is an IOException too; nothing was changed.
if (IsDisposed)
return;
statusLabel.Text = "";
ShowMessage(ex is DrawingLibraryConflictException
? ex.Message + Environment.NewLine + Environment.NewLine + "Close this dialog to reload the library, then edit again."
: $"Could not save the library drawing: {ex.Message}", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
finally
{
saving = false;
if (!IsDisposed)
UpdateCommands();
}
}
private bool CanSave()
{
var settings = ReadSettings();
return session.GetError(notesBox.Text, settings) == null && !session.IsUnchanged(notesBox.Text, settings);
}
private void UpdateCommands()
{
var error = session.GetError(notesBox.Text, ReadSettings());
saveButton.Enabled = !saving && CanSave();
if (!saving)
statusLabel.Text = error == null ? "" : char.ToUpper(error[0], CultureInfo.CurrentCulture) + error[1..];
}
private static decimal SetDegrees(NumericUpDown box, double radians)
{
var degrees = (decimal)System.Math.Round(Angle.ToDegrees(radians), box.DecimalPlaces);
box.Value = System.Math.Clamp(degrees, box.Minimum, box.Maximum);
return box.Value;
}
private static double ReadDegrees(NumericUpDown box, decimal initial, double original) =>
box.Value == initial ? original : Angle.ToRadians((double)box.Value);
}
}