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