From eda4a846f0cebe0fdf8b6b41f84d5180275ed7e7 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 07:57:24 -0400 Subject: [PATCH 1/4] fix(irregular): charge nested block-fill cost against the work budget BlockCatalog's PrepareBlocks pre-pass ran a full independent NFP-based pack (PrivatePlateFill.Run) for up to 8 attempts per part type, but that cost never touched the engine's WorkCounter/WorkBudget. For jobs with several part types at double-digit quantities, these uncounted nested solves could burn the entire 5-minute timeout inside the first sheet of the first strategy variant, so Solve() never returned and nothing was ever committed. Charge quantity^2 * outline vertex count against the shared counter before each nested fill, and stop issuing new block proposals once the budget is exhausted, so the existing effort governor actually sees and bounds this cost. Co-Authored-By: Claude Sonnet 5 --- .../NestingEngines/IrregularBlockTests.cs | 10 +++++----- .../NestingEngines/Irregular/BlockCatalog.cs | 13 +++++++++++-- .../Irregular/IrregularNestingEngine.cs | 7 ++++--- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/OpenNest.Engine.Tests/NestingEngines/IrregularBlockTests.cs b/OpenNest.Engine.Tests/NestingEngines/IrregularBlockTests.cs index 92b79ef..c2327ef 100644 --- a/OpenNest.Engine.Tests/NestingEngines/IrregularBlockTests.cs +++ b/OpenNest.Engine.Tests/NestingEngines/IrregularBlockTests.cs @@ -19,7 +19,7 @@ public class IrregularBlockTests var job = Job(new[] { Part("ell", LShape(9, 7, 3), quantity, RotationPolicy.Automatic) }, new[] { Stock("sheet", 40, 60, spacing: 0.25) }); var types = PartCatalog.Build(job); - using var catalog = new BlockCatalog(0.25, types, NoPairs); + using var catalog = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue); Assert.Empty(catalog.Get(types[0], quantity, new Box(0, 0, 60, 40), CancellationToken.None)); Assert.Equal(0, catalog.PreparationCount); } @@ -31,7 +31,7 @@ public class IrregularBlockTests new[] { Stock("sheet", 40, 60, spacing: 0.25) }); var types = PartCatalog.Build(job); var original = types[0].Orientations.ToArray(); - using var catalog = new BlockCatalog(0.25, types, NoPairs); + using var catalog = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue); var block = catalog.Get(types[0], 7, new Box(0, 0, 60, 40), CancellationToken.None); Assert.Equal(7, block.Count); Assert.Equal(original, types[0].Orientations); @@ -79,7 +79,7 @@ public class IrregularBlockTests var job = Job(new[] { Part("ell", LShape(9, 7, 3), 44, RotationPolicy.Automatic) }, new[] { stock }); var types = PartCatalog.Build(job); var pairs = PairCatalog.Build(types, 0.25, 40, 30, CancellationToken.None); - using var blocks = new BlockCatalog(0.25, types, pairs); + using var blocks = new BlockCatalog(0.25, types, pairs, new WorkCounter(), long.MaxValue); var before = new FrontierPacker(types, new NoFitCache(0.25), pairs, stock, PackAxis.X, 1, new WorkCounter()).Fill(new[] { 44 }, CancellationToken.None); var after = new FrontierPacker(types, new NoFitCache(0.25), pairs, stock, @@ -97,7 +97,7 @@ public class IrregularBlockTests var stock = Stock("sheet", 3, 3, spacing: 0.25); var job = Job(new[] { Rectangle("box", 2, 2, 3) }, new[] { stock }); var types = PartCatalog.Build(job); - using var blocks = new BlockCatalog(0.25, types, NoPairs); + using var blocks = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue); Assert.Empty(blocks.Get(types[0], 3, new Box(0, 0, 3, 3), CancellationToken.None)); Assert.Equal(0, blocks.PreparationCount); var fill = new FrontierPacker(types, new NoFitCache(0.25), NoPairs, stock, @@ -158,7 +158,7 @@ public class IrregularBlockTests { var job = Job(new[] { Rectangle("box", 2, 2, 3) }, new[] { Stock("sheet", 10, 20) }); var types = PartCatalog.Build(job); - using var catalog = new BlockCatalog(0.25, types, NoPairs); + using var catalog = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue); Assert.Throws(() => catalog.Get(types[0], 3, new Box(0, 0, 20, 10), new CancellationToken(true))); } diff --git a/OpenNest.Engine/NestingEngines/Irregular/BlockCatalog.cs b/OpenNest.Engine/NestingEngines/Irregular/BlockCatalog.cs index 78c0991..2df13e3 100644 --- a/OpenNest.Engine/NestingEngines/Irregular/BlockCatalog.cs +++ b/OpenNest.Engine/NestingEngines/Irregular/BlockCatalog.cs @@ -17,6 +17,8 @@ namespace OpenNest.Engine.NestingEngines.Irregular; internal sealed class BlockCatalog : IDisposable { private readonly double spacing; + private readonly WorkCounter counter; + private readonly long budget; private readonly Dictionary drawings = new(); private readonly Dictionary> orientations = new(); private readonly Dictionary attempts = new(); @@ -24,9 +26,11 @@ internal sealed class BlockCatalog : IDisposable private readonly Dictionary<(int Type, int Quantity, double Length, double Width), IReadOnlyList> cache = new(); public BlockCatalog(double spacing, IReadOnlyList types, - IReadOnlyDictionary> pairs) + IReadOnlyDictionary> pairs, WorkCounter counter, long budget) { this.spacing = spacing; + this.counter = counter; + this.budget = budget; foreach (var type in types) { var poses = type.Orientations.ToList(); @@ -65,7 +69,7 @@ internal sealed class BlockCatalog : IDisposable var key = (type.Index, quantity, rectangle.Length, rectangle.Width); if (cache.TryGetValue(key, out var cached)) return cached; - if (rectangle.Area() < 3 * type.Area || attempts.GetValueOrDefault(type.Index) >= 8) + if (rectangle.Area() < 3 * type.Area || attempts.GetValueOrDefault(type.Index) >= 8 || counter.Value >= budget) return Array.Empty(); attempts[type.Index] = attempts.GetValueOrDefault(type.Index) + 1; var result = Build(type, quantity, rectangle, token); @@ -75,6 +79,11 @@ internal sealed class BlockCatalog : IDisposable private IReadOnlyList Build(PartType type, int quantity, Box rectangle, CancellationToken token) { + // PrivatePlateFill.Run below is a full independent NFP-based pack whose own operations + // never touch this solve's effort meter; charge it here using the same factors - demand + // and outline complexity - that drive its real cost, so the shared budget actually sees it. + var vertices = type.Orientations.Count > 0 ? type.Orientations[0].Outline.Count : 1; + counter.Add((long)quantity * quantity * System.Math.Max(1, vertices)); if (!drawings.TryGetValue(type.Index, out var drawing)) drawings[type.Index] = drawing = DrawingJobMapper.CreateDrawing(type.Part); try diff --git a/OpenNest.Engine/NestingEngines/Irregular/IrregularNestingEngine.cs b/OpenNest.Engine/NestingEngines/Irregular/IrregularNestingEngine.cs index aeb106e..12c12ac 100644 --- a/OpenNest.Engine/NestingEngines/Irregular/IrregularNestingEngine.cs +++ b/OpenNest.Engine/NestingEngines/Irregular/IrregularNestingEngine.cs @@ -48,7 +48,7 @@ public sealed class IrregularNestingEngine : INestingEngine ArgumentNullException.ThrowIfNull(job); token.ThrowIfCancellationRequested(); var types = PartCatalog.Build(job); - using var solver = new Solver(job, types, progress, token); + using var solver = new Solver(job, types, progress, token, WorkBudget); // Pair-only orientations may fit stock even when the sampled single poses do not. // Demand that neither a single nor a pair can fit is reported unplaced. @@ -86,7 +86,8 @@ public sealed class IrregularNestingEngine : INestingEngine NestJob job, IReadOnlyList types, IProgress? progress, - CancellationToken token + CancellationToken token, + long workBudget ) : IDisposable { public void Dispose() @@ -104,7 +105,7 @@ public sealed class IrregularNestingEngine : INestingEngine { var spacing = System.Math.Max(0, stock.PartSpacing); if (!blocks.TryGetValue(spacing, out var found)) - blocks[spacing] = found = new BlockCatalog(spacing, types, PairsFor(stock)); + blocks[spacing] = found = new BlockCatalog(spacing, types, PairsFor(stock), Work, workBudget); return found; } From 4a55d6664c79d3f64cb8c4d39e1a4c0da82c4eab Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 14:18:42 -0400 Subject: [PATCH 2/4] refactor(ui): reorganize menu bar - move arrangement ops into Plate Align Selected, Pattern Tile and Expand Spacing lived under Tools even though they only operate on the current plate's parts, while Center Parts and Sequence Parts (the same category) lived under Plate. Folded them into a new Plate > Arrange submenu and left Tools with just generic tools and app/nest configuration. Co-Authored-By: Claude Sonnet 5 --- OpenNest/Forms/MainForm.Designer.cs | 318 +++++++++++++++------------- OpenNest/Forms/MainForm.cs | 2 - 2 files changed, 166 insertions(+), 154 deletions(-) diff --git a/OpenNest/Forms/MainForm.Designer.cs b/OpenNest/Forms/MainForm.Designer.cs index ba6d61a..8da05fe 100644 --- a/OpenNest/Forms/MainForm.Designer.cs +++ b/OpenNest/Forms/MainForm.Designer.cs @@ -73,27 +73,14 @@ mnuTools = new System.Windows.Forms.ToolStripMenuItem(); mnuToolsMeasureArea = new System.Windows.Forms.ToolStripMenuItem(); mnuToolsBestFitViewer = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsPatternTile = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsExpandSpacing = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsAlign = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsAlignLeft = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsAlignRight = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsAlignTop = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsAlignBottom = new System.Windows.Forms.ToolStripMenuItem(); - toolStripMenuItem11 = new System.Windows.Forms.ToolStripSeparator(); - mnuToolsAlignHorizontal = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsAlignVertically = new System.Windows.Forms.ToolStripMenuItem(); - toolStripMenuItem8 = new System.Windows.Forms.ToolStripSeparator(); - mnuToolsEvenlySpaceHorizontal = new System.Windows.Forms.ToolStripMenuItem(); - mnuToolsEvenlySpaceVertical = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem14 = new System.Windows.Forms.ToolStripSeparator(); - mnuSetOffsetIncrement = new System.Windows.Forms.ToolStripMenuItem(); - mnuSetRotationIncrement = new System.Windows.Forms.ToolStripMenuItem(); - toolStripMenuItem15 = new System.Windows.Forms.ToolStripSeparator(); mnuToolsMachineConfig = new System.Windows.Forms.ToolStripMenuItem(); mnuToolsNestDefaults = new System.Windows.Forms.ToolStripMenuItem(); mnuToolsSaveCurrentAsDefaults = new System.Windows.Forms.ToolStripMenuItem(); mnuToolsOptions = new System.Windows.Forms.ToolStripMenuItem(); + toolStripMenuItem15 = new System.Windows.Forms.ToolStripSeparator(); + mnuSetOffsetIncrement = new System.Windows.Forms.ToolStripMenuItem(); + mnuSetRotationIncrement = new System.Windows.Forms.ToolStripMenuItem(); mnuNest = new System.Windows.Forms.ToolStripMenuItem(); mnuNestEdit = new System.Windows.Forms.ToolStripMenuItem(); mnuNestImportDrawing = new System.Windows.Forms.ToolStripMenuItem(); @@ -129,19 +116,34 @@ mnuPlateRotate180 = new System.Windows.Forms.ToolStripMenuItem(); mnuResizeToFitParts = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem13 = new System.Windows.Forms.ToolStripSeparator(); - mnuPlateViewInCad = new System.Windows.Forms.ToolStripMenuItem(); - toolStripMenuItem20 = new System.Windows.Forms.ToolStripSeparator(); + mnuPlateArrange = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateAlignLeft = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateAlignRight = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateAlignTop = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateAlignBottom = new System.Windows.Forms.ToolStripMenuItem(); + toolStripMenuItem11 = new System.Windows.Forms.ToolStripSeparator(); + mnuPlateAlignHorizontal = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateAlignVertically = new System.Windows.Forms.ToolStripMenuItem(); + toolStripMenuItem8 = new System.Windows.Forms.ToolStripSeparator(); + mnuPlateEvenlySpaceHorizontal = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateEvenlySpaceVertical = new System.Windows.Forms.ToolStripMenuItem(); + toolStripMenuItem23 = new System.Windows.Forms.ToolStripSeparator(); + centerPartsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + toolStripMenuItem24 = new System.Windows.Forms.ToolStripSeparator(); + mnuPlatePatternTile = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateExpandSpacing = new System.Windows.Forms.ToolStripMenuItem(); mnuSequenceParts = new System.Windows.Forms.ToolStripMenuItem(); autoSequenceCurrentPlateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); manualSequenceToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); calculateCutTimeToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); - centerPartsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - mnuPlateCutOff = new System.Windows.Forms.ToolStripMenuItem(); - mnuPlateAutomaticCutOff = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem21 = new System.Windows.Forms.ToolStripSeparator(); mnuPlateAssignLeadIns = new System.Windows.Forms.ToolStripMenuItem(); mnuPlatePlaceLeadIn = new System.Windows.Forms.ToolStripMenuItem(); mnuPlateRemoveLeadIns = new System.Windows.Forms.ToolStripMenuItem(); + toolStripMenuItem20 = new System.Windows.Forms.ToolStripSeparator(); + mnuPlateViewInCad = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateCutOff = new System.Windows.Forms.ToolStripMenuItem(); + mnuPlateAutomaticCutOff = new System.Windows.Forms.ToolStripMenuItem(); mnuWindow = new System.Windows.Forms.ToolStripMenuItem(); mnuWindowCascade = new System.Windows.Forms.ToolStripMenuItem(); mnuWindowTileVertical = new System.Windows.Forms.ToolStripMenuItem(); @@ -492,7 +494,7 @@ // // mnuTools // - mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuToolsMeasureArea, mnuToolsBestFitViewer, mnuToolsPatternTile, mnuToolsExpandSpacing, mnuToolsAlign, toolStripMenuItem14, mnuSetOffsetIncrement, mnuSetRotationIncrement, toolStripMenuItem15, mnuToolsMachineConfig, mnuToolsNestDefaults, mnuToolsSaveCurrentAsDefaults, mnuToolsOptions }); + mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuToolsMeasureArea, mnuToolsBestFitViewer, toolStripMenuItem14, mnuToolsMachineConfig, mnuToolsNestDefaults, mnuToolsSaveCurrentAsDefaults, mnuToolsOptions, toolStripMenuItem15, mnuSetOffsetIncrement, mnuSetRotationIncrement }); mnuTools.Name = "mnuTools"; mnuTools.Size = new System.Drawing.Size(47, 20); mnuTools.Text = "&Tools"; @@ -511,93 +513,6 @@ mnuToolsBestFitViewer.Text = "Best-Fit Viewer"; mnuToolsBestFitViewer.Click += BestFitViewer_Click; // - // mnuToolsPatternTile - // - this.mnuToolsPatternTile.Name = "mnuToolsPatternTile"; - this.mnuToolsPatternTile.Size = new System.Drawing.Size(214, 22); - this.mnuToolsPatternTile.Text = "Pattern Tile"; - this.mnuToolsPatternTile.Click += PatternTile_Click; - // - // mnuToolsExpandSpacing - // - this.mnuToolsExpandSpacing.Name = "mnuToolsExpandSpacing"; - this.mnuToolsExpandSpacing.Size = new System.Drawing.Size(214, 22); - this.mnuToolsExpandSpacing.Text = "Expand Spacing"; - this.mnuToolsExpandSpacing.Click += ExpandSpacing_Click; - // - // mnuToolsAlign - // - mnuToolsAlign.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuToolsAlignLeft, mnuToolsAlignRight, mnuToolsAlignTop, mnuToolsAlignBottom, toolStripMenuItem11, mnuToolsAlignHorizontal, mnuToolsAlignVertically, toolStripMenuItem8, mnuToolsEvenlySpaceHorizontal, mnuToolsEvenlySpaceVertical }); - mnuToolsAlign.Name = "mnuToolsAlign"; - mnuToolsAlign.Size = new System.Drawing.Size(214, 22); - mnuToolsAlign.Text = "Align Selected"; - // - // mnuToolsAlignLeft - // - mnuToolsAlignLeft.Name = "mnuToolsAlignLeft"; - mnuToolsAlignLeft.Size = new System.Drawing.Size(209, 22); - mnuToolsAlignLeft.Text = "Left"; - mnuToolsAlignLeft.Click += AlignLeft_Click; - // - // mnuToolsAlignRight - // - mnuToolsAlignRight.Name = "mnuToolsAlignRight"; - mnuToolsAlignRight.Size = new System.Drawing.Size(209, 22); - mnuToolsAlignRight.Text = "Right"; - mnuToolsAlignRight.Click += AlignRight_Click; - // - // mnuToolsAlignTop - // - mnuToolsAlignTop.Name = "mnuToolsAlignTop"; - mnuToolsAlignTop.Size = new System.Drawing.Size(209, 22); - mnuToolsAlignTop.Text = "Top"; - mnuToolsAlignTop.Click += AlignTop_Click; - // - // mnuToolsAlignBottom - // - mnuToolsAlignBottom.Name = "mnuToolsAlignBottom"; - mnuToolsAlignBottom.Size = new System.Drawing.Size(209, 22); - mnuToolsAlignBottom.Text = "Bottom"; - mnuToolsAlignBottom.Click += AlignBottom_Click; - // - // toolStripMenuItem11 - // - toolStripMenuItem11.Name = "toolStripMenuItem11"; - toolStripMenuItem11.Size = new System.Drawing.Size(206, 6); - // - // mnuToolsAlignHorizontal - // - mnuToolsAlignHorizontal.Name = "mnuToolsAlignHorizontal"; - mnuToolsAlignHorizontal.Size = new System.Drawing.Size(209, 22); - mnuToolsAlignHorizontal.Text = "Horizontally"; - mnuToolsAlignHorizontal.Click += AlignHorizontal_Click; - // - // mnuToolsAlignVertically - // - mnuToolsAlignVertically.Name = "mnuToolsAlignVertically"; - mnuToolsAlignVertically.Size = new System.Drawing.Size(209, 22); - mnuToolsAlignVertically.Text = "Vertically"; - mnuToolsAlignVertically.Click += AlignVertical_Click; - // - // toolStripMenuItem8 - // - toolStripMenuItem8.Name = "toolStripMenuItem8"; - toolStripMenuItem8.Size = new System.Drawing.Size(206, 6); - // - // mnuToolsEvenlySpaceHorizontal - // - mnuToolsEvenlySpaceHorizontal.Name = "mnuToolsEvenlySpaceHorizontal"; - mnuToolsEvenlySpaceHorizontal.Size = new System.Drawing.Size(209, 22); - mnuToolsEvenlySpaceHorizontal.Text = "Evenly Space Horizontally"; - mnuToolsEvenlySpaceHorizontal.Click += EvenlySpaceHorizontally_Click; - // - // mnuToolsEvenlySpaceVertical - // - mnuToolsEvenlySpaceVertical.Name = "mnuToolsEvenlySpaceVertical"; - mnuToolsEvenlySpaceVertical.Size = new System.Drawing.Size(209, 22); - mnuToolsEvenlySpaceVertical.Text = "Evenly Space Vertically"; - mnuToolsEvenlySpaceVertical.Click += EvenlySpaceVertically_Click; - // // toolStripMenuItem14 // toolStripMenuItem14.Name = "toolStripMenuItem14"; @@ -796,8 +711,8 @@ mnuNestRemoveLeadIns.Click += NestRemoveLeadIns_Click; // // mnuPlate - // - mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateCutOff, mnuPlateAutomaticCutOff, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateViewInCad, toolStripMenuItem20, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, centerPartsToolStripMenuItem, toolStripMenuItem21, mnuPlateAssignLeadIns, mnuPlatePlaceLeadIn, mnuPlateRemoveLeadIns }); + // + mnuPlate.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateEdit, mnuPlateSetAsDefault, toolStripMenuItem18, mnuPlateAdd, mnuPlateRemove, toolStripMenuItem16, mnuPlateCutOff, mnuPlateAutomaticCutOff, mnuPlateRotate, mnuResizeToFitParts, toolStripMenuItem13, mnuPlateArrange, mnuSequenceParts, calculateCutTimeToolStripMenuItem1, toolStripMenuItem21, mnuPlateAssignLeadIns, mnuPlatePlaceLeadIn, mnuPlateRemoveLeadIns, toolStripMenuItem20, mnuPlateViewInCad }); mnuPlate.Name = "mnuPlate"; mnuPlate.Size = new System.Drawing.Size(45, 20); mnuPlate.Text = "&Plate"; @@ -892,64 +807,149 @@ mnuPlateRotate180.Click += Rotate180_Click; // // mnuResizeToFitParts - // + // mnuResizeToFitParts.Name = "mnuResizeToFitParts"; mnuResizeToFitParts.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.P; mnuResizeToFitParts.Size = new System.Drawing.Size(177, 22); mnuResizeToFitParts.Text = "Resize to Fit"; mnuResizeToFitParts.Click += ResizeToFitParts_Click; - // + // // toolStripMenuItem13 - // + // toolStripMenuItem13.Name = "toolStripMenuItem13"; toolStripMenuItem13.Size = new System.Drawing.Size(174, 6); - // - // mnuPlateViewInCad - // - mnuPlateViewInCad.Name = "mnuPlateViewInCad"; - mnuPlateViewInCad.Size = new System.Drawing.Size(177, 22); - mnuPlateViewInCad.Text = "View in CAD"; - mnuPlateViewInCad.Click += OpenInExternalCad_Click; - // - // toolStripMenuItem20 - // - toolStripMenuItem20.Name = "toolStripMenuItem20"; - toolStripMenuItem20.Size = new System.Drawing.Size(174, 6); - // + // + // mnuPlateArrange + // + mnuPlateArrange.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuPlateAlignLeft, mnuPlateAlignRight, mnuPlateAlignTop, mnuPlateAlignBottom, toolStripMenuItem11, mnuPlateAlignHorizontal, mnuPlateAlignVertically, toolStripMenuItem8, mnuPlateEvenlySpaceHorizontal, mnuPlateEvenlySpaceVertical, toolStripMenuItem23, centerPartsToolStripMenuItem, toolStripMenuItem24, mnuPlatePatternTile, mnuPlateExpandSpacing }); + mnuPlateArrange.Name = "mnuPlateArrange"; + mnuPlateArrange.Size = new System.Drawing.Size(177, 22); + mnuPlateArrange.Text = "Arrange"; + // + // mnuPlateAlignLeft + // + mnuPlateAlignLeft.Name = "mnuPlateAlignLeft"; + mnuPlateAlignLeft.Size = new System.Drawing.Size(209, 22); + mnuPlateAlignLeft.Text = "Left"; + mnuPlateAlignLeft.Click += AlignLeft_Click; + // + // mnuPlateAlignRight + // + mnuPlateAlignRight.Name = "mnuPlateAlignRight"; + mnuPlateAlignRight.Size = new System.Drawing.Size(209, 22); + mnuPlateAlignRight.Text = "Right"; + mnuPlateAlignRight.Click += AlignRight_Click; + // + // mnuPlateAlignTop + // + mnuPlateAlignTop.Name = "mnuPlateAlignTop"; + mnuPlateAlignTop.Size = new System.Drawing.Size(209, 22); + mnuPlateAlignTop.Text = "Top"; + mnuPlateAlignTop.Click += AlignTop_Click; + // + // mnuPlateAlignBottom + // + mnuPlateAlignBottom.Name = "mnuPlateAlignBottom"; + mnuPlateAlignBottom.Size = new System.Drawing.Size(209, 22); + mnuPlateAlignBottom.Text = "Bottom"; + mnuPlateAlignBottom.Click += AlignBottom_Click; + // + // toolStripMenuItem11 + // + toolStripMenuItem11.Name = "toolStripMenuItem11"; + toolStripMenuItem11.Size = new System.Drawing.Size(206, 6); + // + // mnuPlateAlignHorizontal + // + mnuPlateAlignHorizontal.Name = "mnuPlateAlignHorizontal"; + mnuPlateAlignHorizontal.Size = new System.Drawing.Size(209, 22); + mnuPlateAlignHorizontal.Text = "Horizontally"; + mnuPlateAlignHorizontal.Click += AlignHorizontal_Click; + // + // mnuPlateAlignVertically + // + mnuPlateAlignVertically.Name = "mnuPlateAlignVertically"; + mnuPlateAlignVertically.Size = new System.Drawing.Size(209, 22); + mnuPlateAlignVertically.Text = "Vertically"; + mnuPlateAlignVertically.Click += AlignVertical_Click; + // + // toolStripMenuItem8 + // + toolStripMenuItem8.Name = "toolStripMenuItem8"; + toolStripMenuItem8.Size = new System.Drawing.Size(206, 6); + // + // mnuPlateEvenlySpaceHorizontal + // + mnuPlateEvenlySpaceHorizontal.Name = "mnuPlateEvenlySpaceHorizontal"; + mnuPlateEvenlySpaceHorizontal.Size = new System.Drawing.Size(209, 22); + mnuPlateEvenlySpaceHorizontal.Text = "Evenly Space Horizontally"; + mnuPlateEvenlySpaceHorizontal.Click += EvenlySpaceHorizontally_Click; + // + // mnuPlateEvenlySpaceVertical + // + mnuPlateEvenlySpaceVertical.Name = "mnuPlateEvenlySpaceVertical"; + mnuPlateEvenlySpaceVertical.Size = new System.Drawing.Size(209, 22); + mnuPlateEvenlySpaceVertical.Text = "Evenly Space Vertically"; + mnuPlateEvenlySpaceVertical.Click += EvenlySpaceVertically_Click; + // + // toolStripMenuItem23 + // + toolStripMenuItem23.Name = "toolStripMenuItem23"; + toolStripMenuItem23.Size = new System.Drawing.Size(206, 6); + // + // centerPartsToolStripMenuItem + // + centerPartsToolStripMenuItem.Name = "centerPartsToolStripMenuItem"; + centerPartsToolStripMenuItem.Size = new System.Drawing.Size(209, 22); + centerPartsToolStripMenuItem.Text = "Center Parts"; + centerPartsToolStripMenuItem.Click += centerPartsToolStripMenuItem_Click; + // + // toolStripMenuItem24 + // + toolStripMenuItem24.Name = "toolStripMenuItem24"; + toolStripMenuItem24.Size = new System.Drawing.Size(206, 6); + // + // mnuPlatePatternTile + // + mnuPlatePatternTile.Name = "mnuPlatePatternTile"; + mnuPlatePatternTile.Size = new System.Drawing.Size(209, 22); + mnuPlatePatternTile.Text = "Pattern Tile"; + mnuPlatePatternTile.Click += PatternTile_Click; + // + // mnuPlateExpandSpacing + // + mnuPlateExpandSpacing.Name = "mnuPlateExpandSpacing"; + mnuPlateExpandSpacing.Size = new System.Drawing.Size(209, 22); + mnuPlateExpandSpacing.Text = "Expand Spacing"; + mnuPlateExpandSpacing.Click += ExpandSpacing_Click; + // // mnuSequenceParts - // + // mnuSequenceParts.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { autoSequenceCurrentPlateToolStripMenuItem, manualSequenceToolStripMenuItem }); mnuSequenceParts.Name = "mnuSequenceParts"; mnuSequenceParts.Size = new System.Drawing.Size(177, 22); mnuSequenceParts.Text = "Sequence Parts"; - // + // // autoSequenceCurrentPlateToolStripMenuItem - // + // autoSequenceCurrentPlateToolStripMenuItem.Name = "autoSequenceCurrentPlateToolStripMenuItem"; autoSequenceCurrentPlateToolStripMenuItem.Size = new System.Drawing.Size(168, 22); autoSequenceCurrentPlateToolStripMenuItem.Text = "Auto Sequence"; autoSequenceCurrentPlateToolStripMenuItem.Click += AutoSequenceCurrentPlate_Click; - // + // // manualSequenceToolStripMenuItem - // + // manualSequenceToolStripMenuItem.Name = "manualSequenceToolStripMenuItem"; manualSequenceToolStripMenuItem.Size = new System.Drawing.Size(168, 22); manualSequenceToolStripMenuItem.Text = "Manual Sequence"; manualSequenceToolStripMenuItem.Click += ManualSequenceParts_Click; - // + // // calculateCutTimeToolStripMenuItem1 - // + // calculateCutTimeToolStripMenuItem1.Name = "calculateCutTimeToolStripMenuItem1"; calculateCutTimeToolStripMenuItem1.Size = new System.Drawing.Size(177, 22); calculateCutTimeToolStripMenuItem1.Text = "Calculate Cut Time"; calculateCutTimeToolStripMenuItem1.Click += CalculatePlateCutTime_Click; - // - // centerPartsToolStripMenuItem - // - centerPartsToolStripMenuItem.Name = "centerPartsToolStripMenuItem"; - centerPartsToolStripMenuItem.Size = new System.Drawing.Size(177, 22); - centerPartsToolStripMenuItem.Text = "Center Parts"; - centerPartsToolStripMenuItem.Click += centerPartsToolStripMenuItem_Click; // // toolStripMenuItem21 // @@ -977,6 +977,18 @@ mnuPlateRemoveLeadIns.Text = "Remove Lead-ins"; mnuPlateRemoveLeadIns.Click += PlateRemoveLeadIns_Click; // + // toolStripMenuItem20 + // + toolStripMenuItem20.Name = "toolStripMenuItem20"; + toolStripMenuItem20.Size = new System.Drawing.Size(174, 6); + // + // mnuPlateViewInCad + // + mnuPlateViewInCad.Name = "mnuPlateViewInCad"; + mnuPlateViewInCad.Size = new System.Drawing.Size(177, 22); + mnuPlateViewInCad.Text = "View in CAD"; + mnuPlateViewInCad.Click += OpenInExternalCad_Click; + // // mnuWindow // mnuWindow.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuWindowCascade, mnuWindowTileVertical, mnuWindowTileHorizontal, toolStripMenuItem10, closeToolStripMenuItem, mnuCloseAll }); @@ -1378,17 +1390,19 @@ private System.Windows.Forms.ToolStripMenuItem mnuViewZoomToPlate; private System.Windows.Forms.ToolStripMenuItem mnuViewZoomToSelected; private System.Windows.Forms.ToolStripMenuItem mnuResizeToFitParts; - private System.Windows.Forms.ToolStripMenuItem mnuToolsAlign; - private System.Windows.Forms.ToolStripMenuItem mnuToolsAlignLeft; - private System.Windows.Forms.ToolStripMenuItem mnuToolsAlignRight; - private System.Windows.Forms.ToolStripMenuItem mnuToolsAlignTop; - private System.Windows.Forms.ToolStripMenuItem mnuToolsAlignBottom; + private System.Windows.Forms.ToolStripMenuItem mnuPlateArrange; + private System.Windows.Forms.ToolStripMenuItem mnuPlateAlignLeft; + private System.Windows.Forms.ToolStripMenuItem mnuPlateAlignRight; + private System.Windows.Forms.ToolStripMenuItem mnuPlateAlignTop; + private System.Windows.Forms.ToolStripMenuItem mnuPlateAlignBottom; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem11; - private System.Windows.Forms.ToolStripMenuItem mnuToolsAlignHorizontal; - private System.Windows.Forms.ToolStripMenuItem mnuToolsAlignVertically; + private System.Windows.Forms.ToolStripMenuItem mnuPlateAlignHorizontal; + private System.Windows.Forms.ToolStripMenuItem mnuPlateAlignVertically; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem8; - private System.Windows.Forms.ToolStripMenuItem mnuToolsEvenlySpaceHorizontal; - private System.Windows.Forms.ToolStripMenuItem mnuToolsEvenlySpaceVertical; + private System.Windows.Forms.ToolStripMenuItem mnuPlateEvenlySpaceHorizontal; + private System.Windows.Forms.ToolStripMenuItem mnuPlateEvenlySpaceVertical; + private System.Windows.Forms.ToolStripSeparator toolStripMenuItem23; + private System.Windows.Forms.ToolStripSeparator toolStripMenuItem24; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem12; private System.Windows.Forms.ToolStripMenuItem mnuNestPost; private System.Windows.Forms.ToolStripSeparator toolStripMenuItem13; @@ -1424,8 +1438,8 @@ private System.Windows.Forms.ToolStripMenuItem autoSequenceAllPlatesToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem mnuToolsMeasureArea; private System.Windows.Forms.ToolStripMenuItem mnuToolsBestFitViewer; - private System.Windows.Forms.ToolStripMenuItem mnuToolsPatternTile; - private System.Windows.Forms.ToolStripMenuItem mnuToolsExpandSpacing; + private System.Windows.Forms.ToolStripMenuItem mnuPlatePatternTile; + private System.Windows.Forms.ToolStripMenuItem mnuPlateExpandSpacing; private System.Windows.Forms.ToolStripButton btnSaveAs; private System.Windows.Forms.ToolStripMenuItem centerPartsToolStripMenuItem; private System.Windows.Forms.ToolStripStatusLabel gpuStatusLabel; diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 8e9f95c..80a3169 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -232,9 +232,7 @@ namespace OpenNest.Forms mnuNest.Visible = hasValue; mnuPlate.Visible = hasValue; mnuWindow.Visible = hasValue; - mnuToolsAlign.Visible = hasValue; mnuToolsMeasureArea.Visible = hasValue; - mnuToolsExpandSpacing.Visible = hasValue; mnuToolsSaveCurrentAsDefaults.Visible = hasValue; toolStripMenuItem14.Visible = hasValue; From 3a9bd08f39b9157d451cc5c3f3256ce1db20021b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 14:19:08 -0400 Subject: [PATCH 3/4] fix(ui): fix SavedNestsForm maximize bounds, lighten grid, format file size - Open maximized, but with explicit MaximizedBounds (WorkingArea minus 20px) instead of trusting default maximize behavior, since the bottom button row was rendering behind the taskbar. - Lighten the grid's borders, header and row-header colors so the default dark gridlines don't dominate the view. - Format File Size as B/KB/MB/GB instead of a raw byte count. Co-Authored-By: Claude Sonnet 5 --- OpenNest/Forms/SavedNestsForm.cs | 40 ++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/OpenNest/Forms/SavedNestsForm.cs b/OpenNest/Forms/SavedNestsForm.cs index 7bb507c..6d171bb 100644 --- a/OpenNest/Forms/SavedNestsForm.cs +++ b/OpenNest/Forms/SavedNestsForm.cs @@ -38,6 +38,7 @@ public sealed class SavedNestsForm : Form Text = "Open Nest — Database"; StartPosition = FormStartPosition.CenterParent; ClientSize = new Size(880, 480); + WindowState = FormWindowState.Maximized; MinimizeBox = false; MaximizeBox = false; ShowInTaskbar = false; @@ -83,7 +84,22 @@ public sealed class SavedNestsForm : Form SelectionMode = DataGridViewSelectionMode.FullRowSelect, MultiSelect = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells, + BorderStyle = BorderStyle.None, + BackgroundColor = SystemColors.Window, + GridColor = Color.FromArgb(230, 230, 230), + EnableHeadersVisualStyles = false, + ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single, }; + grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(245, 245, 245); + grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(90, 90, 90); + grid.ColumnHeadersDefaultCellStyle.SelectionBackColor = Color.FromArgb(245, 245, 245); + grid.RowHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single; + grid.RowHeadersDefaultCellStyle.BackColor = Color.FromArgb(245, 245, 245); + grid.RowHeadersDefaultCellStyle.ForeColor = Color.FromArgb(90, 90, 90); + grid.RowHeadersDefaultCellStyle.SelectionBackColor = Color.FromArgb(204, 228, 247); + grid.RowHeadersDefaultCellStyle.SelectionForeColor = Color.Black; + grid.DefaultCellStyle.SelectionBackColor = Color.FromArgb(204, 228, 247); + grid.DefaultCellStyle.SelectionForeColor = Color.Black; AddColumn("Name", NestSortField.Name, typeof(string)); AddColumn("Customer", NestSortField.Customer, typeof(string)); AddColumn("Status", NestSortField.Status, typeof(string)); @@ -95,7 +111,7 @@ public sealed class SavedNestsForm : Form AddColumn("Parts", NestSortField.PartCount, typeof(int)); AddColumn("Made By", NestSortField.MadeBy, typeof(string)); AddColumn("Comments", NestSortField.Comments, typeof(string)); - AddColumn("File Size", NestSortField.FileSize, typeof(long)); + AddColumn("File Size", NestSortField.FileSize, typeof(string)); AddColumn("Saved", NestSortField.SavedAt, typeof(DateTime)); grid.Columns.Add(new DataGridViewTextBoxColumn { @@ -149,6 +165,11 @@ public sealed class SavedNestsForm : Form Controls.Add(statusStrip); CancelButton = cancel; + Load += (_, _) => + { + var area = Screen.FromControl(Owner ?? this).WorkingArea; + MaximizedBounds = new Rectangle(area.Left, area.Top, area.Width, area.Height - 20); + }; Shown += async (_, _) => await RunAsync(session.RefreshAsync); } @@ -222,7 +243,7 @@ public sealed class SavedNestsForm : Form record.PartCount, record.MadeBy, record.Comments, - record.FileSize, + FormatFileSize(record.FileSize), record.SavedAt, record.Id ); @@ -246,6 +267,21 @@ public sealed class SavedNestsForm : Form _ => status.ToString(), }; + private static readonly string[] FileSizeUnits = { "B", "KB", "MB", "GB" }; + + private static string FormatFileSize(long bytes) + { + double size = bytes; + var unitIndex = 0; + while (size >= 1024 && unitIndex < FileSizeUnits.Length - 1) + { + size /= 1024; + unitIndex++; + } + + return unitIndex == 0 ? $"{size:0} {FileSizeUnits[unitIndex]}" : $"{size:0.#} {FileSizeUnits[unitIndex]}"; + } + private void Open_Click(object sender, EventArgs e) { if (grid.SelectedRows.Count == 0) From 3fc32e56e1f5cbe213def927822ebb277a7a77a4 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 14:50:36 -0400 Subject: [PATCH 4/4] feat(ui): drag-and-drop to open .nest files in Database mode Open browses server records in Database mode, with no way to load a local file. Dragging .nest files onto the window (or an open plate) now opens them from disk in either storage mode; PlateView forwards file drops via a new FilesDropped event instead of swallowing them, gated on having a listener so other PlateView hosts are unaffected. A dropped document is unbound, so Save in Database mode creates a new record on first save via the existing storage-mode logic. Co-Authored-By: Claude Sonnet 5 --- OpenNest/Controls/PlateView.cs | 14 ++++++++++++ OpenNest/Forms/MainForm.cs | 39 ++++++++++++++++++++++++++++++++++ docs/nest-storage.md | 7 ++++++ 3 files changed, 60 insertions(+) diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index c3c01ef..b68709a 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -78,6 +78,12 @@ namespace OpenNest.Controls public event EventHandler> PartRemoved; public event EventHandler StatusChanged; + /// + /// Raised when files are dropped on the plate and something is listening (see + /// ); callers decide which paths to act on. + /// + public event EventHandler FilesDropped; + public event EventHandler SelectionChanged { add => selection.SelectionChanged += value; @@ -305,10 +311,18 @@ namespace OpenNest.Controls { if (drgevent.Data.GetData(typeof(Drawing)) != null) drgevent.Effect = DragDropEffects.Copy; + else if (FilesDropped != null && drgevent.Data.GetDataPresent(DataFormats.FileDrop)) + drgevent.Effect = DragDropEffects.Copy; } protected override void OnDragDrop(DragEventArgs drgevent) { + if (drgevent.Data.GetDataPresent(DataFormats.FileDrop)) + { + FilesDropped?.Invoke(this, (string[])drgevent.Data.GetData(DataFormats.FileDrop)); + return; + } + var dwg = drgevent.Data.GetData(typeof(Drawing)) as Drawing; if (dwg == null) diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 80a3169..4956f05 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -69,6 +69,7 @@ namespace OpenNest.Forms clickUpdateLocation = true; this.SetBevel(false); + AllowDrop = true; LoadPosts(); EnableCheck(); @@ -173,6 +174,7 @@ namespace OpenNest.Forms if (remoteId != Guid.Empty) editForm.Document.BindRemote(remoteId, serverUrl); editForm.MdiParent = this; + editForm.PlateView.FilesDropped += (sender, paths) => OpenDroppedNestFiles(paths); editForm.PlateChanged += (sender, e) => { NavigationEnableCheck(); @@ -552,6 +554,43 @@ namespace OpenNest.Forms Settings.Default.Save(); } + protected override void OnDragEnter(DragEventArgs e) + { + base.OnDragEnter(e); + e.Effect = HasDroppedNestFile(e.Data) ? DragDropEffects.Copy : DragDropEffects.None; + } + + protected override void OnDragDrop(DragEventArgs e) + { + base.OnDragDrop(e); + if (e.Data.GetData(DataFormats.FileDrop) is string[] paths) + OpenDroppedNestFiles(paths); + } + + private static bool HasDroppedNestFile(IDataObject data) => + data.GetData(DataFormats.FileDrop) is string[] paths + && paths.Any(path => string.Equals( + Path.GetExtension(path), NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)); + + /// + /// Opens dropped .nest files locally regardless of storage mode. Loaded documents are + /// not bound to a server record, so Save/Save As follow the normal storage-mode rules + /// (File mode writes to disk; Database mode creates a new record on first save). + /// + private void OpenDroppedNestFiles(IEnumerable paths) + { + foreach (var path in paths) + { + if (!string.Equals(Path.GetExtension(path), NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase)) + continue; + + var reader = new NestReader(path); + var nest = reader.Read(); + LoadNest(nest); + ShowNestWarnings(reader); + } + } + protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { base.OnClosing(e); diff --git a/docs/nest-storage.md b/docs/nest-storage.md index 8ee1917..12b49c0 100644 --- a/docs/nest-storage.md +++ b/docs/nest-storage.md @@ -18,6 +18,13 @@ The desktop app can save nests two ways: modes) for producing a local file to share or back up. Export does not change the document's file save path or database record association. +Dragging one or more `.nest` files onto the main window (or onto an open +plate) opens them from disk directly, in either mode — this is the only way +to open a local file while in Database mode, since Open there browses server +records instead of showing a file dialog. A document opened this way is not +bound to a server record, so a subsequent Save in Database mode creates a new +record on first save, same as any other unbound document. + The mode and server address are stored per-PC at `%APPDATA%\OpenNest\storage.json` (`OpenNest.Data.NestStorageSettings`), defaulting to File mode so existing installs are unaffected until an operator opts in via **File > Storage Mode...**.