mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 19:31:55 -04:00
feat(desktop): apply and edit machine clamps on the plate
New and BOM import give a new nest the machine clamp rail from clamps.json when it is enabled, converted to the nest's units; with the default (off) settings nests are created exactly as before. An unreadable or partly invalid settings file warns once per session. Nest > Clamps... edits the current nest's rail: clamp width and depth and a sorted list of X centers with Add/Remove. OK applies the rail to every plate (Cancel discards the edits); Load Machine Defaults only fills the dialog; Save as Machine Defaults writes clamps.json at once and is labelled as not undone by Cancel. The dialog refuses to open while nesting or a plate action runs. Its decisions live in the Linux-tested ClampRailEditor; the form only renders and forwards input. PlateView draws clamp zones as translucent hatched amber rectangles over the sheet and parts and repaints when they change. They take part in no hit test, selection or sequencing.
This commit is contained in:
1 parent
99f64840b2
commit
7069f155f5
11 files changed
+769
-5
No files matched your search
@@ -0,0 +1,119 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Decision logic behind the desktop Clamps dialog: edits one nest's clamp rail in that
|
||||
/// nest's units. Three explicit boundaries: <see cref="LoadMachineDefaults"/> only fills
|
||||
/// the editor, <see cref="ApplyTo"/> changes the nest (the dialog's OK), and
|
||||
/// <see cref="ToMachineDefaults"/> is what "Save as Machine Defaults" writes immediately.
|
||||
/// </summary>
|
||||
public sealed class ClampRailEditor
|
||||
{
|
||||
private readonly List<double> _centers = new();
|
||||
|
||||
/// <summary>Starts from the nest's rail, or the default clamp size with no clamps.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>The nest's units; every value is in these units.</summary>
|
||||
public Units Units { get; }
|
||||
|
||||
/// <summary>Clamp extent along the edge (X).</summary>
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>Clamp intrusion into the sheet (Y).</summary>
|
||||
public double Depth { get; set; }
|
||||
|
||||
/// <summary>Whether saved machine defaults give new nests these clamps.</summary>
|
||||
public bool ApplyToNewNests { get; set; }
|
||||
|
||||
/// <summary>Clamp centers along X from the plate origin, ascending and distinct.</summary>
|
||||
public IReadOnlyList<double> Centers => _centers;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a clamp center, keeping the list sorted. Refuses (with an operator-facing
|
||||
/// reason) values that are not finite, negative, or already listed.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the editor's values with the machine defaults converted to this nest's
|
||||
/// units. Nothing is applied or saved.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The edited rail, or null when no clamp is listed. Throws
|
||||
/// <see cref="ArgumentOutOfRangeException"/> for a non-positive width or depth.
|
||||
/// </summary>
|
||||
public ClampRail? BuildRail() =>
|
||||
_centers.Count == 0 ? null : new ClampRail(Width, Depth, _centers);
|
||||
|
||||
/// <summary>The dialog's OK: makes the edited rail the nest's, re-resolving every plate.</summary>
|
||||
public void ApplyTo(Nest nest)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
nest.SetClampRail(BuildRail());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A copy with width, depth and centers expressed in <paramref name="units"/>
|
||||
/// (inches and millimeters convert at 25.4); <see cref="Enabled"/> is kept.
|
||||
/// </summary>
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<ArgumentOutOfRangeException>(() => editor.ApplyTo(nest));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace OpenNest.Forms
|
||||
private readonly BindingList<BomPartRow> _parts;
|
||||
private Dictionary<string, BomGroupPlateSettings> _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<string, BomGroupPlateSettings>();
|
||||
_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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="ClampRailEditor"/>; this form only renders and forwards input.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>The edited rail; the caller applies it to the nest after OK.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Generated
+10
-1
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the machine clamp settings (see <see cref="LoadSavedClampSettings"/>); an
|
||||
/// unreadable or partly invalid file warns once per session, because the new nest's
|
||||
/// clamps may not match the machine.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>The single desktop entry point for the persisted machine clamp settings.</summary>
|
||||
internal static ClampSettings LoadSavedClampSettings(out ClampSettingsStatus status) =>
|
||||
ClampSettings.Load(ClampSettings.DefaultPath, out status);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the persisted nest defaults (see <see cref="LoadSavedNestDefaults"/>);
|
||||
/// 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<EditNestForm>()
|
||||
.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<NestProgressForm>().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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user