From d87d113ceb941c1c436eb703b0fee29c73a866af Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 17 Oct 2026 20:50:23 -0400 Subject: [PATCH] refactor(ui): split MainForm into per-menu partial files Moves members verbatim into MainForm.File/View/Tools/Nest/Plate/Status partials (nested under MainForm.cs in the project); MainForm.cs keeps the shell: fields, startup, overrides and the Window menu. Each file keeps only the usings it needs. No logic changes; private member names used by reflection-based tests are unchanged. Co-Authored-By: Claude Opus 5.5 --- OpenNest/Forms/MainForm.File.cs | 460 +++++++ OpenNest/Forms/MainForm.Nest.cs | 519 ++++++++ OpenNest/Forms/MainForm.Plate.cs | 229 ++++ OpenNest/Forms/MainForm.Status.cs | 240 ++++ OpenNest/Forms/MainForm.Tools.cs | 300 +++++ OpenNest/Forms/MainForm.View.cs | 200 +++ OpenNest/Forms/MainForm.cs | 1874 ----------------------------- OpenNest/OpenNest.csproj | 4 + 8 files changed, 1952 insertions(+), 1874 deletions(-) create mode 100644 OpenNest/Forms/MainForm.File.cs create mode 100644 OpenNest/Forms/MainForm.Nest.cs create mode 100644 OpenNest/Forms/MainForm.Plate.cs create mode 100644 OpenNest/Forms/MainForm.Status.cs create mode 100644 OpenNest/Forms/MainForm.Tools.cs create mode 100644 OpenNest/Forms/MainForm.View.cs diff --git a/OpenNest/Forms/MainForm.File.cs b/OpenNest/Forms/MainForm.File.cs new file mode 100644 index 00000000..773b6be9 --- /dev/null +++ b/OpenNest/Forms/MainForm.File.cs @@ -0,0 +1,460 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using OpenNest.Data; +using OpenNest.Engine.BestFit; +using OpenNest.IO; +using OpenNest.Properties; + +namespace OpenNest.Forms +{ + public partial class MainForm + { + private Nest CreateDefaultNest() + { + var nest = new Nest(); + LoadNestDefaults().ApplyTo(nest); + // After the defaults so the rail is converted to the new nest's units. + LoadClampSettings(this).ApplyToNewNest(nest); + return nest; + } + + /// + /// Loads the machine clamp settings for creating new nests (New, BOM import); an + /// unreadable or partly invalid file warns once per session, because the new nest's + /// clamps may not match the machine. + /// + internal static ClampSettings LoadClampSettings(IWin32Window owner) + { + 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( + owner, + $"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 static 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. + /// + private NestDefaults LoadNestDefaults() + { + var defaults = LoadSavedNestDefaults(out var status); + + if (status == NestDefaultsStatus.Invalid && !defaultsWarned) + { + defaultsWarned = true; + MessageBox.Show( + $"The nest defaults file could not be read:\n{NestDefaults.DefaultPath}\n\nBuilt-in defaults will be used. Re-save your defaults from Tools > Nest Defaults.", + "Nest Defaults", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + } + return defaults; + } + + private bool defaultsWarned; + + /// + /// The single desktop entry point for persisted nest defaults. Units + /// fall back to the legacy DefaultUnit setting whenever the file is + /// missing or unusable or has no valid unit, so New, BOM import and + /// the defaults dialog always agree. + /// + internal static NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) => + NestDefaults.Load( + NestDefaults.DefaultPath, + Properties.Settings.Default.DefaultUnit, + out status + ); + + private string GetNestName(DateTime date, int id) + { + var year = (date.Year % 100).ToString("D2"); + var seq = ToBase36(id).PadLeft(3, '2'); + + return $"N{year}-{seq}"; + } + + private static string ToBase36(int value) + { + const string chars = "2345679ACDEFGHJKLMNPQRSTUVWXYZ"; + if (value == 0) + return chars[0].ToString(); + + var result = ""; + while (value > 0) + { + result = chars[value % chars.Length] + result; + value /= chars.Length; + } + return result; + } + + private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized, + Guid remoteId = default, string serverUrl = null, int remoteRevision = 0) + { + var editForm = new EditNestForm(nest); + if (remoteId != Guid.Empty) + editForm.Document.BindRemote(remoteId, serverUrl, remoteRevision); + editForm.MdiParent = this; + editForm.PlateView.FilesDropped += (sender, paths) => OpenDroppedNestFiles(paths); + editForm.PlateChanged += (sender, e) => + { + NavigationEnableCheck(); + UpdatePlateStatus(); + mnuPlateRemove.Enabled = activeForm?.PlateManager.CanRemoveCurrent ?? false; + }; + editForm.WindowState = windowState; + editForm.Show(); + editForm.PlateView.ZoomToFit(); + } + + #region File Menu Events + + private void New_Click(object sender, EventArgs e) + { + var windowState = + ActiveMdiChild != null ? ActiveMdiChild.WindowState : FormWindowState.Maximized; + + var nest = CreateDefaultNest(); + + nest.DateCreated = DateTime.Now; + nest.DateLastModified = DateTime.Now; + + if (DateTime.Now.Date != Settings.Default.LastNestCreatedDate.Date) + { + Settings.Default.NestNumber = 1; + Settings.Default.LastNestCreatedDate = DateTime.Now.Date; + } + + nest.Name = GetNestName(DateTime.Now, Settings.Default.NestNumber++); + LoadNest(nest, windowState); + } + + private async void Open_Click(object sender, EventArgs e) + { + if (NestStorage.Settings.Mode == NestStorageMode.File) + { + using var dlg = new OpenFileDialog { Filter = NestFormat.FileFilter, Multiselect = true }; + if (dlg.ShowDialog(this) == DialogResult.OK) + { + var reader = new NestReader(dlg.FileName) { BestFitStore = BestFitCacheStore.Shared }; + var nest = reader.Read(); + LoadNest(nest); + ShowNestWarnings(reader); + } + return; + } + + try + { + var repository = NestStorage.CreateRepository(); + var serverUrl = NestStorage.Settings.ServerUrl; + using var dlg = new SavedNestsForm(repository, serverUrl); + if (dlg.ShowDialog(this) != DialogResult.OK) + return; + + databaseOpenInProgress = true; + storageModeMenu.Enabled = false; + mnuToolsOptions.Enabled = false; + var snapshot = await NestHistorySession.ReadCurrentAsync(repository, dlg.SelectedId, serverUrl); + using var stream = new MemoryStream(snapshot.Archive); + var reader = new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared }; + var nest = reader.Read(); + // Metadata edits create revisions without rewriting the archive. Use their + // current editable fields rather than silently reverting them on the next save. + nest.Name = snapshot.Metadata.Name; + nest.Customer = snapshot.Metadata.Customer; + nest.Notes = snapshot.Metadata.Comments; + nest.Status = snapshot.Metadata.Status; + nest.MadeBy = snapshot.Metadata.MadeBy; + nest.DateLastModified = snapshot.Metadata.DateModified; + LoadNest(nest, remoteId: snapshot.Metadata.Id, serverUrl: snapshot.ServerUrl, + remoteRevision: snapshot.Metadata.Revision); + ShowNestWarnings(reader); + } + catch (Exception ex) + { + ShowStorageError("open", ex); + } + finally + { + databaseOpenInProgress = false; + if (!IsDisposed) + { + storageModeMenu.Enabled = !databaseSaveInProgress; + mnuToolsOptions.Enabled = !databaseSaveInProgress; + } + } + } + + private void ShowNestWarnings(NestReader reader) + { + if (reader.Warnings.Count > 0) + MessageBox.Show(this, string.Join(Environment.NewLine, reader.Warnings), + "Nest Load Warnings", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + + private void ImportBom_Click(object sender, EventArgs e) + { + var form = new BomImportForm(); + form.MdiParentForm = this; + form.ShowDialog(this); + } + + private async void Save_Click(object sender, EventArgs e) + { + var form = activeForm; + if (form == null || databaseSaveInProgress) + return; + if (NestStorage.Settings.Mode == NestStorageMode.File) + form.Save(); + else + await SaveDatabaseAsync(form, saveCopy: false); + } + + private async void SaveAs_Click(object sender, EventArgs e) + { + var form = activeForm; + if (form == null || databaseSaveInProgress) + return; + if (NestStorage.Settings.Mode == NestStorageMode.File) + { + form.SaveAs(); + return; + } + + // A pending upload is reconciled without writing, so only ask for a name + // when this click will actually create the copy. + string copyName = null; + if (!form.Document.PendingCreateRequiresReconciliation) + { + using var dlg = new NestNameForm(form.Nest.Name); + if (dlg.ShowDialog(this) != DialogResult.OK) + return; + copyName = dlg.NestName; + } + await SaveDatabaseAsync(form, saveCopy: true, copyName); + } + + private async Task SaveDatabaseAsync(EditNestForm form, bool saveCopy, string copyName = null) + { + var originalName = form.Nest.Name; + databaseSaveInProgress = true; + storageModeMenu.Enabled = false; + mnuToolsOptions.Enabled = false; + EnableCheck(); + try + { + var serverUrl = NestStorage.Settings.ServerUrl; + var repository = NestStorage.CreateRepository(); + if (form.Document.PendingCreateRequiresReconciliation) + { + var reconciled = await form.Document.ReconcilePendingCreateAsync(repository, serverUrl); + if (!IsDisposed) + statusLabel1.Text = reconciled is null + ? $"No pending nest was found. Review your work, then {(form.Document.PendingCreateWasCopy ? "Save As" : "Save")} again to retry the same ID." + : $"Found saved version {reconciled.Revision}. Review the current nest before saving again."; + return; // Never combine reconciliation with a write in the same click. + } + if (copyName != null) + form.Nest.Name = copyName; + var saved = await form.Document.SaveToDatabaseAsync( + repository, serverUrl, saveCopy); + if (!form.IsDisposed) + form.Text = form.Document.Name; + if (!IsDisposed) + statusLabel1.Text = $"Saved {saved.Name} to nest database"; + } + catch (Exception ex) + { + // Keep the new name only while its copy may still be pending on the server; + // otherwise a later Save would rename the original nest. + if (copyName != null && !form.Document.PendingCreateWasCopy) + form.Nest.Name = originalName; + if (!IsDisposed) + ShowStorageError("save", ex); + } + finally + { + databaseSaveInProgress = false; + if (!IsDisposed) + { + storageModeMenu.Enabled = !databaseOpenInProgress; + mnuToolsOptions.Enabled = !databaseOpenInProgress; + EnableCheck(); + } + } + } + + private void StorageMode_Click(object sender, EventArgs e) + { + using var dlg = new StorageModeForm(); + if (dlg.ShowDialog(this) != DialogResult.OK) + return; + try + { + NestStorage.Save(dlg.SelectedMode, dlg.ServerUrl); + statusLabel1.Text = $"Nest storage: {dlg.SelectedMode}"; + foreach (var form in MdiChildren.OfType()) + form.UpdateDrawingLibraryCommand(); + } + catch (Exception ex) + { + ShowStorageError("change storage mode", ex); + } + } + + private void ExportNest_Click(object sender, EventArgs e) + { + var form = activeForm; + if (form == null) + return; + using var dlg = new SaveFileDialog { Filter = NestFormat.FileFilter, FileName = form.Nest.Name }; + if (dlg.ShowDialog(this) != DialogResult.OK) + return; + try + { + // Export never changes the document's save path, remote id, or nest name. + new NestWriter(form.Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(dlg.FileName); + statusLabel1.Text = $"Exported .nest to {dlg.FileName}"; + } + catch (Exception ex) + { + ShowStorageError("export", ex); + } + } + + private void ShowStorageError(string action, Exception ex) => + MessageBox.Show(this, $"Could not {action} nest: {ex.Message}", + "Nest Storage", MessageBoxButtons.OK, MessageBoxIcon.Error); + + private void Export_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.Export(); + } + + private void ExportAll_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.ExportAll(); + } + + private void ExportNestReport_Click(object sender, EventArgs e) + { + var form = activeForm; + if (form == null) + return; + + // The report never depends on a selected post or NC output; it only needs a + // quiescent nest so the snapshot is coherent. + Func isJobBusy = IsNestJobBusy; + EditNestForm.NestReportTargets targets; + try + { + targets = form.CaptureReportTargets(isJobBusy); + } + catch (Exception ex) when (ex is InvalidOperationException or ArgumentException) + { + ReportFailure(null, ex); + return; + } + + var destination = ShowReportSaveDialog(targets.SuggestedFileName); + if (destination == null) + return; // Cancel: nothing written, job untouched. + + try + { + form.WriteNestReport(targets, destination, isJobBusy); + } + catch (Exception ex) + when (ex is InvalidOperationException + or NotSupportedException + or InvalidDataException + or IOException + or UnauthorizedAccessException + or ArgumentException) + { + // Failures describe the captured target; an active-document switch is irrelevant. + ReportFailure(targets, ex); + return; + } + statusLabel1.Text = $"Saved nest report to {destination}"; + ReportSuccess(destination); + } + + /// + /// True while any operation could mutate nests or hold their plate views: whole-job + /// nesting, an open progress window (a closed one can still await its commit), or a + /// background database save serializing the live nest. + /// + private bool IsNestJobBusy() => + nestingInProgress + || Application.OpenForms.OfType().Any() + || databaseSaveInProgress; + + /// Returns the chosen path, or null on cancel. Overwrite consent is the dialog prompt. + internal virtual string ShowReportSaveDialog(string suggestedFileName) + { + using var dlg = new SaveFileDialog + { + Filter = "PDF report (*.pdf)|*.pdf", + FileName = suggestedFileName, + AddExtension = true, + DefaultExt = ".pdf", + OverwritePrompt = true, + }; + return dlg.ShowDialog(this) == DialogResult.OK ? dlg.FileName : null; + } + + /// Failure notice naming the fixed target; internal so tests observe it without dialogs. + internal virtual void ReportFailure( + EditNestForm.NestReportTargets targets, + Exception error) => + MessageBox.Show(this, + $"Could not export the nest report{(targets == null ? "" : $" for '{targets.Nest.Name}'")}: {error.Message}", + "Export Nest Report", MessageBoxButtons.OK, MessageBoxIcon.Warning); + + /// Success notice; internal so tests observe completion without dialogs. + internal virtual void ReportSuccess(string destination) => + MessageBox.Show(this, $"Nest report saved to:\n\n{destination}", "Export Nest Report", + MessageBoxButtons.OK, MessageBoxIcon.Information); + + private void Exit_Click(object sender, EventArgs e) + { + Close(); + } + + #endregion File Menu Events + } +} diff --git a/OpenNest/Forms/MainForm.Nest.cs b/OpenNest/Forms/MainForm.Nest.cs new file mode 100644 index 00000000..bcb2c648 --- /dev/null +++ b/OpenNest/Forms/MainForm.Nest.cs @@ -0,0 +1,519 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using OpenNest.Actions; +using OpenNest.Collections; +using OpenNest.Controls; +using OpenNest.Diagnostics; +using OpenNest.Engine; +using OpenNest.Engine.Fill; +using OpenNest.Engine.Jobs; + +namespace OpenNest.Forms +{ + public partial class MainForm + { + #region Nest Menu Events + + private void Import_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.Import(); + } + + private void ShapeLibrary_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var form = new ShapeLibraryForm(activeForm.Nest.Drawings.Select(d => d.Name)); + form.ShowDialog(); + + var drawings = form.GetDrawings(); + if (drawings.Count == 0) + return; + + drawings.ForEach(d => activeForm.Nest.Drawings.Add(d)); + activeForm.UpdateDrawingList(); + } + + private void DrawingLibrary_Click(object sender, EventArgs e) + { + activeForm?.AddFromDrawingLibrary(); + } + + private void NestMenu_Opening(object sender, EventArgs e) + { + // The drawing library lives on the nest server; File mode has nothing to browse. + var databaseMode = NestStorage.Settings.IsDatabaseMode; + mnuNest.DropDown.ShowItemToolTips = true; + mnuNestDrawingLibrary.Enabled = activeForm != null && databaseMode; + mnuNestDrawingLibrary.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly; + } + + private void EditNest_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.ShowNestInfoEditor(); + } + + private PlateView[] ViewsOf(Nest nest) => + MdiChildren + .OfType() + .Where(form => ReferenceEquals(form.Nest, nest)) + .Select(form => form.PlateView) + .ToArray(); + + /// + /// True while a nest run or fill may be reading 's clamps. + /// Clamps feed nesting and validation: never change them under a running operation. + /// + private bool ClampsInUse(Nest nest) => + nestingInProgress + || Application.OpenForms.OfType().Any() + || ViewsOf(nest).Any(v => v.IsFillInProgress); + + private void Clamps_Click(object sender, EventArgs e) + { + var editForm = activeForm; + if (editForm == null) + return; + + var nest = editForm.Nest; + var views = ViewsOf(nest); + if (ClampsInUse(nest) || views.Any(v => 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) + return; + activeForm.Nest.Plates.RemoveEmptyPlates(); + } + + private void LoadFirstPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateManager.LoadFirst(); + } + + private void LoadLastPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateManager.LoadLast(); + } + + private void LoadPreviousPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateManager.LoadPrevious(); + } + + private void LoadNextPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateManager.LoadNext(); + } + + private RemnantViewerForm remnantViewer; + + private void ShowRemnants_Click(object sender, EventArgs e) + { + if (activeForm?.PlateView?.Plate == null) + return; + + var plate = activeForm.PlateView.Plate; + + // Minimum remnant dimension = smallest part bbox dimension on the plate. + var minDim = 0.0; + var nest = activeForm.Nest; + if (nest != null) + { + foreach (var drawing in nest.Drawings) + { + var bbox = drawing.Program.BoundingBox(); + var dim = System.Math.Min(bbox.Width, bbox.Length); + if (minDim == 0 || dim < minDim) + minDim = dim; + } + } + + var finder = RemnantFinder.FromPlate(plate); + + if (remnantViewer == null || remnantViewer.IsDisposed) + { + remnantViewer = new RemnantViewerForm(); + remnantViewer.Owner = this; + + // Position next to the main form's right edge. + var screen = Screen.FromControl(this); + remnantViewer.Location = new Point( + System.Math.Min(Right, screen.WorkingArea.Right - remnantViewer.Width), + Top + ); + } + + remnantViewer.LoadRemnants(finder, minDim, activeForm.PlateView); + remnantViewer.Show(); + remnantViewer.BringToFront(); + } + + private async void RunAutoNest_Click(object sender, EventArgs e) + { + var target = activeForm; + if (target == null || target.IsDisposed || target.PlateView.Plate == null) + return; + var views = MdiChildren.OfType() + .Where(f => ReferenceEquals(f.Nest, target.Nest)).Select(f => f.PlateView).ToArray(); + if (nestingInProgress || Application.OpenForms.OfType().Any() + || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true)) + { + MessageBox.Show(this, "Finish or cancel the current plate operation before Auto Nest.", + "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + foreach (var view in views) + view.SetAction(typeof(ActionSelect)); + + using var form = new AutoNestForm(target.Nest); + if (target.Nest.PlateOptions.Count > 0) + form.LoadPlateOptions(target.Nest.PlateOptions, target.Nest.SalvageRate); + if (form.ShowDialog(this) != DialogResult.OK) + return; + + var items = form.GetNestItems(); + if (!items.Any(it => it.Quantity > 0)) + return; + var plateOptions = default(List); + if (form.OptimizePlateSize && !form.TryGetPlateOptions(out plateOptions, out var stockError)) + { + MessageBox.Show(this, stockError, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + var engineName = form.EngineName ?? EngineSelection.EngineName; + EngineSelection.EngineName = engineName; + engineComboBox.SelectedItem = engineName; + using var cts = new CancellationTokenSource(); + nestingCts = cts; + using var progressForm = new NestProgressForm(cts, showPlateRow: true) + { + AllowAccept = false, + HoldOpenUntilCompleted = true, + }; + var receivingProgress = true; + var progress = new Progress(p => + { + if (!receivingProgress || target.IsDisposed || cts.IsCancellationRequested) + return; + progressForm.UpdateProgress(p); + if (p.IsOverallBest) + target.PlateView.SetActiveParts(p.BestParts); + target.PlateView.ActiveWorkArea = p.ActiveWorkArea; + }); + var jobProgress = JobEngineNest.CreateProgress(engineName, progress); + SetNestingLockout(true); + try + { + var request = new NestPipelineRequest(engineName, items, + NestStockBuilder.FromTemplate(target.PlateView.Plate, plateOptions), + new NestJobOptions(maxPlates: 100, + salvageRate: plateOptions?.Count > 0 ? form.SalvageRate : 0, + minimumSalvageDimension: form.MinRemnantSize)); + + async Task SolveAsync() + { + try + { + return await Task.Run(() => NestPipeline.Run(request, jobProgress, cts.Token)); + } + finally + { + receivingProgress = false; + progressForm.ShowCompleted(); + progressForm.Close(); + } + } + + var solve = SolveAsync(); + // Owned modal progress holds drawings/plates stable through solve and cancellation. + if (!solve.IsCompleted) + progressForm.ShowDialog(this); + var result = await solve; + cts.Token.ThrowIfCancellationRequested(); + target.PlateView.ClearPreviewParts(); + target.PlateView.ActiveWorkArea = null; + + var allowInvalid = false; + if (!result.IsValid) + { + using var review = new NestValidationForm(result.Violations, result.CanKeep); + if (review.ShowDialog(this) != DialogResult.OK) + return; + allowInvalid = true; + } + + var costs = result.IsValid && result.CanKeep ? NestCostSummary.FromAccepted(result) : null; + var applied = NestPipelineCommit.ApplyToEmptyPlates(result, target.PlateManager, + allowInvalid, cts.Token); + if (applied.Count > 0) + target.PlateManager.LoadAt(target.Nest.Plates.IndexOf(applied[0])); + if (plateOptions?.Count > 0) + { + target.Nest.PlateOptions = plateOptions; + target.Nest.SalvageRate = form.SalvageRate; + } + target.PlateView.Invalidate(); + if (allowInvalid) + { + target.OverlapDisplay = OverlapDisplayMode.Both; + await target.CheckOverlapsAsync(); + } + // D8/D9: name every clamp Auto Nest pulled as a last resort and its added time. + var removalNotices = applied + .Where(plate => plate.RemovedClamps.Count > 0) + .Select(plate => $"Sheet {target.Nest.Plates.IndexOf(plate) + 1}: remove " + + $"{string.Join(", ", plate.RemovedClamps.OrderBy(id => id, StringComparer.Ordinal))} " + + $"(+{plate.ClampRemovalMinutes:0.#} min)") + .ToList(); + if (removalNotices.Count > 0) + MessageBox.Show(this, + "Auto Nest removed clamps as a last resort so a part that would not " + + "otherwise fit could be nested:\n\n" + string.Join("\n", removalNotices), + "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); + if (costs?.Basis == "supplied-cost") + MessageBox.Show(this, $"Gross sheet cost: {costs.GrossTotal:G}\nEstimated salvage credit: {costs.SalvageCredit:G}\nNet score: {costs.NetScore:G}", + "Auto Nest Cost", MessageBoxButtons.OK, MessageBoxIcon.Information); + // Never drop demand silently: name every unplaced part and why (the drawing + // list keeps showing the remaining quantities after this closes). + if (result.Unplaced.Count > 0) + MessageBox.Show(this, $"{engineName} could not place every part:\n\n" + + string.Join("\n", result.Unplaced.Select(u => u.Describe())), + "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + // Stop discards the entire proposal, including engines that ignore cancellation. + } + catch (Exception ex) + { + MessageBox.Show(this, $"Nesting error: {ex.Message}", "Auto Nest", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + receivingProgress = false; + if (!target.IsDisposed) + { + target.PlateView.ClearPreviewParts(); + target.PlateView.ActiveWorkArea = null; + } + SetNestingLockout(false); + nestingCts = null; + } + } + + private void SequenceAllPlates_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + activeForm.AutoSequenceAllPlates(); + } + + private void ValidateNest_Click(object sender, EventArgs e) + { + var editForm = activeForm; + if (editForm == null) + return; + + var views = MdiChildren.OfType() + .Where(form => ReferenceEquals(form.Nest, editForm.Nest)) + .Select(form => form.PlateView).ToArray(); + if (nestingInProgress || Application.OpenForms.OfType().Any() + || views.Any(view => view.IsFillInProgress || view.Actions.CurrentAction?.IsBusy() == true)) + { + MessageBox.Show(this, "Finish or cancel the current nesting or plate action before validating.", + "Validate Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + foreach (var view in views) + view.SetAction(typeof(ActionSelect)); + editForm.OpenValidationInspection(); + } + + private void PostProcessor_Click(object sender, EventArgs e) + { + var menuItem = (ToolStripMenuItem)sender; + var postProcessor = menuItem.Tag as IPostProcessor; + + var editForm = activeForm; + if (postProcessor == null || editForm == null) + return; + + // Closed progress windows can still have a fill awaiting its final commit. + // Keep the nest stable throughout verification and all owned modal dialogs. + if (MdiChildren.OfType().Any(form => form.CuttingInspection != null)) + { + MessageBox.Show(this, "Close cutting inspection before posting. Posting will run fresh checks.", + "Verify Nest Before Posting", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + var views = MdiChildren.OfType() + .Where(form => ReferenceEquals(form.Nest, editForm.Nest)) + .Select(form => form.PlateView).ToArray(); + if (nestingInProgress || Application.OpenForms.OfType().Any() + || views.Any(view => view.IsFillInProgress || view.Actions.CurrentAction?.IsBusy() == true)) + { + MessageBox.Show(this, "Finish or cancel the current nesting or plate action before posting.", + "Verify Nest Before Posting", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + foreach (var view in views) + view.SetAction(typeof(ActionSelect)); + var nest = editForm.Nest; + + if (postProcessor is IPostProcessorNestAware nestAware) + nestAware.PrepareForNest(nest); + + if (postProcessor is IConfigurablePostProcessor configurable) + { + using var configForm = new PostProcessorConfigForm(configurable); + if (configForm.ShowDialog(this) != DialogResult.OK) + return; + } + + using (var verification = new PostVerificationForm(nest, postProcessor)) + { + if (verification.ShowDialog(this) != DialogResult.OK) + return; + } + + using var dialog = new SaveFileDialog(); + dialog.Filter = "CNC File (*.cnc) | *.cnc"; + dialog.FileName = nest.Name; + + if (dialog.ShowDialog(this) == DialogResult.OK) + { + var path = dialog.FileName; + + if (postProcessor is IMultiFilePostProcessor multiFile) + { + var files = multiFile.GetOutputFiles(nest, path); + if (!ConfirmOverwrite(files, path)) + return; + + try + { + postProcessor.Post(nest, path); + } + catch (Exception ex) + when (ex is InvalidOperationException or IOException or UnauthorizedAccessException) + { + MessageBox.Show( + this, + ex.Message, + postProcessor.Name, + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + return; + } + + if (files.Count > 1) + MessageBox.Show( + this, + $"Saved {files.Count} programs, one per sheet:\n\n" + + string.Join("\n", files.Select(Path.GetFileName)) + + $"\n\nin {Path.GetDirectoryName(path)}", + postProcessor.Name, + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + return; + } + + postProcessor.Post(nest, path); + } + } + + /// + /// The save dialog only checks the chosen name; ask before replacing any + /// other existing file a multi-file post will write. + /// + private bool ConfirmOverwrite(IReadOnlyList files, string chosenPath) + { + var existing = files + .Where(f => File.Exists(f) && !string.Equals(f, chosenPath, StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (existing.Count == 0) + return true; + + var answer = MessageBox.Show( + this, + "These files already exist and will be replaced:\n\n" + + string.Join("\n", existing.Select(Path.GetFileName)) + + "\n\nReplace them?", + "Confirm Save", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning + ); + return answer == DialogResult.Yes; + } + + private void CalculateNestCutTime_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + activeForm.CalculateNestCutTime(); + } + + private void NestAssignLeadIns_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.AssignLeadInsAllPlates(); + } + + private void NestRemoveLeadIns_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.RemoveLeadInsAllPlates(); + } + + #endregion Nest Menu Events + } +} diff --git a/OpenNest/Forms/MainForm.Plate.cs b/OpenNest/Forms/MainForm.Plate.cs new file mode 100644 index 00000000..afd7bde6 --- /dev/null +++ b/OpenNest/Forms/MainForm.Plate.cs @@ -0,0 +1,229 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using OpenNest.Actions; +using OpenNest.Controls; + +namespace OpenNest.Forms +{ + public partial class MainForm + { + #region Plate Menu Events + + private void SetAsNestDefault_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.SetCurrentPlateAsNestDefault(); + } + + private void AddPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.Nest.CreatePlate(); + NavigationEnableCheck(); + } + + private void EditPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.EditPlate(); + } + + private void RemovePlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.RemoveCurrentPlate(); + } + + private void ResizeToFitParts_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.ResizePlateToFitParts(); + UpdatePlateStatus(); + } + + private void RotateCw_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.RotateCw(); + } + + private void RotateCcw_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.RotateCcw(); + } + + private void Rotate180_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.Rotate180(); + } + + private void OpenInExternalCad_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.OpenCurrentPlate(); + } + + private void CalculatePlateCutTime_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + activeForm.CalculateCurrentPlateCutTime(); + } + + private void AutoSequenceCurrentPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + activeForm.AutoSequenceCurrentPlate(); + } + + private void ManualSequenceParts_Click(object sender, EventArgs e) + { + if (activeForm == null || activeForm.PlateView.Plate.Parts.Count < 2) + return; + + activeForm.PlateView.SetAction(typeof(ActionSetSequence)); + } + + private void CutOff_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + activeForm.PlateView.SetAction(typeof(ActionCutOff)); + } + + private void AutomaticCutOff_Click(object sender, EventArgs e) => ShowAutomaticCutOff(allPlates: false); + + private void NestAutomaticCutOff_Click(object sender, EventArgs e) => ShowAutomaticCutOff(allPlates: true); + + private void ShowAutomaticCutOff(bool allPlates) + { + var editForm = activeForm; + var view = editForm?.PlateView; + if (view?.Plate == null) + return; + + var nest = editForm.Nest; + var views = MdiChildren.OfType() + .Where(form => ReferenceEquals(form.Nest, nest)) + .Select(form => form.PlateView).ToArray(); + // A closed progress window can still have a fill awaiting completion/commit. + bool IsBusy() => nestingInProgress || Application.OpenForms.OfType().Any() + || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true); + if (IsBusy()) + { + MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.", + "Automatic Scrap Cutoffs", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + try + { + foreach (var plateView in views) + plateView.SetAction(typeof(ActionSelect)); + using var form = allPlates + ? new AutomaticCutOffForm(view, nest, IsBusy) + : new AutomaticCutOffForm(view, nest.Units, IsBusy); + form.ShowDialog(this); + } + catch (Exception ex) + { + MessageBox.Show(this, ex.Message, "Automatic Scrap Cutoffs", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + if (!view.IsDisposed) + view.ClearPreviewParts(); + } + } + + private void PlatePlanCutting_Click(object sender, EventArgs e) => ShowCuttingPlan(allPlates: false); + + private void NestPlanCutting_Click(object sender, EventArgs e) => ShowCuttingPlan(allPlates: true); + + internal bool IsCuttingPlanBusy(PlateView[] views) => nestingInProgress + || Application.OpenForms.OfType().Any() + || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true); + + // Native tests capture this notice without opening a modal dialog in session zero. + internal System.Action CuttingPlanNotice { get; set; } + + private void ShowCuttingPlanNotice(string message, string caption) + { + if (CuttingPlanNotice != null) + CuttingPlanNotice(message, caption); + else + MessageBox.Show(this, message, caption, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + private void ShowCuttingPlan(bool allPlates) + { + if (MdiChildren.OfType().Any(form => form.CuttingInspection != null && !ReferenceEquals(form, activeForm))) + return; + var editForm = activeForm; + if (editForm?.PlateView?.Plate == null) + return; + + var nest = editForm.Nest; + var views = MdiChildren.OfType() + .Where(form => ReferenceEquals(form.Nest, nest)) + .Select(form => form.PlateView).ToArray(); + // A closed progress window can still have a fill awaiting completion/commit. + if (IsCuttingPlanBusy(views)) + { + ShowCuttingPlanNotice("Finish or cancel the current nesting or plate action first.", "Plan Cutting"); + return; + } + + try + { + foreach (var plateView in views) + plateView.SetAction(typeof(ActionSelect)); + editForm.OpenCuttingInspection(allPlates); + } + catch (Exception ex) + { + MessageBox.Show(this, ex.Message, "Plan Cutting", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void PlateAssignLeadIns_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.AssignLeadIns_Click(sender, e); + } + + private void PlatePlaceLeadIn_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlaceLeadIn_Click(sender, e); + } + + private void PlateRemoveLeadIns_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.RemoveLeadIns_Click(sender, e); + } + + #endregion Plate Menu Events + } +} diff --git a/OpenNest/Forms/MainForm.Status.cs b/OpenNest/Forms/MainForm.Status.cs new file mode 100644 index 00000000..60720be8 --- /dev/null +++ b/OpenNest/Forms/MainForm.Status.cs @@ -0,0 +1,240 @@ +using System; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using OpenNest.Collections; +using OpenNest.Geometry; + +namespace OpenNest.Forms +{ + public partial class MainForm + { + private void EnableCheck() + { + NavigationEnableCheck(); + + var hasValue = activeForm != null; + + mnuFileSave.Enabled = hasValue && !databaseSaveInProgress; + btnSave.Enabled = hasValue && !databaseSaveInProgress; + btnSaveAs.Enabled = hasValue && !databaseSaveInProgress; + mnuFileSaveAs.Enabled = hasValue && !databaseSaveInProgress; + exportNestMenu.Enabled = hasValue; + mnuFileExport.Enabled = hasValue; + mnuFileExportAll.Enabled = hasValue; + mnuFileExportNestReport.Enabled = hasValue && !databaseSaveInProgress; + mnuEdit.Visible = hasValue; + mnuView.Visible = hasValue; + mnuNest.Visible = hasValue; + mnuPlate.Visible = hasValue; + mnuWindow.Visible = hasValue; + mnuToolsMeasureArea.Visible = hasValue; + mnuToolsSaveCurrentAsDefaults.Visible = hasValue; + + toolStripMenuItem14.Visible = hasValue; + mnuSetOffsetIncrement.Visible = hasValue; + mnuSetRotationIncrement.Visible = hasValue; + toolStripMenuItem15.Visible = hasValue; + } + + private void NavigationEnableCheck() + { + if (activeForm == null) + { + mnuNestPreviousPlate.Enabled = false; + mnuNestNextPlate.Enabled = false; + mnuNestFirstPlate.Enabled = false; + mnuNestLastPlate.Enabled = false; + } + else + { + mnuNestPreviousPlate.Enabled = !activeForm.PlateManager.IsFirst; + mnuNestNextPlate.Enabled = !activeForm.PlateManager.IsLast; + mnuNestFirstPlate.Enabled = + activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsFirst; + mnuNestLastPlate.Enabled = + activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsLast; + } + } + + private void SetNestingLockout(bool locked) + { + nestingInProgress = locked; + + // Disable nesting-related menus while running + mnuNest.Enabled = !locked; + mnuPlate.Enabled = !locked; + + // Lock plate navigation + mnuNestPreviousPlate.Enabled = + !locked && activeForm != null && !activeForm.PlateManager.IsFirst; + mnuNestNextPlate.Enabled = + !locked && activeForm != null && !activeForm.PlateManager.IsLast; + mnuNestFirstPlate.Enabled = + !locked + && activeForm != null + && activeForm.PlateManager.Count > 0 + && !activeForm.PlateManager.IsFirst; + mnuNestLastPlate.Enabled = + !locked + && activeForm != null + && activeForm.PlateManager.Count > 0 + && !activeForm.PlateManager.IsLast; + } + + private void UpdateLocationStatus() + { + if (activeForm == null) + { + locationStatusLabel.Text = string.Empty; + return; + } + + locationStatusLabel.Text = string.Format( + "Location: [{0}, {1}]", + activeForm.PlateView.CurrentPoint.X.ToString("n4"), + activeForm.PlateView.CurrentPoint.Y.ToString("n4") + ); + } + + private void UpdatePlateStatus() + { + if (activeForm == null) + { + plateIndexStatusLabel.Text = string.Empty; + plateSizeStatusLabel.Text = string.Empty; + plateQtyStatusLabel.Text = string.Empty; + plateUtilStatusLabel.Text = string.Empty; + return; + } + + plateIndexStatusLabel.Text = string.Format( + "Plate: {0} of {1}", + activeForm.PlateManager.CurrentIndex + 1, + activeForm.PlateManager.Count + ); + + plateSizeStatusLabel.Text = string.Format("Size: {0}", activeForm.PlateView.Plate.Size); + + plateQtyStatusLabel.Text = string.Format( + "Qty: {0}", + activeForm.PlateView.Plate.Quantity + ); + + plateUtilStatusLabel.Text = string.Format( + "Util: {0:P1}", + activeForm.PlateView.Plate.Utilization() + ); + } + + private void UpdateSelectionStatus() + { + if (activeForm == null || activeForm.PlateView.SelectedParts.Count == 0) + { + selectionStatusLabel.Text = string.Empty; + return; + } + + var selected = activeForm.PlateView.SelectedParts; + + if (selected.Count == 1) + { + var box = selected[0].BoundingBox; + selectionStatusLabel.Text = string.Format( + "Selected: [{0}, {1}] {2} x {3}", + box.X.ToString("n4"), + box.Y.ToString("n4"), + box.Width.ToString("n4"), + box.Length.ToString("n4") + ); + } + else + { + var bounds = selected.Select(p => p.BasePart).ToList().GetBoundingBox(); + selectionStatusLabel.Text = string.Format( + "Selected ({0}): [{1}, {2}] {3} x {4}", + selected.Count, + bounds.X.ToString("n4"), + bounds.Y.ToString("n4"), + bounds.Width.ToString("n4"), + bounds.Length.ToString("n4") + ); + } + } + + private void UpdateStatus() + { + UpdateLocationStatus(); + UpdatePlateStatus(); + UpdateSelectionStatus(); + } + + private void EngineComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + if (engineComboBox.SelectedItem is string name) + EngineSelection.EngineName = name; + } + + private void UpdateLocationMode() + { + if (activeForm == null) + return; + + if (clickUpdateLocation) + { + clickUpdateLocation = true; + locationStatusLabel.ForeColor = Color.Gray; + activeForm.PlateView.MouseMove -= PlateView_MouseMove; + activeForm.PlateView.MouseClick += PlateView_MouseClick; + } + else + { + clickUpdateLocation = false; + locationStatusLabel.ForeColor = Color.Black; + activeForm.PlateView.MouseMove += PlateView_MouseMove; + activeForm.PlateView.MouseClick -= PlateView_MouseClick; + } + } + + #region Statusbar Events + + private void LocationStatusLabel_Click(object sender, EventArgs e) + { + clickUpdateLocation = !clickUpdateLocation; + UpdateLocationMode(); + } + + #endregion Statusbar Events + + #region PlateView Events + + private void PlateView_PartAdded(object sender, ItemAddedEventArgs e) => + UpdatePlateStatus(); + + private void PlateView_PartRemoved(object sender, ItemRemovedEventArgs e) => + UpdatePlateStatus(); + + private void PlateView_MouseMove(object sender, MouseEventArgs e) + { + UpdateLocationStatus(); + } + + private void PlateView_MouseClick(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + UpdateLocationStatus(); + } + + private void PlateView_StatusChanged(object sender, EventArgs e) + { + statusLabel1.Text = activeForm.PlateView.Status; + } + + private void PlateView_SelectionChanged(object sender, EventArgs e) + { + UpdateSelectionStatus(); + } + + #endregion PlateView Events + } +} diff --git a/OpenNest/Forms/MainForm.Tools.cs b/OpenNest/Forms/MainForm.Tools.cs new file mode 100644 index 00000000..edc6f02c --- /dev/null +++ b/OpenNest/Forms/MainForm.Tools.cs @@ -0,0 +1,300 @@ +using System; +using System.IO; +using System.Linq; +using System.Windows.Forms; +using OpenNest.Actions; +using OpenNest.Data; +using OpenNest.Geometry; + +namespace OpenNest.Forms +{ + public partial class MainForm + { + #region Tools Menu Events + + private void MeasureArea_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + activeForm.PlateView.SetAction(typeof(ActionSelectArea)); + } + + private void BestFitViewer_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var plate = activeForm.PlateView.Plate; + var drawings = activeForm.Nest.Drawings; + + if (drawings.Count == 0) + { + MessageBox.Show( + "No drawings available.", + "Best-Fit Viewer", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + return; + } + + using (var form = new BestFitViewerForm(drawings, plate, activeForm.Nest.Units)) + { + if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null) + { + var parts = + form.SelectedParts + ?? form.SelectedResult.BuildSourceParts(form.SelectedDrawing); + activeForm.PlateView.SetAction(typeof(ActionClone), parts); + } + } + } + + private void ExpandSpacing_Click(object sender, EventArgs e) + { + var editForm = activeForm; + if (editForm == null) + return; + + // The search reads the plate on a worker, so a nest or fill that can still + // add parts must finish first. + if (IsExpandSpacingBusy(editForm)) + { + ShowExpandSpacingNotice( + "Finish or cancel the current nesting or fill before Expand Spacing." + ); + return; + } + + // Complex layouts can search for minutes; run behind a Cancel window. + var expanded = editForm.PlateView.ExpandSelected(work => + OperationProgressForm.Run( + this, + "Expand Spacing", + "Expanding spacing between the selected parts...", + work + ) + ); + + if (!expanded) + ShowExpandSpacingNotice("Select at least two parts on the plate to expand."); + } + + /// + /// True while whole-job nesting or a fill on any view of the nest can still + /// change its plates. Unlike the other plate commands' guard, a busy current + /// action does not count: ActionSelect reports busy whenever parts are + /// selected, and Expand Spacing needs a selection. + /// + internal bool IsExpandSpacingBusy(EditNestForm editForm) => + nestingInProgress + || Application.OpenForms.OfType().Any() + || MdiChildren + .OfType() + .Where(form => ReferenceEquals(form.Nest, editForm.Nest)) + .Any(form => form.PlateView.IsFillInProgress); + + // Native tests capture this notice without opening a modal dialog in session zero. + internal System.Action ExpandSpacingNotice { get; set; } + + private void ShowExpandSpacingNotice(string message) + { + if (ExpandSpacingNotice != null) + ExpandSpacingNotice(message); + else + MessageBox.Show( + this, + message, + "Expand Spacing", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + } + + private void PatternTile_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + if (activeForm.Nest.Drawings.Count == 0) + { + MessageBox.Show( + "No drawings available.", + "Pattern Tile", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); + return; + } + + using (var form = new PatternTileForm(activeForm.Nest)) + { + if (form.ShowDialog(this) != DialogResult.OK || form.Result == null) + return; + + var result = form.Result; + + if (result.Target == PatternTileTarget.CurrentPlate) + { + activeForm.PlateView.Plate.Parts.Clear(); + + foreach (var part in result.Parts) + activeForm.PlateView.Plate.Parts.Add(part); + + activeForm.PlateView.ZoomToFit(); + } + else + { + var plate = activeForm.Nest.CreatePlate(); + plate.Size = result.PlateSize; + + foreach (var part in result.Parts) + plate.Parts.Add(part); + + activeForm.PlateManager.LoadLast(); + } + + activeForm.Nest.UpdateDrawingQuantities(); + } + } + + private void SetOffsetIncrement_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + var form = new SetValueForm(); + form.Text = "Set Offset Increment"; + form.Value = activeForm.PlateView.OffsetIncrementDistance; + + if (form.ShowDialog() == DialogResult.OK) + activeForm.PlateView.OffsetIncrementDistance = form.Value; + } + + private void SetRotationIncrement_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + var form = new SetValueForm(); + form.Text = "Set Rotation Increment"; + form.Value = activeForm.PlateView.RotateIncrementAngle; + form.Maximum = 360; + + if (form.ShowDialog() == DialogResult.OK) + activeForm.PlateView.RotateIncrementAngle = form.Value; + } + + private void Options_Click(object sender, EventArgs e) + { + var form = new OptionsForm(); + form.ShowDialog(); + } + + private void NestDefaults_Click(object sender, EventArgs e) + { + using (var form = new NestDefaultsForm(LoadSavedNestDefaults(out _))) + { + if (form.ShowDialog(this) == DialogResult.OK) + form.GetDefaults().Save(NestDefaults.DefaultPath); + } + } + + private void SaveCurrentAsDefaults_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + NestDefaults + .FromPlate(activeForm.Nest.Units, activeForm.PlateView.Plate) + .Save(NestDefaults.DefaultPath); + } + + private void MachineConfig_Click(object sender, EventArgs e) + { + var appDataPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "OpenNest", + "Machines" + ); + var provider = new LocalJsonProvider(appDataPath); + provider.EnsureDefaults(); + using (var form = new MachineConfigForm(provider)) + { + form.ShowDialog(this); + } + } + + private void AlignLeft_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.Left); + } + + private void AlignRight_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.Right); + } + + private void AlignTop_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.Top); + } + + private void AlignBottom_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.Bottom); + } + + private void AlignVertical_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.Vertically); + } + + private void AlignHorizontal_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.Horizontally); + } + + private void EvenlySpaceHorizontally_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.EvenlySpaceHorizontally); + } + + private void EvenlySpaceVertically_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.AlignSelected(AlignType.EvenlySpaceVertically); + } + + #endregion Tools Menu Events + + private void centerPartsToolStripMenuItem_Click(object sender, EventArgs e) + { + var plateCenter = this.activeForm.PlateView.Plate.BoundingBox(false).Center; + var partsCenter = this.activeForm.PlateView.Parts.GetBoundingBox().Center; + + var offset = plateCenter - partsCenter; + + foreach (var part in this.activeForm.PlateView.Parts) + { + part.Offset(offset); + } + + activeForm.PlateView.Invalidate(); + } + } +} diff --git a/OpenNest/Forms/MainForm.View.cs b/OpenNest/Forms/MainForm.View.cs new file mode 100644 index 00000000..896be863 --- /dev/null +++ b/OpenNest/Forms/MainForm.View.cs @@ -0,0 +1,200 @@ +using System; +using System.Drawing; +using System.Linq; +using OpenNest.Actions; +using OpenNest.Diagnostics; + +namespace OpenNest.Forms +{ + public partial class MainForm + { + #region Edit Menu Events + + private void EditCut_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var selectedParts = activeForm.PlateView.SelectedParts; + + if (selectedParts.Count > 0) + { + var partsToClone = selectedParts.Select(p => p.BasePart).ToList(); + activeForm.PlateView.SetAction(typeof(ActionClone), partsToClone); + + foreach (var part in partsToClone) + activeForm.PlateView.Plate.Parts.Remove(part); + } + } + + private void EditPaste_Click(object sender, EventArgs e) { } + + private void EditCopy_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var selectedParts = activeForm.PlateView.SelectedParts; + + if (selectedParts.Count > 0) + { + var partsToClone = selectedParts.Select(p => p.BasePart).ToList(); + activeForm.PlateView.SetAction(typeof(ActionClone), partsToClone); + } + } + + private void EditSelectAll_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.SelectAllParts(); + } + + #endregion Edit Menu Events + + #region View Menu Events + + private void OverlapStateChanged(object sender, EventArgs e) => UpdateOverlapMenu(); + private void OverlapMenu_Opening(object sender, EventArgs e) => UpdateOverlapMenu(); + + private void UpdateOverlapMenu() + { + var hasPlate = activeForm != null && !activeForm.IsDisposed && activeForm.PlateView.Plate != null; + mnuViewOverlapCheck.Enabled = hasPlate; + mnuOverlapCheckActive.Enabled = hasPlate; + mnuOverlapCancel.Enabled = hasPlate && activeForm.PlateView.IsOverlapCheckRunning; + mnuOverlapDisplay.Enabled = hasPlate; + mnuOverlapOff.Enabled = hasPlate; + mnuOverlapAreas.Enabled = hasPlate; + mnuOverlapCentroids.Enabled = hasPlate; + mnuOverlapBoth.Enabled = hasPlate; + mnuOverlapOff.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Off; + mnuOverlapAreas.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Areas; + mnuOverlapCentroids.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Centroids; + mnuOverlapBoth.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Both; + } + + private async void CheckOverlaps_Click(object sender, EventArgs e) + { + var form = activeForm; + if (form != null) + await form.CheckOverlapsAsync(); + } + + private void CancelOverlapCheck_Click(object sender, EventArgs e) => activeForm?.CancelOverlapCheck(); + + private void OverlapOff_Click(object sender, EventArgs e) + { + if (activeForm != null) + activeForm.OverlapDisplay = OverlapDisplayMode.Off; + } + + private void OverlapAreas_Click(object sender, EventArgs e) + { + if (activeForm != null) + activeForm.OverlapDisplay = OverlapDisplayMode.Areas; + } + + private void OverlapCentroids_Click(object sender, EventArgs e) + { + if (activeForm != null) + activeForm.OverlapDisplay = OverlapDisplayMode.Centroids; + } + + private void OverlapBoth_Click(object sender, EventArgs e) + { + if (activeForm != null) + activeForm.OverlapDisplay = OverlapDisplayMode.Both; + } + + private void ToggleDrawRapids_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.ToggleRapid(); + mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid; + } + + private void ToggleDrawPiercePoints_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.TogglePiercePoints(); + mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints; + } + + private void ToggleDrawBounds_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.ToggleDrawBounds(); + mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds; + } + + private void ToggleDrawOffset_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.ToggleDrawOffset(); + mnuViewDrawOffset.Checked = activeForm.PlateView.DrawOffset; + } + + private void ToggleDrawCutDirection_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.ToggleCutDirection(); + mnuViewDrawCutDirection.Checked = activeForm.PlateView.DrawCutDirection; + } + + private void ZoomToArea_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.SetAction(typeof(ActionZoomWindow)); + } + + private void ZoomToFit_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.ZoomToFit(); + } + + private void ZoomToPlate_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.ZoomToPlate(); + } + + private void ZoomToSelected_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + activeForm.PlateView.ZoomToSelected(); + } + + private void ZoomIn_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var pt = new Point(activeForm.PlateView.Width / 2, activeForm.PlateView.Height / 2); + + activeForm.PlateView.ZoomToControlPoint(pt, ZoomInFactor); + } + + private void ZoomOut_Click(object sender, EventArgs e) + { + if (activeForm == null) + return; + + var pt = new Point(activeForm.PlateView.Width / 2, activeForm.PlateView.Height / 2); + + activeForm.PlateView.ZoomToControlPoint(pt, ZoomOutFactor); + } + + #endregion View Menu Events + } +} diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 6ae44e57..6aecadca 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -5,20 +5,10 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading; -using System.Threading.Tasks; using System.Windows.Forms; -using OpenNest.Actions; -using OpenNest.Collections; -using OpenNest.Controls; using OpenNest.Data; -using OpenNest.Diagnostics; -using OpenNest.Engine; using OpenNest.Engine.BestFit; -using OpenNest.Engine.Fill; using OpenNest.Engine.Jobs; -using OpenNest.Engine.Jobs.Adapters; -using OpenNest.Engine.Jobs.Placement; -using OpenNest.Geometry; using OpenNest.IO; using OpenNest.Properties; @@ -206,128 +196,6 @@ namespace OpenNest.Forms updateStatusLabel.Visible = true; } - private Nest CreateDefaultNest() - { - var nest = new Nest(); - LoadNestDefaults().ApplyTo(nest); - // After the defaults so the rail is converted to the new nest's units. - LoadClampSettings(this).ApplyToNewNest(nest); - return nest; - } - - /// - /// Loads the machine clamp settings for creating new nests (New, BOM import); an - /// unreadable or partly invalid file warns once per session, because the new nest's - /// clamps may not match the machine. - /// - internal static ClampSettings LoadClampSettings(IWin32Window owner) - { - 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( - owner, - $"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 static 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. - /// - private NestDefaults LoadNestDefaults() - { - var defaults = LoadSavedNestDefaults(out var status); - - if (status == NestDefaultsStatus.Invalid && !defaultsWarned) - { - defaultsWarned = true; - MessageBox.Show( - $"The nest defaults file could not be read:\n{NestDefaults.DefaultPath}\n\nBuilt-in defaults will be used. Re-save your defaults from Tools > Nest Defaults.", - "Nest Defaults", - MessageBoxButtons.OK, - MessageBoxIcon.Warning - ); - } - return defaults; - } - - private bool defaultsWarned; - - /// - /// The single desktop entry point for persisted nest defaults. Units - /// fall back to the legacy DefaultUnit setting whenever the file is - /// missing or unusable or has no valid unit, so New, BOM import and - /// the defaults dialog always agree. - /// - internal static NestDefaults LoadSavedNestDefaults(out NestDefaultsStatus status) => - NestDefaults.Load( - NestDefaults.DefaultPath, - Properties.Settings.Default.DefaultUnit, - out status - ); - - private string GetNestName(DateTime date, int id) - { - var year = (date.Year % 100).ToString("D2"); - var seq = ToBase36(id).PadLeft(3, '2'); - - return $"N{year}-{seq}"; - } - - private static string ToBase36(int value) - { - const string chars = "2345679ACDEFGHJKLMNPQRSTUVWXYZ"; - if (value == 0) - return chars[0].ToString(); - - var result = ""; - while (value > 0) - { - result = chars[value % chars.Length] + result; - value /= chars.Length; - } - return result; - } - - private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized, - Guid remoteId = default, string serverUrl = null, int remoteRevision = 0) - { - var editForm = new EditNestForm(nest); - if (remoteId != Guid.Empty) - editForm.Document.BindRemote(remoteId, serverUrl, remoteRevision); - editForm.MdiParent = this; - editForm.PlateView.FilesDropped += (sender, paths) => OpenDroppedNestFiles(paths); - editForm.PlateChanged += (sender, e) => - { - NavigationEnableCheck(); - UpdatePlateStatus(); - mnuPlateRemove.Enabled = activeForm?.PlateManager.CanRemoveCurrent ?? false; - }; - editForm.WindowState = windowState; - editForm.Show(); - editForm.PlateView.ZoomToFit(); - } private void LoadSettings() { @@ -355,193 +223,6 @@ namespace OpenNest.Forms Settings.Default.Save(); } - private void EnableCheck() - { - NavigationEnableCheck(); - - var hasValue = activeForm != null; - - mnuFileSave.Enabled = hasValue && !databaseSaveInProgress; - btnSave.Enabled = hasValue && !databaseSaveInProgress; - btnSaveAs.Enabled = hasValue && !databaseSaveInProgress; - mnuFileSaveAs.Enabled = hasValue && !databaseSaveInProgress; - exportNestMenu.Enabled = hasValue; - mnuFileExport.Enabled = hasValue; - mnuFileExportAll.Enabled = hasValue; - mnuFileExportNestReport.Enabled = hasValue && !databaseSaveInProgress; - mnuEdit.Visible = hasValue; - mnuView.Visible = hasValue; - mnuNest.Visible = hasValue; - mnuPlate.Visible = hasValue; - mnuWindow.Visible = hasValue; - mnuToolsMeasureArea.Visible = hasValue; - mnuToolsSaveCurrentAsDefaults.Visible = hasValue; - - toolStripMenuItem14.Visible = hasValue; - mnuSetOffsetIncrement.Visible = hasValue; - mnuSetRotationIncrement.Visible = hasValue; - toolStripMenuItem15.Visible = hasValue; - } - - private void NavigationEnableCheck() - { - if (activeForm == null) - { - mnuNestPreviousPlate.Enabled = false; - mnuNestNextPlate.Enabled = false; - mnuNestFirstPlate.Enabled = false; - mnuNestLastPlate.Enabled = false; - } - else - { - mnuNestPreviousPlate.Enabled = !activeForm.PlateManager.IsFirst; - mnuNestNextPlate.Enabled = !activeForm.PlateManager.IsLast; - mnuNestFirstPlate.Enabled = - activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsFirst; - mnuNestLastPlate.Enabled = - activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsLast; - } - } - - private void SetNestingLockout(bool locked) - { - nestingInProgress = locked; - - // Disable nesting-related menus while running - mnuNest.Enabled = !locked; - mnuPlate.Enabled = !locked; - - // Lock plate navigation - mnuNestPreviousPlate.Enabled = - !locked && activeForm != null && !activeForm.PlateManager.IsFirst; - mnuNestNextPlate.Enabled = - !locked && activeForm != null && !activeForm.PlateManager.IsLast; - mnuNestFirstPlate.Enabled = - !locked - && activeForm != null - && activeForm.PlateManager.Count > 0 - && !activeForm.PlateManager.IsFirst; - mnuNestLastPlate.Enabled = - !locked - && activeForm != null - && activeForm.PlateManager.Count > 0 - && !activeForm.PlateManager.IsLast; - } - - private void UpdateLocationStatus() - { - if (activeForm == null) - { - locationStatusLabel.Text = string.Empty; - return; - } - - locationStatusLabel.Text = string.Format( - "Location: [{0}, {1}]", - activeForm.PlateView.CurrentPoint.X.ToString("n4"), - activeForm.PlateView.CurrentPoint.Y.ToString("n4") - ); - } - - private void UpdatePlateStatus() - { - if (activeForm == null) - { - plateIndexStatusLabel.Text = string.Empty; - plateSizeStatusLabel.Text = string.Empty; - plateQtyStatusLabel.Text = string.Empty; - plateUtilStatusLabel.Text = string.Empty; - return; - } - - plateIndexStatusLabel.Text = string.Format( - "Plate: {0} of {1}", - activeForm.PlateManager.CurrentIndex + 1, - activeForm.PlateManager.Count - ); - - plateSizeStatusLabel.Text = string.Format("Size: {0}", activeForm.PlateView.Plate.Size); - - plateQtyStatusLabel.Text = string.Format( - "Qty: {0}", - activeForm.PlateView.Plate.Quantity - ); - - plateUtilStatusLabel.Text = string.Format( - "Util: {0:P1}", - activeForm.PlateView.Plate.Utilization() - ); - } - - private void UpdateSelectionStatus() - { - if (activeForm == null || activeForm.PlateView.SelectedParts.Count == 0) - { - selectionStatusLabel.Text = string.Empty; - return; - } - - var selected = activeForm.PlateView.SelectedParts; - - if (selected.Count == 1) - { - var box = selected[0].BoundingBox; - selectionStatusLabel.Text = string.Format( - "Selected: [{0}, {1}] {2} x {3}", - box.X.ToString("n4"), - box.Y.ToString("n4"), - box.Width.ToString("n4"), - box.Length.ToString("n4") - ); - } - else - { - var bounds = selected.Select(p => p.BasePart).ToList().GetBoundingBox(); - selectionStatusLabel.Text = string.Format( - "Selected ({0}): [{1}, {2}] {3} x {4}", - selected.Count, - bounds.X.ToString("n4"), - bounds.Y.ToString("n4"), - bounds.Width.ToString("n4"), - bounds.Length.ToString("n4") - ); - } - } - - private void UpdateStatus() - { - UpdateLocationStatus(); - UpdatePlateStatus(); - UpdateSelectionStatus(); - } - - private void EngineComboBox_SelectedIndexChanged(object sender, EventArgs e) - { - if (engineComboBox.SelectedItem is string name) - EngineSelection.EngineName = name; - } - - private void UpdateLocationMode() - { - if (activeForm == null) - return; - - if (clickUpdateLocation) - { - clickUpdateLocation = true; - locationStatusLabel.ForeColor = Color.Gray; - activeForm.PlateView.MouseMove -= PlateView_MouseMove; - activeForm.PlateView.MouseClick += PlateView_MouseClick; - } - else - { - clickUpdateLocation = false; - locationStatusLabel.ForeColor = Color.Black; - activeForm.PlateView.MouseMove += PlateView_MouseMove; - activeForm.PlateView.MouseClick -= PlateView_MouseClick; - } - } - private static ToolStripButton CreateActionButton(string name, string text, Image image, EventHandler click) { @@ -752,1505 +433,6 @@ namespace OpenNest.Forms #endregion - #region File Menu Events - - private void New_Click(object sender, EventArgs e) - { - var windowState = - ActiveMdiChild != null ? ActiveMdiChild.WindowState : FormWindowState.Maximized; - - var nest = CreateDefaultNest(); - - nest.DateCreated = DateTime.Now; - nest.DateLastModified = DateTime.Now; - - if (DateTime.Now.Date != Settings.Default.LastNestCreatedDate.Date) - { - Settings.Default.NestNumber = 1; - Settings.Default.LastNestCreatedDate = DateTime.Now.Date; - } - - nest.Name = GetNestName(DateTime.Now, Settings.Default.NestNumber++); - LoadNest(nest, windowState); - } - - private async void Open_Click(object sender, EventArgs e) - { - if (NestStorage.Settings.Mode == NestStorageMode.File) - { - using var dlg = new OpenFileDialog { Filter = NestFormat.FileFilter, Multiselect = true }; - if (dlg.ShowDialog(this) == DialogResult.OK) - { - var reader = new NestReader(dlg.FileName) { BestFitStore = BestFitCacheStore.Shared }; - var nest = reader.Read(); - LoadNest(nest); - ShowNestWarnings(reader); - } - return; - } - - try - { - var repository = NestStorage.CreateRepository(); - var serverUrl = NestStorage.Settings.ServerUrl; - using var dlg = new SavedNestsForm(repository, serverUrl); - if (dlg.ShowDialog(this) != DialogResult.OK) - return; - - databaseOpenInProgress = true; - storageModeMenu.Enabled = false; - mnuToolsOptions.Enabled = false; - var snapshot = await NestHistorySession.ReadCurrentAsync(repository, dlg.SelectedId, serverUrl); - using var stream = new MemoryStream(snapshot.Archive); - var reader = new NestReader(stream) { BestFitStore = BestFitCacheStore.Shared }; - var nest = reader.Read(); - // Metadata edits create revisions without rewriting the archive. Use their - // current editable fields rather than silently reverting them on the next save. - nest.Name = snapshot.Metadata.Name; - nest.Customer = snapshot.Metadata.Customer; - nest.Notes = snapshot.Metadata.Comments; - nest.Status = snapshot.Metadata.Status; - nest.MadeBy = snapshot.Metadata.MadeBy; - nest.DateLastModified = snapshot.Metadata.DateModified; - LoadNest(nest, remoteId: snapshot.Metadata.Id, serverUrl: snapshot.ServerUrl, - remoteRevision: snapshot.Metadata.Revision); - ShowNestWarnings(reader); - } - catch (Exception ex) - { - ShowStorageError("open", ex); - } - finally - { - databaseOpenInProgress = false; - if (!IsDisposed) - { - storageModeMenu.Enabled = !databaseSaveInProgress; - mnuToolsOptions.Enabled = !databaseSaveInProgress; - } - } - } - - private void ShowNestWarnings(NestReader reader) - { - if (reader.Warnings.Count > 0) - MessageBox.Show(this, string.Join(Environment.NewLine, reader.Warnings), - "Nest Load Warnings", MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - - private void ImportBom_Click(object sender, EventArgs e) - { - var form = new BomImportForm(); - form.MdiParentForm = this; - form.ShowDialog(this); - } - - private async void Save_Click(object sender, EventArgs e) - { - var form = activeForm; - if (form == null || databaseSaveInProgress) - return; - if (NestStorage.Settings.Mode == NestStorageMode.File) - form.Save(); - else - await SaveDatabaseAsync(form, saveCopy: false); - } - - private async void SaveAs_Click(object sender, EventArgs e) - { - var form = activeForm; - if (form == null || databaseSaveInProgress) - return; - if (NestStorage.Settings.Mode == NestStorageMode.File) - { - form.SaveAs(); - return; - } - - // A pending upload is reconciled without writing, so only ask for a name - // when this click will actually create the copy. - string copyName = null; - if (!form.Document.PendingCreateRequiresReconciliation) - { - using var dlg = new NestNameForm(form.Nest.Name); - if (dlg.ShowDialog(this) != DialogResult.OK) - return; - copyName = dlg.NestName; - } - await SaveDatabaseAsync(form, saveCopy: true, copyName); - } - - private async Task SaveDatabaseAsync(EditNestForm form, bool saveCopy, string copyName = null) - { - var originalName = form.Nest.Name; - databaseSaveInProgress = true; - storageModeMenu.Enabled = false; - mnuToolsOptions.Enabled = false; - EnableCheck(); - try - { - var serverUrl = NestStorage.Settings.ServerUrl; - var repository = NestStorage.CreateRepository(); - if (form.Document.PendingCreateRequiresReconciliation) - { - var reconciled = await form.Document.ReconcilePendingCreateAsync(repository, serverUrl); - if (!IsDisposed) - statusLabel1.Text = reconciled is null - ? $"No pending nest was found. Review your work, then {(form.Document.PendingCreateWasCopy ? "Save As" : "Save")} again to retry the same ID." - : $"Found saved version {reconciled.Revision}. Review the current nest before saving again."; - return; // Never combine reconciliation with a write in the same click. - } - if (copyName != null) - form.Nest.Name = copyName; - var saved = await form.Document.SaveToDatabaseAsync( - repository, serverUrl, saveCopy); - if (!form.IsDisposed) - form.Text = form.Document.Name; - if (!IsDisposed) - statusLabel1.Text = $"Saved {saved.Name} to nest database"; - } - catch (Exception ex) - { - // Keep the new name only while its copy may still be pending on the server; - // otherwise a later Save would rename the original nest. - if (copyName != null && !form.Document.PendingCreateWasCopy) - form.Nest.Name = originalName; - if (!IsDisposed) - ShowStorageError("save", ex); - } - finally - { - databaseSaveInProgress = false; - if (!IsDisposed) - { - storageModeMenu.Enabled = !databaseOpenInProgress; - mnuToolsOptions.Enabled = !databaseOpenInProgress; - EnableCheck(); - } - } - } - - private void StorageMode_Click(object sender, EventArgs e) - { - using var dlg = new StorageModeForm(); - if (dlg.ShowDialog(this) != DialogResult.OK) - return; - try - { - NestStorage.Save(dlg.SelectedMode, dlg.ServerUrl); - statusLabel1.Text = $"Nest storage: {dlg.SelectedMode}"; - foreach (var form in MdiChildren.OfType()) - form.UpdateDrawingLibraryCommand(); - } - catch (Exception ex) - { - ShowStorageError("change storage mode", ex); - } - } - - private void ExportNest_Click(object sender, EventArgs e) - { - var form = activeForm; - if (form == null) - return; - using var dlg = new SaveFileDialog { Filter = NestFormat.FileFilter, FileName = form.Nest.Name }; - if (dlg.ShowDialog(this) != DialogResult.OK) - return; - try - { - // Export never changes the document's save path, remote id, or nest name. - new NestWriter(form.Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(dlg.FileName); - statusLabel1.Text = $"Exported .nest to {dlg.FileName}"; - } - catch (Exception ex) - { - ShowStorageError("export", ex); - } - } - - private void ShowStorageError(string action, Exception ex) => - MessageBox.Show(this, $"Could not {action} nest: {ex.Message}", - "Nest Storage", MessageBoxButtons.OK, MessageBoxIcon.Error); - - private void Export_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.Export(); - } - - private void ExportAll_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.ExportAll(); - } - - private void ExportNestReport_Click(object sender, EventArgs e) - { - var form = activeForm; - if (form == null) - return; - - // The report never depends on a selected post or NC output; it only needs a - // quiescent nest so the snapshot is coherent. - Func isJobBusy = IsNestJobBusy; - EditNestForm.NestReportTargets targets; - try - { - targets = form.CaptureReportTargets(isJobBusy); - } - catch (Exception ex) when (ex is InvalidOperationException or ArgumentException) - { - ReportFailure(null, ex); - return; - } - - var destination = ShowReportSaveDialog(targets.SuggestedFileName); - if (destination == null) - return; // Cancel: nothing written, job untouched. - - try - { - form.WriteNestReport(targets, destination, isJobBusy); - } - catch (Exception ex) - when (ex is InvalidOperationException - or NotSupportedException - or InvalidDataException - or IOException - or UnauthorizedAccessException - or ArgumentException) - { - // Failures describe the captured target; an active-document switch is irrelevant. - ReportFailure(targets, ex); - return; - } - statusLabel1.Text = $"Saved nest report to {destination}"; - ReportSuccess(destination); - } - - /// - /// True while any operation could mutate nests or hold their plate views: whole-job - /// nesting, an open progress window (a closed one can still await its commit), or a - /// background database save serializing the live nest. - /// - private bool IsNestJobBusy() => - nestingInProgress - || Application.OpenForms.OfType().Any() - || databaseSaveInProgress; - - /// Returns the chosen path, or null on cancel. Overwrite consent is the dialog prompt. - internal virtual string ShowReportSaveDialog(string suggestedFileName) - { - using var dlg = new SaveFileDialog - { - Filter = "PDF report (*.pdf)|*.pdf", - FileName = suggestedFileName, - AddExtension = true, - DefaultExt = ".pdf", - OverwritePrompt = true, - }; - return dlg.ShowDialog(this) == DialogResult.OK ? dlg.FileName : null; - } - - /// Failure notice naming the fixed target; internal so tests observe it without dialogs. - internal virtual void ReportFailure( - EditNestForm.NestReportTargets targets, - Exception error) => - MessageBox.Show(this, - $"Could not export the nest report{(targets == null ? "" : $" for '{targets.Nest.Name}'")}: {error.Message}", - "Export Nest Report", MessageBoxButtons.OK, MessageBoxIcon.Warning); - - /// Success notice; internal so tests observe completion without dialogs. - internal virtual void ReportSuccess(string destination) => - MessageBox.Show(this, $"Nest report saved to:\n\n{destination}", "Export Nest Report", - MessageBoxButtons.OK, MessageBoxIcon.Information); - - private void Exit_Click(object sender, EventArgs e) - { - Close(); - } - - #endregion File Menu Events - - #region Edit Menu Events - - private void EditCut_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - var selectedParts = activeForm.PlateView.SelectedParts; - - if (selectedParts.Count > 0) - { - var partsToClone = selectedParts.Select(p => p.BasePart).ToList(); - activeForm.PlateView.SetAction(typeof(ActionClone), partsToClone); - - foreach (var part in partsToClone) - activeForm.PlateView.Plate.Parts.Remove(part); - } - } - - private void EditPaste_Click(object sender, EventArgs e) { } - - private void EditCopy_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - var selectedParts = activeForm.PlateView.SelectedParts; - - if (selectedParts.Count > 0) - { - var partsToClone = selectedParts.Select(p => p.BasePart).ToList(); - activeForm.PlateView.SetAction(typeof(ActionClone), partsToClone); - } - } - - private void EditSelectAll_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.SelectAllParts(); - } - - #endregion Edit Menu Events - - #region View Menu Events - - private void OverlapStateChanged(object sender, EventArgs e) => UpdateOverlapMenu(); - private void OverlapMenu_Opening(object sender, EventArgs e) => UpdateOverlapMenu(); - - private void UpdateOverlapMenu() - { - var hasPlate = activeForm != null && !activeForm.IsDisposed && activeForm.PlateView.Plate != null; - mnuViewOverlapCheck.Enabled = hasPlate; - mnuOverlapCheckActive.Enabled = hasPlate; - mnuOverlapCancel.Enabled = hasPlate && activeForm.PlateView.IsOverlapCheckRunning; - mnuOverlapDisplay.Enabled = hasPlate; - mnuOverlapOff.Enabled = hasPlate; - mnuOverlapAreas.Enabled = hasPlate; - mnuOverlapCentroids.Enabled = hasPlate; - mnuOverlapBoth.Enabled = hasPlate; - mnuOverlapOff.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Off; - mnuOverlapAreas.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Areas; - mnuOverlapCentroids.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Centroids; - mnuOverlapBoth.Checked = hasPlate && activeForm.OverlapDisplay == OverlapDisplayMode.Both; - } - - private async void CheckOverlaps_Click(object sender, EventArgs e) - { - var form = activeForm; - if (form != null) - await form.CheckOverlapsAsync(); - } - - private void CancelOverlapCheck_Click(object sender, EventArgs e) => activeForm?.CancelOverlapCheck(); - - private void OverlapOff_Click(object sender, EventArgs e) - { - if (activeForm != null) - activeForm.OverlapDisplay = OverlapDisplayMode.Off; - } - - private void OverlapAreas_Click(object sender, EventArgs e) - { - if (activeForm != null) - activeForm.OverlapDisplay = OverlapDisplayMode.Areas; - } - - private void OverlapCentroids_Click(object sender, EventArgs e) - { - if (activeForm != null) - activeForm.OverlapDisplay = OverlapDisplayMode.Centroids; - } - - private void OverlapBoth_Click(object sender, EventArgs e) - { - if (activeForm != null) - activeForm.OverlapDisplay = OverlapDisplayMode.Both; - } - - private void ToggleDrawRapids_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.ToggleRapid(); - mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid; - } - - private void ToggleDrawPiercePoints_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.TogglePiercePoints(); - mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints; - } - - private void ToggleDrawBounds_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.ToggleDrawBounds(); - mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds; - } - - private void ToggleDrawOffset_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.ToggleDrawOffset(); - mnuViewDrawOffset.Checked = activeForm.PlateView.DrawOffset; - } - - private void ToggleDrawCutDirection_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.ToggleCutDirection(); - mnuViewDrawCutDirection.Checked = activeForm.PlateView.DrawCutDirection; - } - - private void ZoomToArea_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.SetAction(typeof(ActionZoomWindow)); - } - - private void ZoomToFit_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.ZoomToFit(); - } - - private void ZoomToPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.ZoomToPlate(); - } - - private void ZoomToSelected_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.ZoomToSelected(); - } - - private void ZoomIn_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - var pt = new Point(activeForm.PlateView.Width / 2, activeForm.PlateView.Height / 2); - - activeForm.PlateView.ZoomToControlPoint(pt, ZoomInFactor); - } - - private void ZoomOut_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - var pt = new Point(activeForm.PlateView.Width / 2, activeForm.PlateView.Height / 2); - - activeForm.PlateView.ZoomToControlPoint(pt, ZoomOutFactor); - } - - #endregion View Menu Events - - #region Tools Menu Events - - private void MeasureArea_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - activeForm.PlateView.SetAction(typeof(ActionSelectArea)); - } - - private void BestFitViewer_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - var plate = activeForm.PlateView.Plate; - var drawings = activeForm.Nest.Drawings; - - if (drawings.Count == 0) - { - MessageBox.Show( - "No drawings available.", - "Best-Fit Viewer", - MessageBoxButtons.OK, - MessageBoxIcon.Information - ); - return; - } - - using (var form = new BestFitViewerForm(drawings, plate, activeForm.Nest.Units)) - { - if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null) - { - var parts = - form.SelectedParts - ?? form.SelectedResult.BuildSourceParts(form.SelectedDrawing); - activeForm.PlateView.SetAction(typeof(ActionClone), parts); - } - } - } - - private void ExpandSpacing_Click(object sender, EventArgs e) - { - var editForm = activeForm; - if (editForm == null) - return; - - // The search reads the plate on a worker, so a nest or fill that can still - // add parts must finish first. - if (IsExpandSpacingBusy(editForm)) - { - ShowExpandSpacingNotice( - "Finish or cancel the current nesting or fill before Expand Spacing." - ); - return; - } - - // Complex layouts can search for minutes; run behind a Cancel window. - var expanded = editForm.PlateView.ExpandSelected(work => - OperationProgressForm.Run( - this, - "Expand Spacing", - "Expanding spacing between the selected parts...", - work - ) - ); - - if (!expanded) - ShowExpandSpacingNotice("Select at least two parts on the plate to expand."); - } - - /// - /// True while whole-job nesting or a fill on any view of the nest can still - /// change its plates. Unlike the other plate commands' guard, a busy current - /// action does not count: ActionSelect reports busy whenever parts are - /// selected, and Expand Spacing needs a selection. - /// - internal bool IsExpandSpacingBusy(EditNestForm editForm) => - nestingInProgress - || Application.OpenForms.OfType().Any() - || MdiChildren - .OfType() - .Where(form => ReferenceEquals(form.Nest, editForm.Nest)) - .Any(form => form.PlateView.IsFillInProgress); - - // Native tests capture this notice without opening a modal dialog in session zero. - internal System.Action ExpandSpacingNotice { get; set; } - - private void ShowExpandSpacingNotice(string message) - { - if (ExpandSpacingNotice != null) - ExpandSpacingNotice(message); - else - MessageBox.Show( - this, - message, - "Expand Spacing", - MessageBoxButtons.OK, - MessageBoxIcon.Information - ); - } - - private void PatternTile_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - if (activeForm.Nest.Drawings.Count == 0) - { - MessageBox.Show( - "No drawings available.", - "Pattern Tile", - MessageBoxButtons.OK, - MessageBoxIcon.Information - ); - return; - } - - using (var form = new PatternTileForm(activeForm.Nest)) - { - if (form.ShowDialog(this) != DialogResult.OK || form.Result == null) - return; - - var result = form.Result; - - if (result.Target == PatternTileTarget.CurrentPlate) - { - activeForm.PlateView.Plate.Parts.Clear(); - - foreach (var part in result.Parts) - activeForm.PlateView.Plate.Parts.Add(part); - - activeForm.PlateView.ZoomToFit(); - } - else - { - var plate = activeForm.Nest.CreatePlate(); - plate.Size = result.PlateSize; - - foreach (var part in result.Parts) - plate.Parts.Add(part); - - activeForm.PlateManager.LoadLast(); - } - - activeForm.Nest.UpdateDrawingQuantities(); - } - } - - private void SetOffsetIncrement_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - var form = new SetValueForm(); - form.Text = "Set Offset Increment"; - form.Value = activeForm.PlateView.OffsetIncrementDistance; - - if (form.ShowDialog() == DialogResult.OK) - activeForm.PlateView.OffsetIncrementDistance = form.Value; - } - - private void SetRotationIncrement_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - var form = new SetValueForm(); - form.Text = "Set Rotation Increment"; - form.Value = activeForm.PlateView.RotateIncrementAngle; - form.Maximum = 360; - - if (form.ShowDialog() == DialogResult.OK) - activeForm.PlateView.RotateIncrementAngle = form.Value; - } - - private void Options_Click(object sender, EventArgs e) - { - var form = new OptionsForm(); - form.ShowDialog(); - } - - private void NestDefaults_Click(object sender, EventArgs e) - { - using (var form = new NestDefaultsForm(LoadSavedNestDefaults(out _))) - { - if (form.ShowDialog(this) == DialogResult.OK) - form.GetDefaults().Save(NestDefaults.DefaultPath); - } - } - - private void SaveCurrentAsDefaults_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - NestDefaults - .FromPlate(activeForm.Nest.Units, activeForm.PlateView.Plate) - .Save(NestDefaults.DefaultPath); - } - - private void MachineConfig_Click(object sender, EventArgs e) - { - var appDataPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "OpenNest", - "Machines" - ); - var provider = new LocalJsonProvider(appDataPath); - provider.EnsureDefaults(); - using (var form = new MachineConfigForm(provider)) - { - form.ShowDialog(this); - } - } - - private void AlignLeft_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.Left); - } - - private void AlignRight_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.Right); - } - - private void AlignTop_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.Top); - } - - private void AlignBottom_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.Bottom); - } - - private void AlignVertical_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.Vertically); - } - - private void AlignHorizontal_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.Horizontally); - } - - private void EvenlySpaceHorizontally_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.EvenlySpaceHorizontally); - } - - private void EvenlySpaceVertically_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateView.AlignSelected(AlignType.EvenlySpaceVertically); - } - - #endregion Tools Menu Events - - #region Nest Menu Events - - private void Import_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.Import(); - } - - private void ShapeLibrary_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - var form = new ShapeLibraryForm(activeForm.Nest.Drawings.Select(d => d.Name)); - form.ShowDialog(); - - var drawings = form.GetDrawings(); - if (drawings.Count == 0) - return; - - drawings.ForEach(d => activeForm.Nest.Drawings.Add(d)); - activeForm.UpdateDrawingList(); - } - - private void DrawingLibrary_Click(object sender, EventArgs e) - { - activeForm?.AddFromDrawingLibrary(); - } - - private void NestMenu_Opening(object sender, EventArgs e) - { - // The drawing library lives on the nest server; File mode has nothing to browse. - var databaseMode = NestStorage.Settings.IsDatabaseMode; - mnuNest.DropDown.ShowItemToolTips = true; - mnuNestDrawingLibrary.Enabled = activeForm != null && databaseMode; - mnuNestDrawingLibrary.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly; - } - - private void EditNest_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.ShowNestInfoEditor(); - } - - private PlateView[] ViewsOf(Nest nest) => - MdiChildren - .OfType() - .Where(form => ReferenceEquals(form.Nest, nest)) - .Select(form => form.PlateView) - .ToArray(); - - /// - /// True while a nest run or fill may be reading 's clamps. - /// Clamps feed nesting and validation: never change them under a running operation. - /// - private bool ClampsInUse(Nest nest) => - nestingInProgress - || Application.OpenForms.OfType().Any() - || ViewsOf(nest).Any(v => v.IsFillInProgress); - - private void Clamps_Click(object sender, EventArgs e) - { - var editForm = activeForm; - if (editForm == null) - return; - - var nest = editForm.Nest; - var views = ViewsOf(nest); - if (ClampsInUse(nest) || views.Any(v => 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) - return; - activeForm.Nest.Plates.RemoveEmptyPlates(); - } - - private void LoadFirstPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateManager.LoadFirst(); - } - - private void LoadLastPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateManager.LoadLast(); - } - - private void LoadPreviousPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateManager.LoadPrevious(); - } - - private void LoadNextPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlateManager.LoadNext(); - } - - private RemnantViewerForm remnantViewer; - - private void ShowRemnants_Click(object sender, EventArgs e) - { - if (activeForm?.PlateView?.Plate == null) - return; - - var plate = activeForm.PlateView.Plate; - - // Minimum remnant dimension = smallest part bbox dimension on the plate. - var minDim = 0.0; - var nest = activeForm.Nest; - if (nest != null) - { - foreach (var drawing in nest.Drawings) - { - var bbox = drawing.Program.BoundingBox(); - var dim = System.Math.Min(bbox.Width, bbox.Length); - if (minDim == 0 || dim < minDim) - minDim = dim; - } - } - - var finder = RemnantFinder.FromPlate(plate); - - if (remnantViewer == null || remnantViewer.IsDisposed) - { - remnantViewer = new RemnantViewerForm(); - remnantViewer.Owner = this; - - // Position next to the main form's right edge. - var screen = Screen.FromControl(this); - remnantViewer.Location = new Point( - System.Math.Min(Right, screen.WorkingArea.Right - remnantViewer.Width), - Top - ); - } - - remnantViewer.LoadRemnants(finder, minDim, activeForm.PlateView); - remnantViewer.Show(); - remnantViewer.BringToFront(); - } - - private async void RunAutoNest_Click(object sender, EventArgs e) - { - var target = activeForm; - if (target == null || target.IsDisposed || target.PlateView.Plate == null) - return; - var views = MdiChildren.OfType() - .Where(f => ReferenceEquals(f.Nest, target.Nest)).Select(f => f.PlateView).ToArray(); - if (nestingInProgress || Application.OpenForms.OfType().Any() - || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true)) - { - MessageBox.Show(this, "Finish or cancel the current plate operation before Auto Nest.", - "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - foreach (var view in views) - view.SetAction(typeof(ActionSelect)); - - using var form = new AutoNestForm(target.Nest); - if (target.Nest.PlateOptions.Count > 0) - form.LoadPlateOptions(target.Nest.PlateOptions, target.Nest.SalvageRate); - if (form.ShowDialog(this) != DialogResult.OK) - return; - - var items = form.GetNestItems(); - if (!items.Any(it => it.Quantity > 0)) - return; - var plateOptions = default(List); - if (form.OptimizePlateSize && !form.TryGetPlateOptions(out plateOptions, out var stockError)) - { - MessageBox.Show(this, stockError, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - var engineName = form.EngineName ?? EngineSelection.EngineName; - EngineSelection.EngineName = engineName; - engineComboBox.SelectedItem = engineName; - using var cts = new CancellationTokenSource(); - nestingCts = cts; - using var progressForm = new NestProgressForm(cts, showPlateRow: true) - { - AllowAccept = false, - HoldOpenUntilCompleted = true, - }; - var receivingProgress = true; - var progress = new Progress(p => - { - if (!receivingProgress || target.IsDisposed || cts.IsCancellationRequested) - return; - progressForm.UpdateProgress(p); - if (p.IsOverallBest) - target.PlateView.SetActiveParts(p.BestParts); - target.PlateView.ActiveWorkArea = p.ActiveWorkArea; - }); - var jobProgress = JobEngineNest.CreateProgress(engineName, progress); - SetNestingLockout(true); - try - { - var request = new NestPipelineRequest(engineName, items, - NestStockBuilder.FromTemplate(target.PlateView.Plate, plateOptions), - new NestJobOptions(maxPlates: 100, - salvageRate: plateOptions?.Count > 0 ? form.SalvageRate : 0, - minimumSalvageDimension: form.MinRemnantSize)); - - async Task SolveAsync() - { - try - { - return await Task.Run(() => NestPipeline.Run(request, jobProgress, cts.Token)); - } - finally - { - receivingProgress = false; - progressForm.ShowCompleted(); - progressForm.Close(); - } - } - - var solve = SolveAsync(); - // Owned modal progress holds drawings/plates stable through solve and cancellation. - if (!solve.IsCompleted) - progressForm.ShowDialog(this); - var result = await solve; - cts.Token.ThrowIfCancellationRequested(); - target.PlateView.ClearPreviewParts(); - target.PlateView.ActiveWorkArea = null; - - var allowInvalid = false; - if (!result.IsValid) - { - using var review = new NestValidationForm(result.Violations, result.CanKeep); - if (review.ShowDialog(this) != DialogResult.OK) - return; - allowInvalid = true; - } - - var costs = result.IsValid && result.CanKeep ? NestCostSummary.FromAccepted(result) : null; - var applied = NestPipelineCommit.ApplyToEmptyPlates(result, target.PlateManager, - allowInvalid, cts.Token); - if (applied.Count > 0) - target.PlateManager.LoadAt(target.Nest.Plates.IndexOf(applied[0])); - if (plateOptions?.Count > 0) - { - target.Nest.PlateOptions = plateOptions; - target.Nest.SalvageRate = form.SalvageRate; - } - target.PlateView.Invalidate(); - if (allowInvalid) - { - target.OverlapDisplay = OverlapDisplayMode.Both; - await target.CheckOverlapsAsync(); - } - // D8/D9: name every clamp Auto Nest pulled as a last resort and its added time. - var removalNotices = applied - .Where(plate => plate.RemovedClamps.Count > 0) - .Select(plate => $"Sheet {target.Nest.Plates.IndexOf(plate) + 1}: remove " - + $"{string.Join(", ", plate.RemovedClamps.OrderBy(id => id, StringComparer.Ordinal))} " - + $"(+{plate.ClampRemovalMinutes:0.#} min)") - .ToList(); - if (removalNotices.Count > 0) - MessageBox.Show(this, - "Auto Nest removed clamps as a last resort so a part that would not " - + "otherwise fit could be nested:\n\n" + string.Join("\n", removalNotices), - "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); - if (costs?.Basis == "supplied-cost") - MessageBox.Show(this, $"Gross sheet cost: {costs.GrossTotal:G}\nEstimated salvage credit: {costs.SalvageCredit:G}\nNet score: {costs.NetScore:G}", - "Auto Nest Cost", MessageBoxButtons.OK, MessageBoxIcon.Information); - // Never drop demand silently: name every unplaced part and why (the drawing - // list keeps showing the remaining quantities after this closes). - if (result.Unplaced.Count > 0) - MessageBox.Show(this, $"{engineName} could not place every part:\n\n" - + string.Join("\n", result.Unplaced.Select(u => u.Describe())), - "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - // Stop discards the entire proposal, including engines that ignore cancellation. - } - catch (Exception ex) - { - MessageBox.Show(this, $"Nesting error: {ex.Message}", "Auto Nest", - MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally - { - receivingProgress = false; - if (!target.IsDisposed) - { - target.PlateView.ClearPreviewParts(); - target.PlateView.ActiveWorkArea = null; - } - SetNestingLockout(false); - nestingCts = null; - } - } - - private void SequenceAllPlates_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - activeForm.AutoSequenceAllPlates(); - } - - private void ValidateNest_Click(object sender, EventArgs e) - { - var editForm = activeForm; - if (editForm == null) - return; - - var views = MdiChildren.OfType() - .Where(form => ReferenceEquals(form.Nest, editForm.Nest)) - .Select(form => form.PlateView).ToArray(); - if (nestingInProgress || Application.OpenForms.OfType().Any() - || views.Any(view => view.IsFillInProgress || view.Actions.CurrentAction?.IsBusy() == true)) - { - MessageBox.Show(this, "Finish or cancel the current nesting or plate action before validating.", - "Validate Nest", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - foreach (var view in views) - view.SetAction(typeof(ActionSelect)); - editForm.OpenValidationInspection(); - } - - private void PostProcessor_Click(object sender, EventArgs e) - { - var menuItem = (ToolStripMenuItem)sender; - var postProcessor = menuItem.Tag as IPostProcessor; - - var editForm = activeForm; - if (postProcessor == null || editForm == null) - return; - - // Closed progress windows can still have a fill awaiting its final commit. - // Keep the nest stable throughout verification and all owned modal dialogs. - if (MdiChildren.OfType().Any(form => form.CuttingInspection != null)) - { - MessageBox.Show(this, "Close cutting inspection before posting. Posting will run fresh checks.", - "Verify Nest Before Posting", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - var views = MdiChildren.OfType() - .Where(form => ReferenceEquals(form.Nest, editForm.Nest)) - .Select(form => form.PlateView).ToArray(); - if (nestingInProgress || Application.OpenForms.OfType().Any() - || views.Any(view => view.IsFillInProgress || view.Actions.CurrentAction?.IsBusy() == true)) - { - MessageBox.Show(this, "Finish or cancel the current nesting or plate action before posting.", - "Verify Nest Before Posting", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - foreach (var view in views) - view.SetAction(typeof(ActionSelect)); - var nest = editForm.Nest; - - if (postProcessor is IPostProcessorNestAware nestAware) - nestAware.PrepareForNest(nest); - - if (postProcessor is IConfigurablePostProcessor configurable) - { - using var configForm = new PostProcessorConfigForm(configurable); - if (configForm.ShowDialog(this) != DialogResult.OK) - return; - } - - using (var verification = new PostVerificationForm(nest, postProcessor)) - { - if (verification.ShowDialog(this) != DialogResult.OK) - return; - } - - using var dialog = new SaveFileDialog(); - dialog.Filter = "CNC File (*.cnc) | *.cnc"; - dialog.FileName = nest.Name; - - if (dialog.ShowDialog(this) == DialogResult.OK) - { - var path = dialog.FileName; - - if (postProcessor is IMultiFilePostProcessor multiFile) - { - var files = multiFile.GetOutputFiles(nest, path); - if (!ConfirmOverwrite(files, path)) - return; - - try - { - postProcessor.Post(nest, path); - } - catch (Exception ex) - when (ex is InvalidOperationException or IOException or UnauthorizedAccessException) - { - MessageBox.Show( - this, - ex.Message, - postProcessor.Name, - MessageBoxButtons.OK, - MessageBoxIcon.Warning - ); - return; - } - - if (files.Count > 1) - MessageBox.Show( - this, - $"Saved {files.Count} programs, one per sheet:\n\n" - + string.Join("\n", files.Select(Path.GetFileName)) - + $"\n\nin {Path.GetDirectoryName(path)}", - postProcessor.Name, - MessageBoxButtons.OK, - MessageBoxIcon.Information - ); - return; - } - - postProcessor.Post(nest, path); - } - } - - /// - /// The save dialog only checks the chosen name; ask before replacing any - /// other existing file a multi-file post will write. - /// - private bool ConfirmOverwrite(IReadOnlyList files, string chosenPath) - { - var existing = files - .Where(f => File.Exists(f) && !string.Equals(f, chosenPath, StringComparison.OrdinalIgnoreCase)) - .ToList(); - if (existing.Count == 0) - return true; - - var answer = MessageBox.Show( - this, - "These files already exist and will be replaced:\n\n" - + string.Join("\n", existing.Select(Path.GetFileName)) - + "\n\nReplace them?", - "Confirm Save", - MessageBoxButtons.YesNo, - MessageBoxIcon.Warning - ); - return answer == DialogResult.Yes; - } - - private void CalculateNestCutTime_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - activeForm.CalculateNestCutTime(); - } - - private void NestAssignLeadIns_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.AssignLeadInsAllPlates(); - } - - private void NestRemoveLeadIns_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.RemoveLeadInsAllPlates(); - } - - #endregion Nest Menu Events - - #region Plate Menu Events - - private void SetAsNestDefault_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.SetCurrentPlateAsNestDefault(); - } - - private void AddPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.Nest.CreatePlate(); - NavigationEnableCheck(); - } - - private void EditPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.EditPlate(); - } - - private void RemovePlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.RemoveCurrentPlate(); - } - - private void ResizeToFitParts_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.ResizePlateToFitParts(); - UpdatePlateStatus(); - } - - private void RotateCw_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.RotateCw(); - } - - private void RotateCcw_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.RotateCcw(); - } - - private void Rotate180_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.Rotate180(); - } - - private void OpenInExternalCad_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.OpenCurrentPlate(); - } - - private void CalculatePlateCutTime_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - activeForm.CalculateCurrentPlateCutTime(); - } - - private void AutoSequenceCurrentPlate_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - activeForm.AutoSequenceCurrentPlate(); - } - - private void ManualSequenceParts_Click(object sender, EventArgs e) - { - if (activeForm == null || activeForm.PlateView.Plate.Parts.Count < 2) - return; - - activeForm.PlateView.SetAction(typeof(ActionSetSequence)); - } - - private void CutOff_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - - activeForm.PlateView.SetAction(typeof(ActionCutOff)); - } - - private void AutomaticCutOff_Click(object sender, EventArgs e) => ShowAutomaticCutOff(allPlates: false); - - private void NestAutomaticCutOff_Click(object sender, EventArgs e) => ShowAutomaticCutOff(allPlates: true); - - private void ShowAutomaticCutOff(bool allPlates) - { - var editForm = activeForm; - var view = editForm?.PlateView; - if (view?.Plate == null) - return; - - var nest = editForm.Nest; - var views = MdiChildren.OfType() - .Where(form => ReferenceEquals(form.Nest, nest)) - .Select(form => form.PlateView).ToArray(); - // A closed progress window can still have a fill awaiting completion/commit. - bool IsBusy() => nestingInProgress || Application.OpenForms.OfType().Any() - || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true); - if (IsBusy()) - { - MessageBox.Show(this, "Finish or cancel the current nesting or plate action first.", - "Automatic Scrap Cutoffs", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - try - { - foreach (var plateView in views) - plateView.SetAction(typeof(ActionSelect)); - using var form = allPlates - ? new AutomaticCutOffForm(view, nest, IsBusy) - : new AutomaticCutOffForm(view, nest.Units, IsBusy); - form.ShowDialog(this); - } - catch (Exception ex) - { - MessageBox.Show(this, ex.Message, "Automatic Scrap Cutoffs", - MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally - { - if (!view.IsDisposed) - view.ClearPreviewParts(); - } - } - - private void PlatePlanCutting_Click(object sender, EventArgs e) => ShowCuttingPlan(allPlates: false); - - private void NestPlanCutting_Click(object sender, EventArgs e) => ShowCuttingPlan(allPlates: true); - - internal bool IsCuttingPlanBusy(PlateView[] views) => nestingInProgress - || Application.OpenForms.OfType().Any() - || views.Any(v => v.IsFillInProgress || v.Actions.CurrentAction?.IsBusy() == true); - - // Native tests capture this notice without opening a modal dialog in session zero. - internal System.Action CuttingPlanNotice { get; set; } - - private void ShowCuttingPlanNotice(string message, string caption) - { - if (CuttingPlanNotice != null) - CuttingPlanNotice(message, caption); - else - MessageBox.Show(this, message, caption, MessageBoxButtons.OK, MessageBoxIcon.Information); - } - - private void ShowCuttingPlan(bool allPlates) - { - if (MdiChildren.OfType().Any(form => form.CuttingInspection != null && !ReferenceEquals(form, activeForm))) - return; - var editForm = activeForm; - if (editForm?.PlateView?.Plate == null) - return; - - var nest = editForm.Nest; - var views = MdiChildren.OfType() - .Where(form => ReferenceEquals(form.Nest, nest)) - .Select(form => form.PlateView).ToArray(); - // A closed progress window can still have a fill awaiting completion/commit. - if (IsCuttingPlanBusy(views)) - { - ShowCuttingPlanNotice("Finish or cancel the current nesting or plate action first.", "Plan Cutting"); - return; - } - - try - { - foreach (var plateView in views) - plateView.SetAction(typeof(ActionSelect)); - editForm.OpenCuttingInspection(allPlates); - } - catch (Exception ex) - { - MessageBox.Show(this, ex.Message, "Plan Cutting", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - - private void PlateAssignLeadIns_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.AssignLeadIns_Click(sender, e); - } - - private void PlatePlaceLeadIn_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.PlaceLeadIn_Click(sender, e); - } - - private void PlateRemoveLeadIns_Click(object sender, EventArgs e) - { - if (activeForm == null) - return; - activeForm.RemoveLeadIns_Click(sender, e); - } - - #endregion Plate Menu Events - #region Window Menu Events private void TileVertical_Click(object sender, EventArgs e) @@ -2281,61 +463,5 @@ namespace OpenNest.Forms } #endregion Window Menu Events - - #region Statusbar Events - - private void LocationStatusLabel_Click(object sender, EventArgs e) - { - clickUpdateLocation = !clickUpdateLocation; - UpdateLocationMode(); - } - - #endregion Statusbar Events - - #region PlateView Events - - private void PlateView_PartAdded(object sender, ItemAddedEventArgs e) => - UpdatePlateStatus(); - - private void PlateView_PartRemoved(object sender, ItemRemovedEventArgs e) => - UpdatePlateStatus(); - - private void PlateView_MouseMove(object sender, MouseEventArgs e) - { - UpdateLocationStatus(); - } - - private void PlateView_MouseClick(object sender, MouseEventArgs e) - { - if (e.Button == MouseButtons.Left) - UpdateLocationStatus(); - } - - private void PlateView_StatusChanged(object sender, EventArgs e) - { - statusLabel1.Text = activeForm.PlateView.Status; - } - - private void PlateView_SelectionChanged(object sender, EventArgs e) - { - UpdateSelectionStatus(); - } - - #endregion PlateView Events - - private void centerPartsToolStripMenuItem_Click(object sender, EventArgs e) - { - var plateCenter = this.activeForm.PlateView.Plate.BoundingBox(false).Center; - var partsCenter = this.activeForm.PlateView.Parts.GetBoundingBox().Center; - - var offset = plateCenter - partsCenter; - - foreach (var part in this.activeForm.PlateView.Parts) - { - part.Offset(offset); - } - - activeForm.PlateView.Invalidate(); - } } } diff --git a/OpenNest/OpenNest.csproj b/OpenNest/OpenNest.csproj index 3393f0fe..8b01d8b6 100644 --- a/OpenNest/OpenNest.csproj +++ b/OpenNest/OpenNest.csproj @@ -44,6 +44,10 @@ Settings.settings True + + MainForm.cs + Code +