Files
OpenNest/OpenNest/Forms/EditNestForm.cs
AJ Isaacs b6bd7eda6e refactor: move CutParameters to OpenNest.Api namespace with new properties
Relocates CutParameters from OpenNest namespace to OpenNest.Api, adds
LeadInLength and PostProcessor properties, and provides a typed Default
factory. Updates Timing.cs, the WinForms project reference, and the three
consuming forms to resolve the type from the new namespace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 08:25:32 -04:00

820 lines
25 KiB
C#

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Api;
using OpenNest.Actions;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Collections;
using OpenNest.Controls;
using OpenNest.Engine;
using OpenNest.Engine.RapidPlanning;
using OpenNest.Engine.Sequencing;
using OpenNest.IO;
using OpenNest.Math;
using OpenNest.Properties;
using Timer = System.Timers.Timer;
namespace OpenNest.Forms
{
public partial class EditNestForm : Form
{
public event EventHandler PlateChanged;
public readonly Nest Nest;
public readonly PlateView PlateView;
private readonly Timer updateDrawingListTimer;
private Panel plateHeaderPanel;
private Label plateInfoLabel;
private Button btnFirstPlate;
private Button btnPreviousPlate;
private Button btnNextPlate;
private Button btnLastPlate;
/// <summary>
/// Used to distinguish between single/double click on drawing within drawinglistbox.
/// If double click, this is set to false so the single click action won't be triggered.
/// </summary>
private bool addPart;
private EditNestForm()
{
PlateView = new PlateView();
PlateView.Enter += PlateView_Enter;
PlateView.PartAdded += PlateView_PartAdded;
PlateView.PartRemoved += PlateView_PartRemoved;
PlateView.Dock = DockStyle.Fill;
InitializeComponent();
CreatePlateHeader();
splitContainer.Panel2.Controls.Add(PlateView);
splitContainer.Panel2.Controls.Add(plateHeaderPanel);
var renderer = new ToolStripRenderer(ToolbarTheme.Toolbar);
toolStrip1.Renderer = renderer;
toolStrip2.Renderer = renderer;
platesListView.SelectedIndexChanged += (sender, e) =>
{
if (platesListView.SelectedIndices.Count == 0)
return;
CurrentPlateIndex = platesListView.SelectedIndices[0];
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged(false);
};
}
private void CreatePlateHeader()
{
plateHeaderPanel = new Panel
{
Dock = DockStyle.Top,
Height = 30,
BackColor = Color.FromArgb(240, 240, 240),
Padding = new Padding(4, 0, 4, 0)
};
plateInfoLabel = new Label
{
AutoSize = true,
TextAlign = ContentAlignment.MiddleLeft,
Font = new Font("Segoe UI", 12f, FontStyle.Bold),
ForeColor = Color.FromArgb(120, 120, 120),
Dock = DockStyle.Left,
Padding = new Padding(4, 4, 4, 4)
};
var btnSize = new System.Drawing.Size(28, 28);
btnFirstPlate = CreateNavButton(Resources.move_first);
btnFirstPlate.Click += (s, e) => LoadFirstPlate();
btnPreviousPlate = CreateNavButton(Resources.move_previous);
btnPreviousPlate.Click += (s, e) => LoadPreviousPlate();
btnNextPlate = CreateNavButton(Resources.move_next);
btnNextPlate.Click += (s, e) => LoadNextPlate();
btnLastPlate = CreateNavButton(Resources.move_last);
btnLastPlate.Click += (s, e) => LoadLastPlate();
// Panel that holds the nav buttons and centers itself in the header
var navPanel = new Panel
{
Width = btnSize.Width * 4,
Height = btnSize.Height,
Anchor = AnchorStyles.None
};
btnFirstPlate.Location = new Point(0, 0);
btnPreviousPlate.Location = new Point(btnSize.Width, 0);
btnNextPlate.Location = new Point(btnSize.Width * 2, 0);
btnLastPlate.Location = new Point(btnSize.Width * 3, 0);
navPanel.Controls.AddRange(new Control[] { btnFirstPlate, btnPreviousPlate, btnNextPlate, btnLastPlate });
var btnRemovePlate = CreateNavButton(Resources.remove);
btnRemovePlate.Dock = DockStyle.Right;
btnRemovePlate.Click += (s, e) => RemoveCurrentPlate();
var btnAddPlate = CreateNavButton(Resources.add);
btnAddPlate.Dock = DockStyle.Right;
btnAddPlate.Click += (s, e) => Nest.CreatePlate();
plateHeaderPanel.Controls.Add(navPanel);
plateHeaderPanel.Controls.Add(plateInfoLabel);
plateHeaderPanel.Controls.Add(btnRemovePlate);
plateHeaderPanel.Controls.Add(btnAddPlate);
// Center the nav panel on resize
CenterNavPanel(navPanel);
plateHeaderPanel.Resize += (s, e) => CenterNavPanel(navPanel);
}
private void CenterNavPanel(Panel navPanel)
{
navPanel.Left = (plateHeaderPanel.Width - navPanel.Width) / 2;
navPanel.Top = (plateHeaderPanel.Height - navPanel.Height) / 2;
}
private static Button CreateNavButton(System.Drawing.Image image)
{
return new Button
{
Image = image,
Size = new System.Drawing.Size(28, 28),
FlatStyle = FlatStyle.Flat,
FlatAppearance = { BorderSize = 0 },
Cursor = Cursors.Hand
};
}
public EditNestForm(Nest nest)
: this()
{
updateDrawingListTimer = new Timer()
{
AutoReset = false,
Enabled = true,
Interval = 50
};
updateDrawingListTimer.Elapsed += drawingListUpdateTimer_Elapsed;
Nest = nest;
Nest.Plates.ItemAdded += Plates_PlateAdded;
Nest.Plates.ItemRemoved += Plates_PlateRemoved;
if (Nest.Plates.Count == 0)
Nest.CreatePlate();
UpdatePlateList();
UpdateDrawingList();
LoadFirstPlate();
Text = Nest.Name;
drawingListBox1.Units = Nest.Units;
}
public string LastSavePath { get; private set; }
public DateTime LastSaveDate { get; private set; }
public int CurrentPlateIndex { get; private set; }
public int PlateCount
{
get { return Nest.Plates.Count; }
}
public void LoadFirstPlate()
{
if (Nest.Plates.Count > 0)
{
CurrentPlateIndex = 0;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
}
}
public void LoadLastPlate()
{
if (Nest.Plates.Count > 0)
{
CurrentPlateIndex = Nest.Plates.Count - 1;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
}
}
public bool LoadNextPlate()
{
if (CurrentPlateIndex + 1 >= Nest.Plates.Count)
return false;
CurrentPlateIndex++;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
return true;
}
public bool LoadPreviousPlate()
{
if (Nest.Plates.Count == 0 || CurrentPlateIndex - 1 < 0)
return false;
CurrentPlateIndex--;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
return true;
}
public bool IsFirstPlate()
{
return (Nest.Plates.Count == 0 || (CurrentPlateIndex - 1) < 0);
}
public bool IsLastPlate()
{
return CurrentPlateIndex + 1 >= Nest.Plates.Count;
}
public void UpdatePlateList()
{
platesListView.Items.Clear();
var items = new ListViewItem[Nest.Plates.Count];
for (int i = 0; i < items.Length; ++i)
{
var plate = Nest.Plates[i];
var item = GetListViewItem(plate, i + 1);
items[i] = item;
}
platesListView.Items.AddRange(items);
}
public void UpdateDrawingList()
{
drawingListBox1.Items.Clear();
foreach (var dwg in Nest.Drawings.OrderBy(d => d.Name).ToList())
drawingListBox1.Items.Add(dwg);
}
public void Save()
{
if (File.Exists(LastSavePath))
SaveAs(LastSavePath);
else
SaveAs();
}
public void SaveAs()
{
var dlg = new SaveFileDialog();
dlg.Filter = $"{NestFormat.FileFilter}|Template File|*.nstdot";
dlg.FileName = Nest.Name;
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
if (dlg.FilterIndex == 2)
SaveTemplate(dlg.FileName);
else
SaveAs(dlg.FileName);
}
}
public void SaveAs(string path)
{
var name = Path.GetFileNameWithoutExtension(path);
Nest.Name = name;
Text = name;
LastSaveDate = DateTime.Now;
LastSavePath = path;
var writer = new NestWriter(Nest);
writer.Write(path);
}
public void SaveTemplate(string path)
{
var nst = new Nest();
nst.Name = Path.GetFileNameWithoutExtension(path);
nst.PlateDefaults = Nest.PlateDefaults;
nst.Units = Nest.Units;
var writer = new NestWriter(nst);
writer.Write(path);
}
public void Import()
{
var dlg = new OpenFileDialog();
dlg.Multiselect = true;
dlg.Filter = "DXF Files (*.dxf) | *.dxf";
if (dlg.ShowDialog() != DialogResult.OK)
return;
var converter = new CadConverterForm();
converter.AddFiles(dlg.FileNames);
var result = converter.ShowDialog();
if (result != DialogResult.OK)
return;
var drawings = converter.GetDrawings();
drawings.ForEach(d => Nest.Drawings.Add(d));
UpdateDrawingList();
tabControl1.SelectedIndex = 1;
}
public bool Export()
{
var dlg = new SaveFileDialog();
dlg.Filter = "DXF file (*.dxf)|*.dxf|" +
"Image as displayed (*.jpg)|*.jpg|" +
"Locations and rotations (*.txt)|*.txt";
dlg.FileName = string.Format("{0}-P{1}", Nest.Name, CurrentPlateIndex + 1);
dlg.AddExtension = true;
dlg.DefaultExt = ".";
if (dlg.ShowDialog() == DialogResult.OK)
{
if (dlg.FilterIndex == 1)
{
var exporter = new DxfExporter();
var success = exporter.ExportPlate(PlateView.Plate, dlg.FileName);
return success;
}
else if (dlg.FilterIndex == 2)
{
try
{
var img = new Bitmap(PlateView.Width, PlateView.Height);
PlateView.DrawToBitmap(img, new Rectangle(0, 0, PlateView.Width, PlateView.Height));
img.Save(dlg.FileName);
}
catch { }
return true;
}
else if (dlg.FilterIndex == 3)
{
StreamWriter writer = null;
try
{
writer = new StreamWriter(dlg.FileName);
foreach (var part in PlateView.Plate.Parts)
{
var pt = part.BaseDrawing.Source.Offset.Rotate(part.Rotation);
writer.WriteLine("{0}|{1},{2}|{3}",
part.BaseDrawing.Source.Path,
System.Math.Round(part.Location.X - pt.X, 8),
System.Math.Round(part.Location.Y - pt.Y, 8),
Angle.ToDegrees(part.Rotation));
}
}
catch { }
finally
{
if (writer != null)
writer.Dispose();
}
}
}
return false;
}
public void ExportAll()
{
LoadFirstPlate();
do
{
if (!Export()) return;
}
while (LoadNextPlate());
}
public void RotateCw()
{
PlateView.Plate.Rotate90(RotationType.CW);
PlateView.ZoomToFit();
}
public void RotateCcw()
{
PlateView.Plate.Rotate90(RotationType.CCW);
PlateView.ZoomToFit();
}
public void Rotate180()
{
PlateView.Plate.Rotate180();
PlateView.ZoomToFit();
}
public void ShowNestInfoEditor()
{
var form = new EditNestInfoForm();
form.LoadNestInfo(Nest);
if (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
form.SaveNestInfo(Nest);
drawingListBox1.Units = Nest.Units;
drawingListBox1.Invalidate();
}
}
public void ResizePlateToFitParts()
{
PlateView.Plate.AutoSize(Settings.Default.AutoSizePlateFactor);
PlateView.ZoomToPlate();
PlateView.Refresh();
UpdatePlateList();
}
public void SelectAllParts()
{
PlateView.SelectAll();
PlateView.Invalidate();
}
public void ToggleRapid()
{
PlateView.DrawRapid = !PlateView.DrawRapid;
PlateView.Invalidate();
}
public void ToggleDrawBounds()
{
PlateView.DrawBounds = !PlateView.DrawBounds;
PlateView.Invalidate();
}
public void ToggleDrawOffset()
{
PlateView.DrawOffset = !PlateView.DrawOffset;
PlateView.Invalidate();
}
public void ToggleFillParts()
{
PlateView.FillParts = !PlateView.FillParts;
PlateView.Invalidate();
}
public void SetCurrentPlateAsNestDefault()
{
Nest.PlateDefaults.SetFromExisting(PlateView.Plate);
}
public void EditPlate()
{
var form = new EditPlateForm(PlateView.Plate);
form.Units = Nest.Units;
if (form.ShowDialog() == DialogResult.OK)
{
PlateView.Invalidate();
FirePlateChanged(false);
UpdatePlateList();
}
Nest.UpdateDrawingQuantities();
drawingListBox1.Refresh();
if (PlateView.Plate.Parts.Count == 0)
Nest.PlateDefaults.SetFromExisting(PlateView.Plate);
}
/// <summary>
/// Opens the current plate as a DXF file by the default program set in Windows.
/// </summary>
public void OpenCurrentPlate()
{
var plate = PlateView.Plate;
var name = string.Format("{0}-P{1}.dxf", Nest.Name, CurrentPlateIndex + 1);
var path = Path.Combine(Path.GetTempPath(), name);
var exporter = new DxfExporter();
exporter.ExportPlate(plate, path);
Process.Start(path);
}
public void RemoveCurrentPlate()
{
if (Nest.Plates.Count < 2)
return;
Nest.Plates.RemoveAt(CurrentPlateIndex);
}
public void AutoSequenceCurrentPlate()
{
SequencePlate(PlateView.Plate);
PlateView.Invalidate();
}
public void AutoSequenceAllPlates()
{
foreach (var plate in Nest.Plates)
SequencePlate(plate);
PlateView.Invalidate();
}
private static void SequencePlate(Plate plate)
{
var parameters = new SequenceParameters { Method = SequenceMethod.LeastCode };
var sequencer = PartSequencerFactory.Create(parameters);
var ordered = sequencer.Sequence(plate.Parts.ToList(), plate);
plate.Parts.Clear();
for (var i = ordered.Count - 1; i >= 0; i--)
plate.Parts.Add(ordered[i].Part);
}
public void CalculateCurrentPlateCutTime()
{
var cutParamsForm = new CutParametersForm();
cutParamsForm.Units = Nest.Units;
if (cutParamsForm.ShowDialog() == DialogResult.OK)
{
var cutparams = cutParamsForm.GetCutParameters();
var info = Timing.GetTimingInfo(PlateView.Plate);
var time = Timing.CalculateTime(info, cutparams);
var timingForm = new TimingForm();
timingForm.Units = Nest.Units;
timingForm.SetCutDistance(info.CutDistance);
timingForm.SetCutTime(time);
timingForm.SetIntersectionCount(info.IntersectionCount);
timingForm.SetPierceCount(info.PierceCount);
timingForm.SetRapidDistance(info.TravelDistance);
timingForm.SetCutParameters(cutparams);
timingForm.ShowDialog();
}
}
public void CalculateNestCutTime()
{
var cutParamsForm = new CutParametersForm();
cutParamsForm.Units = Nest.Units;
if (cutParamsForm.ShowDialog() == DialogResult.OK)
{
var cutparams = cutParamsForm.GetCutParameters();
var info = Timing.GetTimingInfo(Nest);
var time = Timing.CalculateTime(info, cutparams);
var timingForm = new TimingForm();
timingForm.Units = Nest.Units;
timingForm.SetCutDistance(info.CutDistance);
timingForm.SetCutTime(time);
timingForm.SetIntersectionCount(info.IntersectionCount);
timingForm.SetPierceCount(info.PierceCount);
timingForm.SetRapidDistance(info.TravelDistance);
timingForm.SetCutParameters(cutparams);
timingForm.ShowDialog();
}
}
private void FirePlateChanged(bool updateListView = true)
{
if (updateListView)
platesListView.Items[CurrentPlateIndex].Selected = true;
UpdatePlateHeader();
if (PlateChanged != null)
PlateChanged.Invoke(this, EventArgs.Empty);
}
private void UpdatePlateHeader()
{
var plate = Nest.Plates.Count > 0 ? Nest.Plates[CurrentPlateIndex] : null;
if (plate != null)
{
plateInfoLabel.Text = string.Format("Plate {0} of {1} | {2}",
CurrentPlateIndex + 1, Nest.Plates.Count, plate.Size);
}
else
{
plateInfoLabel.Text = "No plates";
}
btnFirstPlate.Enabled = !IsFirstPlate();
btnPreviousPlate.Enabled = !IsFirstPlate();
btnNextPlate.Enabled = !IsLastPlate();
btnLastPlate.Enabled = !IsLastPlate();
}
#region Overrides
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
PlateView.Invalidate();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
Icon = Icon.Clone() as Icon;
PlateView.DrawBounds = Settings.Default.PlateViewDrawBounds;
PlateView.DrawRapid = Settings.Default.PlateViewDrawRapid;
splitContainer.SplitterDistance = Settings.Default.SplitterDistance;
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
Settings.Default.PlateViewDrawBounds = PlateView.DrawBounds;
Settings.Default.PlateViewDrawRapid = PlateView.DrawRapid;
Settings.Default.SplitterDistance = splitContainer.SplitterDistance;
Settings.Default.Save();
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
PlateView.Invalidate();
}
#endregion
#region Plates/Drawings Panel Events
private void AddPlate_Click(object sender, EventArgs e)
{
Nest.CreatePlate();
}
private void EditSelectedPlate_Click(object sender, EventArgs e)
{
if (platesListView.SelectedIndices.Count != 0)
EditPlate();
}
private void RemoveSelectedPlate_Click(object sender, EventArgs e)
{
RemoveCurrentPlate();
}
private void CalculateSelectedPlateCutTime_Click(object sender, EventArgs e)
{
CalculateCurrentPlateCutTime();
}
private void ImportDrawings_Click(object sender, EventArgs e)
{
Import();
}
private void CleanUnusedDrawings_Click(object sender, EventArgs e)
{
var result = MessageBox.Show(
"Remove unused drawings?",
"Clean Drawings",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
if (result == DialogResult.Yes)
{
Nest.Drawings.RemoveWhere(d => d.Quantity.Nested == 0);
UpdateDrawingList();
}
}
#endregion
#region Plate Collection Events
private void Plates_PlateRemoved(object sender, ItemRemovedEventArgs<Plate> e)
{
if (Nest.Plates.Count <= CurrentPlateIndex)
LoadLastPlate();
else
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
UpdatePlateList();
PlateView.ZoomToFit();
}
private void Plates_PlateAdded(object sender, ItemAddedEventArgs<Plate> e)
{
tabControl1.SelectedIndex = 0;
UpdatePlateList();
LoadLastPlate();
PlateView.ZoomToFit();
}
#endregion
private static ListViewItem GetListViewItem(Plate plate, int id)
{
var item = new ListViewItem();
item.Text = id.ToString();
item.SubItems.Add(plate.Size.ToString());
item.SubItems.Add(plate.Quantity.ToString());
return item;
}
private void PlateView_PartRemoved(object sender, ItemRemovedEventArgs<Part> e)
{
updateDrawingListTimer.Stop();
updateDrawingListTimer.Start();
}
private void PlateView_PartAdded(object sender, ItemAddedEventArgs<Part> e)
{
updateDrawingListTimer.Stop();
updateDrawingListTimer.Start();
}
private void drawingListUpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
drawingListBox1.Invoke(new MethodInvoker(() =>
{
drawingListBox1.Refresh();
}));
}
private void drawingListBox1_DoubleClick(object sender, EventArgs e)
{
addPart = false;
var drawing = drawingListBox1.SelectedItem as Drawing;
if (drawing == null)
return;
var form = new EditDrawingForm();
form.LoadDrawing(drawing);
if (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
form.SaveDrawing(drawing);
foreach (var part in PlateView.Parts)
part.Update();
PlateView.Invalidate();
}
}
private void drawingListBox1_Click(object sender, EventArgs e)
{
addPart = true;
}
private void PlateView_Enter(object sender, EventArgs e)
{
if (!addPart)
return;
var drawing = drawingListBox1.SelectedItem as Drawing;
if (drawing == null)
return;
PlateView.SetAction(typeof(ActionClone), drawing);
addPart = false;
}
}
}