diff --git a/OpenNest.Data/ClampRailEditor.cs b/OpenNest.Data/ClampRailEditor.cs new file mode 100644 index 0000000..45ba887 --- /dev/null +++ b/OpenNest.Data/ClampRailEditor.cs @@ -0,0 +1,119 @@ +namespace OpenNest.Data; + +/// +/// Decision logic behind the desktop Clamps dialog: edits one nest's clamp rail in that +/// nest's units. Three explicit boundaries: only fills +/// the editor, changes the nest (the dialog's OK), and +/// is what "Save as Machine Defaults" writes immediately. +/// +public sealed class ClampRailEditor +{ + private readonly List _centers = new(); + + /// Starts from the nest's rail, or the default clamp size with no clamps. + public ClampRailEditor(ClampRail? rail, Units units, bool applyToNewNests) + { + Units = units; + ApplyToNewNests = applyToNewNests; + var initial = ClampSettings.FromRail(rail, units, applyToNewNests); + Width = initial.Width; + Depth = initial.Depth; + _centers.AddRange(initial.Centers); + } + + /// The nest's units; every value is in these units. + public Units Units { get; } + + /// Clamp extent along the edge (X). + public double Width { get; set; } + + /// Clamp intrusion into the sheet (Y). + public double Depth { get; set; } + + /// Whether saved machine defaults give new nests these clamps. + public bool ApplyToNewNests { get; set; } + + /// Clamp centers along X from the plate origin, ascending and distinct. + public IReadOnlyList Centers => _centers; + + /// + /// Adds a clamp center, keeping the list sorted. Refuses (with an operator-facing + /// reason) values that are not finite, negative, or already listed. + /// + public bool TryAddCenter(double center, out string? error) + { + if (!double.IsFinite(center) || center < 0) + { + error = "Enter a clamp center measured from the plate origin (zero or more)."; + return false; + } + + var index = _centers.BinarySearch(center); + if (index >= 0) + { + error = $"A clamp is already centered at {center:0.####}."; + return false; + } + + _centers.Insert(~index, center); + error = null; + return true; + } + + public void RemoveCenterAt(int index) + { + if (index >= 0 && index < _centers.Count) + _centers.RemoveAt(index); + } + + /// + /// Replaces the editor's values with the machine defaults converted to this nest's + /// units. Nothing is applied or saved. + /// + public void LoadMachineDefaults(ClampSettings settings) + { + ArgumentNullException.ThrowIfNull(settings); + var converted = settings.ConvertedTo(Units); + Width = converted.Width; + Depth = converted.Depth; + ApplyToNewNests = settings.Enabled; + _centers.Clear(); + _centers.AddRange(converted.Centers.Distinct().OrderBy(c => c)); + } + + /// + /// The edited rail, or null when no clamp is listed. Throws + /// for a non-positive width or depth. + /// + public ClampRail? BuildRail() => + _centers.Count == 0 ? null : new ClampRail(Width, Depth, _centers); + + /// The dialog's OK: makes the edited rail the nest's, re-resolving every plate. + public void ApplyTo(Nest nest) + { + ArgumentNullException.ThrowIfNull(nest); + nest.SetClampRail(BuildRail()); + } + + /// + /// The machine settings "Save as Machine Defaults" writes: these values in this nest's + /// units, enabled for new nests only when requested and at least one clamp is listed. + /// + public ClampSettings ToMachineDefaults() + { + // Validate before anything is persisted. + if (!double.IsFinite(Width) || Width <= 0 || !double.IsFinite(Depth) || Depth <= 0) + throw new ArgumentOutOfRangeException( + nameof(Width), + "Clamp width and depth must be positive." + ); + return new ClampSettings + { + Enabled = ApplyToNewNests && _centers.Count > 0, + Units = Units, + Width = Width, + Depth = Depth, + Centers = _centers.ToArray(), + }; + } +} diff --git a/OpenNest.Data/ClampSettings.cs b/OpenNest.Data/ClampSettings.cs index 5192019..3f23152 100644 --- a/OpenNest.Data/ClampSettings.cs +++ b/OpenNest.Data/ClampSettings.cs @@ -95,8 +95,25 @@ public sealed class ClampSettings if (!Enabled || Centers.Count == 0) return null; + var converted = ConvertedTo(units); + return new ClampRail(converted.Width, converted.Depth, converted.Centers); + } + + /// + /// A copy with width, depth and centers expressed in + /// (inches and millimeters convert at 25.4); is kept. + /// + public ClampSettings ConvertedTo(Units units) + { var scale = Scale(Units, units); - return new ClampRail(Width * scale, Depth * scale, Centers.Select(c => c * scale)); + return new ClampSettings + { + Enabled = Enabled, + Units = units, + Width = Width * scale, + Depth = Depth * scale, + Centers = Centers.Select(c => c * scale).ToArray(), + }; } /// diff --git a/OpenNest.Tests/Data/ClampRailEditorTests.cs b/OpenNest.Tests/Data/ClampRailEditorTests.cs new file mode 100644 index 0000000..52df228 --- /dev/null +++ b/OpenNest.Tests/Data/ClampRailEditorTests.cs @@ -0,0 +1,171 @@ +using OpenNest.Data; +using OpenNest.Geometry; + +namespace OpenNest.Tests.Data; + +/// +/// Decision logic of the desktop Clamps dialog: edits one nest's rail in its units, keeps +/// "apply to this nest" (OK) separate from "save as machine defaults" (immediate write). +/// +public class ClampRailEditorTests +{ + private static readonly double[] MachineCenters = { 6, 21, 36, 51, 66, 81, 96, 111 }; + + [Fact] + public void NestWithoutARailStartsEmptyAtTheDefaultClampSize() + { + var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: false); + + Assert.Equal(1.5, editor.Width); + Assert.Equal(0.625, editor.Depth); + Assert.Empty(editor.Centers); + Assert.Null(editor.BuildRail()); + + var metric = new ClampRailEditor(null, Units.Millimeters, applyToNewNests: false); + Assert.Equal(1.5 * 25.4, metric.Width, 9); + } + + [Fact] + public void NestRailIsEditedInPlace() + { + var rail = new ClampRail(1.5, 0.625, MachineCenters); + + var editor = new ClampRailEditor(rail, Units.Inches, applyToNewNests: true); + + Assert.Equal(MachineCenters, editor.Centers); + Assert.Equal(rail, editor.BuildRail()); + Assert.True(editor.ApplyToNewNests); + } + + [Fact] + public void CentersStaySortedAndRefuseInvalidOrDuplicateValues() + { + var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: false); + + Assert.True(editor.TryAddCenter(21, out _)); + Assert.True(editor.TryAddCenter(6, out var none)); + Assert.Null(none); + Assert.False(editor.TryAddCenter(21, out var duplicate)); + Assert.Contains("already", duplicate); + Assert.False(editor.TryAddCenter(-1, out var negative)); + Assert.Contains("origin", negative); + Assert.False(editor.TryAddCenter(double.NaN, out _)); + Assert.Equal(new[] { 6.0, 21 }, editor.Centers); + + editor.RemoveCenterAt(0); + editor.RemoveCenterAt(5); // out of range: ignored + Assert.Equal(new[] { 21.0 }, editor.Centers); + } + + [Fact] + public void ApplyingToTheNestReResolvesEveryPlate() + { + var nest = new Nest(); + nest.PlateDefaults.Size = new Size(48, 120); + var full = nest.CreatePlate(); + var shorter = nest.CreatePlate(); + shorter.Size = new Size(48, 60); + var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: false); + foreach (var center in MachineCenters) + Assert.True(editor.TryAddCenter(center, out _)); + + editor.ApplyTo(nest); + + Assert.Equal(8, full.ClampZones.Count); + Assert.Equal(4, shorter.ClampZones.Count); + Assert.Equal(8, nest.CreatePlate().ClampZones.Count); + + // Removing every clamp and applying removes them from the nest. + while (editor.Centers.Count > 0) + editor.RemoveCenterAt(0); + editor.ApplyTo(nest); + Assert.Null(nest.PlateDefaults.ClampRail); + Assert.All(nest.Plates, p => Assert.Empty(p.ClampZones)); + } + + [Fact] + public void InvalidClampSizeIsRefusedBeforeApplyingOrSaving() + { + var nest = new Nest(); + var plate = nest.CreatePlate(); + var editor = new ClampRailEditor(null, Units.Inches, applyToNewNests: true); + editor.TryAddCenter(6, out _); + editor.Width = 0; + + Assert.Throws(() => editor.ApplyTo(nest)); + Assert.Throws(() => editor.ToMachineDefaults()); + Assert.Empty(plate.ClampZones); + Assert.Null(nest.PlateDefaults.ClampRail); + } + + [Fact] + public void MachineDefaultsCaptureTheEditedValuesInTheNestsUnits() + { + var editor = new ClampRailEditor(null, Units.Millimeters, applyToNewNests: true); + editor.Width = 40; + editor.Depth = 16; + editor.TryAddCenter(150, out _); + + var settings = editor.ToMachineDefaults(); + + Assert.True(settings.Enabled); + Assert.Equal(Units.Millimeters, settings.Units); + Assert.Equal(40, settings.Width); + Assert.Equal(16, settings.Depth); + Assert.Equal(new[] { 150.0 }, settings.Centers); + + // Without any clamp listed, saved defaults never enable clamps for new nests. + editor.RemoveCenterAt(0); + Assert.False(editor.ToMachineDefaults().Enabled); + editor.ApplyToNewNests = false; + editor.TryAddCenter(150, out _); + Assert.False(editor.ToMachineDefaults().Enabled); + } + + [Fact] + public void LoadingMachineDefaultsOnlyFillsTheEditorInTheNestsUnits() + { + var nest = new Nest { Units = Units.Millimeters }; + var plate = nest.CreatePlate(); + var defaults = new ClampSettings + { + Enabled = true, + Units = Units.Inches, + Width = 1.5, + Depth = 0.625, + Centers = new[] { 21.0, 6 }, + }; + var editor = new ClampRailEditor(null, Units.Millimeters, applyToNewNests: false); + + editor.LoadMachineDefaults(defaults); + + Assert.Equal(1.5 * 25.4, editor.Width, 9); + Assert.Equal(0.625 * 25.4, editor.Depth, 9); + Assert.Equal(new[] { 6 * 25.4, 21 * 25.4 }, editor.Centers); + Assert.True(editor.ApplyToNewNests); + Assert.Empty(plate.ClampZones); // nothing applied until OK + Assert.Null(nest.PlateDefaults.ClampRail); + + // Disabled defaults still load their values but do not ask to enable new nests. + defaults.Enabled = false; + editor.LoadMachineDefaults(defaults); + Assert.False(editor.ApplyToNewNests); + Assert.Equal(2, editor.Centers.Count); + } + + [Fact] + public void ConvertedSettingsKeepEnabledAndScaleEveryLength() + { + var inches = new ClampSettings { Enabled = true, Width = 1.5, Depth = 0.625, Centers = new[] { 6.0 } }; + + var metric = inches.ConvertedTo(Units.Millimeters); + + Assert.True(metric.Enabled); + Assert.Equal(Units.Millimeters, metric.Units); + Assert.Equal(38.1, metric.Width, 9); + Assert.Equal(15.875, metric.Depth, 9); + Assert.Equal(152.4, metric.Centers[0], 9); + Assert.Equal(1.5, inches.Width); // the source is not modified + Assert.Equal(1.5, inches.ConvertedTo(Units.Inches).Width); + } +} diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs index a411af9..351ce59 100644 --- a/OpenNest/Controls/PlateRenderer.cs +++ b/OpenNest/Controls/PlateRenderer.cs @@ -168,6 +168,43 @@ namespace OpenNest.Controls DrawAllCutDirectionArrows(g); } + // Machine clamps are fixtures, not material: a hatched amber keep-out drawn over the + // sheet and parts (translucent, so a part placed on a clamp stays visible). They take + // part in no hit test, selection or sequencing. + private static readonly Color ClampHatchColor = Color.FromArgb(150, 70, 80, 90); + private static readonly Color ClampBackColor = Color.FromArgb(70, 230, 170, 40); + private static readonly Color ClampOutlineColor = Color.FromArgb(220, 150, 100, 0); + + public void DrawClampZones(Graphics g) + { + var plate = view.DisplayPlate; + if (plate == null || plate.ClampZones.Count == 0) + return; + + // Zones are in plate coordinates (lower-left sheet corner); move them to the + // sheet's place around the origin, as DrawPlate places the sheet itself. + var left = plate.Quadrant is 2 or 3 ? -plate.Size.Length : 0; + var bottom = plate.Quadrant is 3 or 4 ? -plate.Size.Width : 0; + + using var fill = new HatchBrush(HatchStyle.WideUpwardDiagonal, ClampHatchColor, ClampBackColor); + using var pen = new Pen(ClampOutlineColor, 1.5f); + + foreach (var zone in plate.ClampZones) + { + var box = zone.Bounds.Translate(left, bottom); + var rect = new RectangleF + { + Location = view.PointWorldToGraph(box.Location), + Width = view.LengthWorldToGui(box.Length), + Height = view.LengthWorldToGui(box.Width), + }; + rect.Y -= rect.Height; + + g.FillRectangle(fill, rect); + g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); + } + } + public void DrawCutOffs(Graphics g) { var plate = view.DisplayPlate; diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index d8a5ba0..4a942d2 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -267,6 +267,7 @@ namespace OpenNest.Controls plate.PartAdded -= plate_PartAdded; plate.PartRemoved -= plate_PartRemoved; plate.PartsReordered -= plate_PartsReordered; + plate.ClampZonesChanged -= plate_ClampZonesChanged; parts.Clear(); previewManager.Clear(); selection.Clear(); @@ -277,6 +278,7 @@ namespace OpenNest.Controls plate.PartAdded += plate_PartAdded; plate.PartRemoved += plate_PartRemoved; plate.PartsReordered += plate_PartsReordered; + plate.ClampZonesChanged += plate_ClampZonesChanged; foreach (var part in plate.Parts) parts.Add(LayoutPart.Create(part, this)); @@ -709,6 +711,7 @@ namespace OpenNest.Controls renderer.DrawPlate(e.Graphics); renderer.DrawParts(e.Graphics); + renderer.DrawClampZones(e.Graphics); if (!HasProposalDisplay) { renderer.DrawCutOffs(e.Graphics); @@ -966,6 +969,9 @@ namespace OpenNest.Controls parts.RemoveAll(p => p.BasePart == e.Item); } + // Clamp zones are display-only here: repaint, nothing to select or rebuild. + private void plate_ClampZonesChanged(object sender, EventArgs e) => Invalidate(); + private void plate_PartsReordered(object sender, EventArgs e) { // Part numbers are drawn from the layout order, and an applied cutting plan can also diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index 2d4077f..673104d 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -18,6 +18,7 @@ namespace OpenNest.Forms private readonly BindingList _parts; private Dictionary _groupSettings; private NestDefaults _defaults; + private ClampSettings _clamps; public Form MdiParentForm { get; set; } @@ -30,6 +31,7 @@ namespace OpenNest.Forms dgvParts.DataSource = _parts; _groupSettings = new Dictionary(); _defaults = MainForm.LoadSavedNestDefaults(out _); + _clamps = MainForm.LoadSavedClampSettings(out _); ApplyDefaults(); } @@ -427,7 +429,17 @@ namespace OpenNest.Forms }; } - var result = BomNestBuilder.Build(group, plate, jobName, _defaults.ApplyTo); + var result = BomNestBuilder.Build( + group, + plate, + jobName, + nest => + { + // As New does: saved defaults, then the machine clamps in those units. + _defaults.ApplyTo(nest); + _clamps.ApplyToNewNest(nest); + } + ); importErrors.AddRange(result.Errors); if (result.Nest == null) diff --git a/OpenNest/Forms/ClampsForm.cs b/OpenNest/Forms/ClampsForm.cs new file mode 100644 index 0000000..8143d76 --- /dev/null +++ b/OpenNest/Forms/ClampsForm.cs @@ -0,0 +1,287 @@ +using System; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Windows.Forms; +using OpenNest.Data; + +namespace OpenNest.Forms; + +/// +/// Nest > Clamps...: edits the current nest's machine clamp rail (identical clamps at X +/// centers measured from the plate origin along the origin-side edge). OK applies the rail +/// to every plate of this nest; Cancel discards those edits. Save as Machine Defaults writes +/// clamps.json immediately and is not undone by Cancel. All decisions live in +/// ; this form only renders and forwards input. +/// +public sealed class ClampsForm : Form +{ + private readonly ClampSettings machineDefaults; + private readonly string settingsPath; + private readonly NumericUpDown widthBox; + private readonly NumericUpDown depthBox; + private readonly NumericUpDown centerBox; + private readonly ListBox centersList; + private readonly Button addButton; + private readonly Button removeButton; + private readonly Button okButton; + private readonly CheckBox applyToNewNestsBox; + private readonly Label savedLabel; + + public ClampsForm(ClampRail rail, Units units, ClampSettings machineDefaults, string settingsPath = null) + { + this.machineDefaults = machineDefaults ?? ClampSettings.Fallback; + this.settingsPath = settingsPath ?? ClampSettings.DefaultPath; + Editor = new ClampRailEditor(rail, units, this.machineDefaults.Enabled); + var unit = UnitsHelper.GetShortString(units); + + Text = "Clamps"; + StartPosition = FormStartPosition.CenterParent; + ClientSize = new Size(480, 470); + MinimumSize = new Size(440, 460); + MinimizeBox = false; + MaximizeBox = false; + ShowInTaskbar = false; + FormBorderStyle = FormBorderStyle.Sizable; + + var layout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 4, + Padding = new Padding(10), + }; + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + + layout.Controls.Add( + new Label + { + AutoSize = true, + MaximumSize = new Size(450, 0), + Padding = new Padding(0, 0, 0, 8), + Text = + "Fixed clamps along the plate edge through the origin. Parts and nesting keep " + + "clear of each clamp by the plate's bottom edge spacing. Clamps that do not " + + "reach a shorter plate are left off it.", + } + ); + + var size = new TableLayoutPanel { AutoSize = true, ColumnCount = 3, Dock = DockStyle.Top }; + widthBox = NewLengthBox(Editor.Width, minimum: 0.0001m); + depthBox = NewLengthBox(Editor.Depth, minimum: 0.0001m); + AddRow(size, "Clamp width (along the edge):", widthBox, unit); + AddRow(size, "Clamp depth (into the plate):", depthBox, unit); + layout.Controls.Add(size); + + var centersGroup = new GroupBox + { + Text = $"Clamp centers, measured from the plate origin ({unit})", + Dock = DockStyle.Fill, + Padding = new Padding(8), + }; + centersList = new ListBox { Dock = DockStyle.Fill, IntegralHeight = false }; + var centerEntry = new FlowLayoutPanel + { + Dock = DockStyle.Bottom, + AutoSize = true, + WrapContents = false, + }; + centerBox = NewLengthBox(0, minimum: 0m); + addButton = new Button { Text = "Add", AutoSize = true }; + removeButton = new Button { Text = "Remove", AutoSize = true, Enabled = false }; + centerEntry.Controls.Add(new Label { Text = "Center:", AutoSize = true, Padding = new Padding(0, 6, 0, 0) }); + centerEntry.Controls.Add(centerBox); + centerEntry.Controls.Add(addButton); + centerEntry.Controls.Add(removeButton); + centersGroup.Controls.Add(centersList); + centersGroup.Controls.Add(centerEntry); + layout.Controls.Add(centersGroup); + + var defaultsGroup = new GroupBox + { + Text = "Machine defaults (clamps.json)", + Dock = DockStyle.Top, + AutoSize = true, + Padding = new Padding(8), + }; + var defaultsLayout = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + AutoSize = true, + FlowDirection = FlowDirection.TopDown, + WrapContents = false, + }; + applyToNewNestsBox = new CheckBox + { + Text = "Give new nests these clamps", + AutoSize = true, + Checked = Editor.ApplyToNewNests, + }; + var defaultsButtons = new FlowLayoutPanel { AutoSize = true, WrapContents = false }; + var loadDefaultsButton = new Button { Text = "Load Machine Defaults", AutoSize = true }; + var saveDefaultsButton = new Button { Text = "Save as Machine Defaults", AutoSize = true }; + defaultsButtons.Controls.Add(loadDefaultsButton); + defaultsButtons.Controls.Add(saveDefaultsButton); + savedLabel = new Label + { + AutoSize = true, + MaximumSize = new Size(430, 0), + Text = + "Save as Machine Defaults writes the file immediately; Cancel below does not undo " + + "it. OK and Cancel only affect this nest.", + }; + defaultsLayout.Controls.Add(applyToNewNestsBox); + defaultsLayout.Controls.Add(defaultsButtons); + defaultsLayout.Controls.Add(savedLabel); + defaultsGroup.Controls.Add(defaultsLayout); + layout.Controls.Add(defaultsGroup); + + var buttons = new FlowLayoutPanel + { + Dock = DockStyle.Bottom, + Height = 44, + FlowDirection = FlowDirection.RightToLeft, + Padding = new Padding(6), + }; + var cancel = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel }; + okButton = new Button { Text = "OK", AutoSize = true }; + buttons.Controls.Add(cancel); + buttons.Controls.Add(okButton); + + Controls.Add(layout); + Controls.Add(buttons); + AcceptButton = okButton; + CancelButton = cancel; + + addButton.Click += (_, _) => AddCenter(); + removeButton.Click += (_, _) => RemoveCenter(); + centersList.SelectedIndexChanged += (_, _) => + removeButton.Enabled = centersList.SelectedIndex >= 0; + // Enter in the center box adds that center instead of closing the dialog. + centerBox.Enter += (_, _) => AcceptButton = addButton; + centerBox.Leave += (_, _) => AcceptButton = okButton; + loadDefaultsButton.Click += (_, _) => LoadMachineDefaults(); + saveDefaultsButton.Click += (_, _) => SaveMachineDefaults(); + okButton.Click += (_, _) => Accept(); + + RefreshCenters(); + } + + /// The edited rail; the caller applies it to the nest after OK. + internal ClampRailEditor Editor { get; } + + private static NumericUpDown NewLengthBox(double value, decimal minimum) + { + var box = new NumericUpDown + { + DecimalPlaces = 4, + Minimum = minimum, + Maximum = 100000, + Increment = 0.125m, + Width = 110, + }; + box.Value = ToBoxValue(box, value); + return box; + } + + private static decimal ToBoxValue(NumericUpDown box, double value) + { + if (!double.IsFinite(value) || value < (double)box.Minimum) + return box.Minimum; + return value > (double)box.Maximum ? box.Maximum : (decimal)value; + } + + private static void AddRow(TableLayoutPanel table, string label, Control box, string unit) + { + table.Controls.Add(new Label { Text = label, AutoSize = true, Padding = new Padding(0, 6, 6, 0) }); + table.Controls.Add(box); + table.Controls.Add(new Label { Text = unit, AutoSize = true, Padding = new Padding(0, 6, 0, 0) }); + } + + private void ReadSizes() + { + Editor.Width = (double)widthBox.Value; + Editor.Depth = (double)depthBox.Value; + Editor.ApplyToNewNests = applyToNewNestsBox.Checked; + } + + private void AddCenter() + { + if (!Editor.TryAddCenter((double)centerBox.Value, out var error)) + { + MessageBox.Show(this, error, Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + RefreshCenters(); + centerBox.Select(0, centerBox.Text.Length); + } + + private void RemoveCenter() + { + var index = centersList.SelectedIndex; + Editor.RemoveCenterAt(index); + RefreshCenters(); + if (centersList.Items.Count > 0) + centersList.SelectedIndex = System.Math.Min(index, centersList.Items.Count - 1); + } + + private void RefreshCenters() + { + centersList.BeginUpdate(); + centersList.Items.Clear(); + foreach (var center in Editor.Centers) + centersList.Items.Add(center.ToString("0.####", CultureInfo.CurrentCulture)); + centersList.EndUpdate(); + removeButton.Enabled = centersList.SelectedIndex >= 0; + } + + private void LoadMachineDefaults() + { + Editor.LoadMachineDefaults(machineDefaults); + widthBox.Value = ToBoxValue(widthBox, Editor.Width); + depthBox.Value = ToBoxValue(depthBox, Editor.Depth); + applyToNewNestsBox.Checked = Editor.ApplyToNewNests; + RefreshCenters(); + } + + private void SaveMachineDefaults() + { + ReadSizes(); + try + { + var settings = Editor.ToMachineDefaults(); + settings.Save(settingsPath); + savedLabel.Text = settings.Enabled + ? $"Saved {DateTime.Now:t}: new nests get these {settings.Centers.Count} clamps. Cancel does not undo this." + : $"Saved {DateTime.Now:t}: new nests get no clamps. Cancel does not undo this."; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException) + { + MessageBox.Show( + this, + $"The machine defaults could not be saved:\n{settingsPath}\n\n{ex.Message}", + Text, + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + } + + private void Accept() + { + ReadSizes(); + try + { + Editor.BuildRail(); + } + catch (ArgumentException ex) + { + MessageBox.Show(this, ex.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + DialogResult = DialogResult.OK; + } +} diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index 835f1c4..e6995ad 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -88,6 +88,7 @@ namespace OpenNest.Forms mnuSetRotationIncrement = new System.Windows.Forms.ToolStripMenuItem(); mnuNest = new System.Windows.Forms.ToolStripMenuItem(); mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem(); + mnuNestClamps = new System.Windows.Forms.ToolStripMenuItem(); mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem(); mnuNestShapeLibrary = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem7 = new System.Windows.Forms.ToolStripSeparator(); @@ -573,7 +574,7 @@ namespace OpenNest.Forms // // mnuNest // - mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns }); + mnuNest.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuNestEdit, mnuNestClamps, mnuNestImportDrawing, mnuNestShapeLibrary, toolStripMenuItem7, mnuNestFirstPlate, mnuNestLastPlate, toolStripMenuItem6, mnuNestNextPlate, mnuNestPreviousPlate, toolStripMenuItem12, runAutoNestToolStripMenuItem, autoSequenceAllPlatesToolStripMenuItem, mnuNestRemoveEmptyPlates, mnuNestValidate, mnuNestPost, toolStripMenuItem19, calculateCutTimeToolStripMenuItem, toolStripMenuItem22, mnuNestAutomaticCutOff, mnuNestPlanCutting, mnuNestAssignLeadIns, mnuNestRemoveLeadIns }); mnuNest.Name = "mnuNest"; mnuNest.Size = new System.Drawing.Size(43, 20); mnuNest.Text = "&Nest"; @@ -585,6 +586,13 @@ namespace OpenNest.Forms mnuNestEdit.Text = "Edit"; mnuNestEdit.Click += EditNest_Click; // + // mnuNestClamps + // + mnuNestClamps.Name = "mnuNestClamps"; + mnuNestClamps.Size = new System.Drawing.Size(205, 22); + mnuNestClamps.Text = "Clamps..."; + mnuNestClamps.Click += Clamps_Click; + // // mnuNestImportDrawing // mnuNestImportDrawing.Image = Properties.Resources.import; @@ -1331,6 +1339,7 @@ namespace OpenNest.Forms private System.Windows.Forms.ToolStripMenuItem mnuToolsOptions; private System.Windows.Forms.ToolStripMenuItem mnuNest; private System.Windows.Forms.ToolStripMenuItem mnuNestEdit; + private System.Windows.Forms.ToolStripMenuItem mnuNestClamps; private System.Windows.Forms.ToolStripMenuItem mnuNestImportDrawing; private System.Windows.Forms.ToolStripMenuItem mnuNestShapeLibrary; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem7; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index f046272..b44101f 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -197,9 +197,46 @@ namespace OpenNest.Forms { var nest = new Nest(); LoadNestDefaults().ApplyTo(nest); + // After the defaults so the rail is converted to the new nest's units. + LoadClampSettings().ApplyToNewNest(nest); return nest; } + /// + /// Loads the machine clamp settings (see ); an + /// unreadable or partly invalid file warns once per session, because the new nest's + /// clamps may not match the machine. + /// + private ClampSettings LoadClampSettings() + { + var settings = LoadSavedClampSettings(out var status); + + if ( + status is ClampSettingsStatus.Invalid or ClampSettingsStatus.Partial + && !clampSettingsWarned + ) + { + clampSettingsWarned = true; + var problem = + status == ClampSettingsStatus.Invalid + ? "could not be read, so new nests have no clamps" + : "has invalid values that were ignored, so new nests may be missing clamps"; + MessageBox.Show( + $"The clamp settings file {problem}:\n{ClampSettings.DefaultPath}\n\nCheck the clamps in Nest > Clamps and use Save as Machine Defaults to rewrite the file.", + "Clamps", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + } + return settings; + } + + private bool clampSettingsWarned; + + /// The single desktop entry point for the persisted machine clamp settings. + internal static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) => + ClampSettings.Load(ClampSettings.DefaultPath, out status); + /// /// Loads the persisted nest defaults (see ); /// a corrupt or unreadable file warns once per session. @@ -1437,6 +1474,45 @@ namespace OpenNest.Forms activeForm.ShowNestInfoEditor(); } + private void Clamps_Click(object sender, EventArgs e) + { + var editForm = activeForm; + if (editForm == null) + return; + + var nest = editForm.Nest; + var views = MdiChildren + .OfType() + .Where(form => ReferenceEquals(form.Nest, nest)) + .Select(form => form.PlateView) + .ToArray(); + // Clamps feed nesting and validation: never change them under a running operation. + if ( + nestingInProgress + || Application.OpenForms.OfType().Any() + || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true) + ) + { + MessageBox.Show( + this, + "Finish or cancel the current nesting or plate action first.", + "Clamps", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + return; + } + + var defaults = LoadSavedClampSettings(out _); + using var form = new ClampsForm(nest.PlateDefaults.ClampRail, nest.Units, defaults); + if (form.ShowDialog(this) != DialogResult.OK) + return; + + form.Editor.ApplyTo(nest); + foreach (var view in views) + view.Invalidate(); + } + private void RemoveEmptyPlates_Click(object sender, EventArgs e) { if (activeForm == null) diff --git a/docs/automatic-nesting.md b/docs/automatic-nesting.md index 470049c..96c8612 100644 --- a/docs/automatic-nesting.md +++ b/docs/automatic-nesting.md @@ -40,6 +40,34 @@ maps to the engine that replaced it (see [nesting engines](nesting-engines.md)). If the saved engine is unavailable, Default is selected and the status bar reports the fallback; startup does not replace the saved missing-engine preference. +## Machine edge clamps + +A nest can carry the machine's fixed clamp rail: identical rectangular clamps +(width along the edge, depth into the sheet) at X centers measured from the +plate origin along the origin-side edge. **Nest > Clamps...** edits the current +nest's rail. **OK** applies it to every plate of the nest (**Cancel** discards +the edits); removing every center removes the clamps. **Load Machine Defaults** +only fills the dialog. **Save as Machine Defaults** writes +`%APPDATA%\OpenNest\clamps.json` immediately, with the clamps, their units and +whether new nests receive them; Cancel does not undo that write. The dialog +refuses to open while nesting or a plate action is running. + +Clamps are off by default. Until `clamps.json` is saved with clamps enabled and +at least one center, New and BOM import create nests without clamps exactly as +before. When enabled, a new nest gets the rail converted to its units. A missing +file keeps clamps off; an unreadable file, or one with ignored invalid values +(a bad clamp center is dropped), warns once per session that new nests may not +match the machine. + +Every plate follows the nest's rail at its own size and quadrant: clamps wholly +beyond a shorter sheet are left off it, one still reaching onto the sheet is +clipped to it, and resizing or rotating a plate re-resolves its clamps. Auto Nest +offers each stock size with the rail resolved for that size, and every committed +sheet keeps exactly the clamps its layout was validated against. Clamps are +drawn as hatched amber rectangles over the sheet; they cannot be selected, +moved or sequenced and never count as parts. Interactive fill/push, the overlap +check and cutoffs do not yet avoid clamps. + ## Integration constraints Callers must hold drawings and target state stable from snapshot through attachment. Do not append whole-job placements to an occupied target or flatten multiple returned sheets onto one plate: that would commit a layout different from the one checked. Do not trust arbitrary engine fulfillment metadata as a substitute for counting returned placements. diff --git a/docs/nesting-engines.md b/docs/nesting-engines.md index 3ae9306..44ae3b3 100644 --- a/docs/nesting-engines.md +++ b/docs/nesting-engines.md @@ -90,8 +90,10 @@ grid can make layout validation (including Default's candidate scoring) throw. I static-obstacle preparation does not repair these separate paths. The ordinary clamp fixtures pass for both lanes and Default; this is not an all-input clamp-safety guarantee. -This is whole-job engine support, not interactive fill/push, desktop clamp editing or -cutoff/post-verification support; those consumers are separate work. Other engines and +This is whole-job engine support, not interactive fill/push or cutoff/post-verification +support; those consumers are separate work. Desktop clamp editing and the stock-size +resolution of a nest's clamp rail are described in +[automatic nesting](automatic-nesting.md#machine-edge-clamps). Other engines and plug-ins have not gained clamp-aware placement from this change. ## Repeating completed layouts