Compare commits

...

21 Commits

Author SHA1 Message Date
aj d65f3460a9 feat: move add/remove plate buttons to plate tab, sync remove state
Removed add and remove plate buttons from the plate header panel.
The plate tab toolbar now has add/remove buttons with the remove
button state driven by PlateManager.CanRemoveCurrent. MainForm's
Plate > Remove menu item also syncs on plate change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:12:59 -04:00
aj ede06b1bf6 fix: enforce sentinel reactively in OnPlateAdded/OnPlateRemoved
Without this, RemoveEmptyPlates would destroy the sentinel with no
recovery, and tail-plate subscriptions would go stale after plate list
mutations. Added tests for both scenarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:06:35 -04:00
aj 51eea6d1e6 refactor: wire EditNestForm to use Document for save state
EditNestForm now holds a Document instead of a bare Nest field,
eliminating duplicated LastSavePath, LastSaveDate, and SaveAs logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:05:16 -04:00
aj 3d23ad8073 refactor: update MainForm callsites to use PlateManager directly
Replace all backward-compat wrapper calls on EditNestForm (LoadFirstPlate,
LoadLastPlate, LoadNextPlate, LoadPreviousPlate, IsFirstPlate, IsLastPlate,
EnsureSentinelPlate, CurrentPlateIndex, PlateCount) with direct access to
activeForm.PlateManager. Remove the now-unused wrapper methods and properties
from EditNestForm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:58:57 -04:00
aj 107fd86066 refactor: wire PlateManager into EditNestForm, replacing inline plate management
Replace direct plate collection event handlers, navigation methods, and
sentinel logic in EditNestForm with PlateManager delegation. Navigation
buttons, list selection, export, and plate removal now route through
PlateManager. Backward-compatible delegating wrappers kept for MainForm
until Task 7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:56:23 -04:00
aj d12f0cee3e fix: restore auto-navigation on plate add in PlateManager
OnPlateAdded now navigates to the new plate when suppressNavigation is
false, matching the original EditNestForm behavior. Fixed CanRemoveCurrent
test to account for this auto-navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:52:32 -04:00
aj d93b69c524 feat: implement PlateManager sentinel, reactive subscriptions, and batch ops (Tasks 3-5)
- EnsureSentinel() maintains exactly one trailing empty plate, suppressing navigation events during mutation
- Reactive tail subscriptions (PartAdded/PartRemoved on last two plates) call EnsureSentinel automatically; re-subscribed after each plate list change
- BeginBatch()/EndBatch() defers sentinel enforcement during bulk operations
- GetOrCreateEmpty() returns or creates an empty plate; RemoveCurrent() removes the current plate with index clamping; CanRemoveCurrent guards deletion
- 13 new tests (30 total PlateManager tests), all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:51:25 -04:00
aj a65598615e fix: assign part colors to drawings created by BOM importer and MCP
Drawings created by BomImportForm and MCP InputTools were missing color
assignments, causing them to render with default empty color instead of
the standard part color palette. Moved PartColors and GetNextColor() to
Drawing in Core so all consumers share one definition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 23:49:48 -04:00
aj ed082a6799 feat: add PlateManager with navigation state and disposal
Introduces PlateChangedEventArgs and PlateManager in OpenNest.Core to centralize plate navigation logic (CurrentIndex, LoadFirst/Last/Next/Previous/At, IsFirst/IsLast). Includes full xUnit test coverage (17 tests) verifying navigation, event firing, and disposal unsubscription.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:47:18 -04:00
aj c9b17619ef fix: intercept arrow keys in CadConverterForm for file list navigation
FileListControl loses focus when interacting with other controls on the
form, making arrow key navigation stop working. Intercept Up/Down at
the form level via ProcessCmdKey and forward to the file list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:41:03 -04:00
aj f78cc78a65 fix: improve fill progress reporting and engine pipeline
- Strategies now promote results to IsOverallBest when they beat the
  pipeline best, so the UI updates immediately on improvement rather
  than waiting for each phase to complete
- PlateView only updates the main view on overall-best results, fixing
  intermediate angle-sweep layouts leaking to the plate display
- Skip Row/Column strategies for rectangle parts (redundant with Linear)
- Intercept Escape key at MainForm level via ProcessCmdKey so it always
  reaches the active PlateView regardless of focus state
- Restore keyboard focus to PlateView after fill progress form closes
- Remnant engines use SelectBestFitPair for orientation-aware pair
  selection; DefaultNestEngine tries both landscape and portrait pairs
- RemnantFiller preserves more parts during topmost-part removal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:52:13 -04:00
aj 37130e8a28 feat: add sentinel plate and plate list enhancements
Always keep a trailing empty plate so users can immediately place parts
without manually adding a plate. Auto-appends a new sentinel when parts
land on the last plate; trims excess trailing empties on removal.

Plate list now shows Parts count and Utilization % columns. Empty plates
are filtered from save and export. Sentinel updates are deferred via
BeginInvoke to avoid collection-modified exceptions and debounced to
prevent per-part overhead on bulk operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:56:54 -04:00
aj 6f19fe1822 feat: add context menu to delete drawings from the drawing list
Adds a right-click "Delete" option on the drawings tab that removes the
selected drawing and all its placed parts from every plate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:34:18 -04:00
aj 81c167320d feat: redesign AutoNest dialog with grouped layout and engine selector
Rebuild the dialog from a flat layout into grouped sections: engine
selector at top, Parts group with rotation columns and summary label,
Options group, collapsible Plate Optimizer with single-field size
parsing, and a clean button bar. Adds engine sync between dialog and
toolbar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:16:09 -04:00
aj 981188f65e feat: persist plate optimizer settings across autonest runs
Add LoadPlateOptions() method to AutoNestForm that restores saved plate
options and salvage rate from the Nest. Call this method in
RunAutoNest_Click when opening the dialog if saved options exist, and save
settings back to Nest after dialog completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:38:59 -04:00
aj ffd060bf61 feat: serialize plate optimizer settings in nest files
Add PlateOptions and SalvageRate properties to the Nest class and
round-trip them through NestWriter/NestReader via a new PlateOptionDto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:38:02 -04:00
aj a360452da3 feat: integrate PlateOptimizer into autonest flow
When "Optimize plate size" is enabled in AutoNestForm, NestSinglePlateAsync
calls PlateOptimizer.Optimize instead of engine.Nest, trying multiple plate
sizes and resizing the plate to the winning option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:36:29 -04:00
aj b3e9e5e28b feat: add plate optimizer UI controls to AutoNestForm
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:34:34 -04:00
aj 7380a43349 feat: add PlateOptimizer with cost-aware plate size selection
Tries each candidate plate size via the nesting engine, compares results
by part count then net cost (accounting for salvage credit on remnant
material), and returns the best option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:31:36 -04:00
aj 59e00cd707 feat: add PlateOption and PlateOptimizerResult data classes 2026-04-05 00:27:40 -04:00
aj 44cb6e4a2b feat: add quantity status bars and hide-nested toggle to DrawingListBox
Add colored left-edge bars (green=met, orange=short) to indicate nesting
quantity status. Replace blue selection highlight with a border outline.
Add toolbar toggle to hide fully nested drawings, auto-updating as parts
are placed or removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:26:20 -04:00
39 changed files with 2897 additions and 593 deletions
+24
View File
@@ -12,8 +12,32 @@ namespace OpenNest
public class Drawing
{
private static int nextId;
private static int nextColorIndex;
private Program program;
public static readonly Color[] PartColors = new Color[]
{
Color.FromArgb(205, 92, 92), // Indian Red
Color.FromArgb(148, 103, 189), // Medium Purple
Color.FromArgb(75, 180, 175), // Teal
Color.FromArgb(210, 190, 75), // Goldenrod
Color.FromArgb(190, 85, 175), // Orchid
Color.FromArgb(185, 115, 85), // Sienna
Color.FromArgb(120, 100, 190), // Slate Blue
Color.FromArgb(200, 100, 140), // Rose
Color.FromArgb(80, 175, 155), // Sea Green
Color.FromArgb(195, 160, 85), // Dark Khaki
Color.FromArgb(175, 95, 160), // Plum
Color.FromArgb(215, 130, 130), // Light Coral
};
public static Color GetNextColor()
{
var color = PartColors[nextColorIndex % PartColors.Length];
nextColorIndex++;
return color;
}
public Drawing()
: this(string.Empty, new Program())
{
+5
View File
@@ -1,6 +1,7 @@
using OpenNest.Collections;
using OpenNest.Geometry;
using System;
using System.Collections.Generic;
namespace OpenNest
{
@@ -51,6 +52,10 @@ namespace OpenNest
public PlateSettings PlateDefaults { get; set; }
public List<PlateOption> PlateOptions { get; set; } = new();
public double SalvageRate { get; set; } = 0.5;
public Plate CreatePlate()
{
var plate = PlateDefaults.CreateNew();
+243
View File
@@ -0,0 +1,243 @@
using OpenNest.Collections;
using System;
namespace OpenNest
{
public class PlateChangedEventArgs : EventArgs
{
public Plate Plate { get; }
public int Index { get; }
public PlateChangedEventArgs(Plate plate, int index)
{
Plate = plate;
Index = index;
}
}
public class PlateManager : IDisposable
{
private readonly Nest nest;
private bool disposed;
private bool suppressNavigation;
private bool batching;
private Plate subscribedLast;
private Plate subscribedSecondToLast;
public event EventHandler<PlateChangedEventArgs> CurrentPlateChanged;
public event EventHandler PlateListChanged;
public PlateManager(Nest nest)
{
this.nest = nest;
nest.Plates.ItemAdded += OnPlateAdded;
nest.Plates.ItemRemoved += OnPlateRemoved;
}
public int CurrentIndex { get; private set; }
public Plate CurrentPlate => nest.Plates.Count > 0 ? nest.Plates[CurrentIndex] : null;
public int Count => nest.Plates.Count;
public bool IsFirst => Count == 0 || CurrentIndex <= 0;
public bool IsLast => CurrentIndex + 1 >= Count;
public bool CanRemoveCurrent => Count > 1 && CurrentPlate != null && CurrentPlate.Parts.Count > 0;
public void LoadFirst()
{
if (Count == 0)
return;
CurrentIndex = 0;
FireCurrentPlateChanged();
}
public void LoadLast()
{
if (Count == 0)
return;
CurrentIndex = Count - 1;
FireCurrentPlateChanged();
}
public bool LoadNext()
{
if (CurrentIndex + 1 >= Count)
return false;
CurrentIndex++;
FireCurrentPlateChanged();
return true;
}
public bool LoadPrevious()
{
if (Count == 0 || CurrentIndex - 1 < 0)
return false;
CurrentIndex--;
FireCurrentPlateChanged();
return true;
}
public void LoadAt(int index)
{
if (index < 0 || index >= Count)
return;
CurrentIndex = index;
FireCurrentPlateChanged();
}
public void EnsureSentinel()
{
suppressNavigation = true;
try
{
if (Count == 0 || nest.Plates[^1].Parts.Count > 0)
nest.CreatePlate();
while (Count > 1
&& nest.Plates[^1].Parts.Count == 0
&& nest.Plates[^2].Parts.Count == 0)
{
nest.Plates.RemoveAt(Count - 1);
}
}
finally
{
suppressNavigation = false;
}
SubscribeToTailPlates();
}
public void BeginBatch()
{
batching = true;
}
public void EndBatch()
{
batching = false;
EnsureSentinel();
PlateListChanged?.Invoke(this, EventArgs.Empty);
FireCurrentPlateChanged();
}
public Plate GetOrCreateEmpty()
{
for (var i = Count - 1; i >= 0; i--)
{
if (nest.Plates[i].Parts.Count == 0)
return nest.Plates[i];
}
return nest.CreatePlate();
}
public void RemoveCurrent()
{
if (Count < 2)
return;
nest.Plates.RemoveAt(CurrentIndex);
}
private void SubscribeToTailPlates()
{
UnsubscribeFromTailPlates();
if (Count > 0)
{
subscribedLast = nest.Plates[^1];
subscribedLast.PartAdded += OnTailPartAdded;
subscribedLast.PartRemoved += OnTailPartRemoved;
}
if (Count > 1)
{
subscribedSecondToLast = nest.Plates[^2];
subscribedSecondToLast.PartAdded += OnTailPartAdded;
subscribedSecondToLast.PartRemoved += OnTailPartRemoved;
}
}
private void UnsubscribeFromTailPlates()
{
if (subscribedLast != null)
{
subscribedLast.PartAdded -= OnTailPartAdded;
subscribedLast.PartRemoved -= OnTailPartRemoved;
subscribedLast = null;
}
if (subscribedSecondToLast != null)
{
subscribedSecondToLast.PartAdded -= OnTailPartAdded;
subscribedSecondToLast.PartRemoved -= OnTailPartRemoved;
subscribedSecondToLast = null;
}
}
private void OnTailPartAdded(object sender, ItemAddedEventArgs<Part> e)
{
if (!batching)
EnsureSentinel();
}
private void OnTailPartRemoved(object sender, ItemRemovedEventArgs<Part> e)
{
if (!batching)
EnsureSentinel();
}
private void OnPlateAdded(object sender, ItemAddedEventArgs<Plate> e)
{
if (!suppressNavigation && !batching)
EnsureSentinel();
PlateListChanged?.Invoke(this, EventArgs.Empty);
if (!suppressNavigation)
{
CurrentIndex = Count - 1;
FireCurrentPlateChanged();
}
}
private void OnPlateRemoved(object sender, ItemRemovedEventArgs<Plate> e)
{
if (CurrentIndex >= Count && Count > 0)
CurrentIndex = Count - 1;
if (!suppressNavigation && !batching)
EnsureSentinel();
PlateListChanged?.Invoke(this, EventArgs.Empty);
if (!suppressNavigation)
FireCurrentPlateChanged();
}
private void FireCurrentPlateChanged()
{
CurrentPlateChanged?.Invoke(this, new PlateChangedEventArgs(CurrentPlate, CurrentIndex));
}
public void Dispose()
{
if (disposed)
return;
disposed = true;
UnsubscribeFromTailPlates();
nest.Plates.ItemAdded -= OnPlateAdded;
nest.Plates.ItemRemoved -= OnPlateRemoved;
}
}
}
+12
View File
@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace OpenNest
{
public class PlateOptimizerResult
{
public List<Part> Parts { get; set; } = new();
public PlateOption ChosenSize { get; set; }
public double NetCost { get; set; }
public double Utilization { get; set; }
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace OpenNest
{
public class PlateOption
{
public double Width { get; set; }
public double Length { get; set; }
public double Cost { get; set; }
public double Area => Width * Length;
}
}
+52 -13
View File
@@ -139,28 +139,63 @@ namespace OpenNest
var bestFits = BestFitCache.GetOrCompute(
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
var best = bestFits.FirstOrDefault(r => r.Keep);
var best = SelectBestFitPair(bestFits);
if (best == null)
return null;
var parts = best.BuildParts(drawing);
// BuildParts produces landscape orientation (Width >= Height).
// Try both landscape and portrait (90° rotated) and let the
// engine's comparer pick the better orientation.
var landscape = best.BuildParts(drawing);
var portrait = RotatePair90(landscape);
// BuildParts positions at origin — offset to work area.
var lFits = TryOffsetToWorkArea(landscape, workArea);
var pFits = TryOffsetToWorkArea(portrait, workArea);
if (!lFits && !pFits)
return null;
if (lFits && pFits)
return IsBetterFill(portrait, landscape, workArea) ? portrait : landscape;
return lFits ? landscape : portrait;
}
private static List<Part> RotatePair90(List<Part> parts)
{
var rotated = new List<Part>(parts.Count);
foreach (var p in parts)
rotated.Add((Part)p.Clone());
var bbox = ((IEnumerable<IBoundable>)rotated).GetBoundingBox();
var center = bbox.Center;
foreach (var p in rotated)
p.Rotate(-Angle.HalfPI, center);
var newBbox = ((IEnumerable<IBoundable>)rotated).GetBoundingBox();
var offset = new Vector(-newBbox.Left, -newBbox.Bottom);
foreach (var p in rotated)
{
p.Offset(offset);
p.UpdateBounds();
}
return rotated;
}
private static bool TryOffsetToWorkArea(List<Part> parts, Box workArea)
{
var bbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
if (bbox.Width > workArea.Width + Tolerance.Epsilon ||
bbox.Length > workArea.Length + Tolerance.Epsilon)
return false;
var offset = workArea.Location - bbox.Location;
foreach (var p in parts)
{
p.Offset(offset);
p.UpdateBounds();
}
// Verify pair fits in work area.
bbox = ((IEnumerable<IBoundable>)parts).GetBoundingBox();
if (bbox.Width > workArea.Width + Tolerance.Epsilon ||
bbox.Length > workArea.Length + Tolerance.Epsilon)
return null;
return parts;
return true;
}
/// <summary>
@@ -309,14 +344,18 @@ namespace OpenNest
// during progress reporting.
PhaseResults.Add(phaseResult);
if (context.Policy.Comparer.IsBetter(result, context.CurrentBest, context.WorkArea))
// FillContext.ReportProgress updates CurrentBest during the
// strategy's angle sweep. This catches strategies that return a
// result without reporting it (e.g. RectBestFit).
var improved = context.Policy.Comparer.IsBetter(result, context.CurrentBest, context.WorkArea);
if (improved)
{
context.CurrentBest = result;
context.CurrentBestScore = FillScore.Compute(result, context.WorkArea);
context.WinnerPhase = strategy.Phase;
}
if (context.CurrentBest != null && context.CurrentBest.Count > 0)
if (improved && context.CurrentBest != null && context.CurrentBest.Count > 0)
{
ReportProgress(context.Progress, new ProgressReport
{
+1 -1
View File
@@ -106,7 +106,7 @@ namespace OpenNest.Engine.Fill
// rectangular obstacle boundary. Without this, gaps between
// individual bounding boxes cause the next drawing to fill
// into inter-row spaces, producing an interleaved layout.
if (placed.Count > 1)
if (placed.Count > 2)
RemoveTopmostPart(placed);
allParts.AddRange(placed);
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using OpenNest.Engine;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -26,6 +27,20 @@ namespace OpenNest
public override ShrinkAxis TrimAxis => ShrinkAxis.Length;
protected override BestFitResult SelectBestFitPair(List<BestFitResult> results)
{
BestFitResult best = null;
foreach (var r in results)
{
if (!r.Keep) continue;
if (best == null || r.BoundingHeight < best.BoundingHeight)
best = r;
}
return best;
}
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
{
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
+6 -1
View File
@@ -56,6 +56,11 @@ namespace OpenNest
protected FillPolicy BuildPolicy() => new FillPolicy(Comparer, PreferredDirection);
protected virtual BestFitResult SelectBestFitPair(List<BestFitResult> results)
{
return results.FirstOrDefault(r => r.Keep);
}
// --- Virtual methods (side-effect-free, return parts) ---
public virtual List<Part> Fill(NestItem item, Box workArea,
@@ -333,7 +338,7 @@ namespace OpenNest
var bestFits = BestFitCache.GetOrCompute(
item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
var bestFit = bestFits.FirstOrDefault(r => r.Keep);
var bestFit = SelectBestFitPair(bestFits);
if (bestFit == null) continue;
var parts = bestFit.BuildParts(item.Drawing);
+165
View File
@@ -0,0 +1,165 @@
using OpenNest.Engine;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace OpenNest
{
public static class PlateOptimizer
{
public static PlateOptimizerResult Optimize(
List<NestItem> items,
List<PlateOption> plateOptions,
double salvageRate,
Plate templatePlate,
IProgress<NestProgress> progress = null,
CancellationToken token = default)
{
if (items == null || items.Count == 0 || plateOptions == null || plateOptions.Count == 0)
return null;
// Find the minimum dimension needed to fit the largest part.
var minPartWidth = 0.0;
var minPartLength = 0.0;
foreach (var item in items)
{
if (item.Quantity <= 0) continue;
var bb = item.Drawing.Program.BoundingBox();
var shortSide = System.Math.Min(bb.Width, bb.Length);
var longSide = System.Math.Max(bb.Width, bb.Length);
if (shortSide > minPartWidth) minPartWidth = shortSide;
if (longSide > minPartLength) minPartLength = longSide;
}
// Sort candidates by cost ascending — try cheapest first.
var candidates = plateOptions
.Where(o => FitsPart(o, minPartWidth, minPartLength, templatePlate.EdgeSpacing))
.OrderBy(o => o.Cost)
.ToList();
if (candidates.Count == 0)
return null;
PlateOptimizerResult best = null;
foreach (var option in candidates)
{
if (token.IsCancellationRequested)
break;
var result = TryPlateSize(option, items, salvageRate, templatePlate, progress, token);
if (result == null)
continue;
if (IsBetter(result, best))
best = result;
// Early exit: when salvage is zero, cheapest plate that fits everything wins.
// With salvage > 0, larger plates may have lower net cost, so keep searching.
if (salvageRate <= 0)
{
var allPlaced = items.All(i => i.Quantity <= 0 ||
result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) >= i.Quantity);
if (allPlaced)
{
Debug.WriteLine($"[PlateOptimizer] Early exit: {option.Width}x{option.Length} placed all items");
break;
}
}
}
return best;
}
private static bool FitsPart(PlateOption option, double minWidth, double minLength, Spacing edgeSpacing)
{
var workW = option.Width - edgeSpacing.Left - edgeSpacing.Right;
var workL = option.Length - edgeSpacing.Top - edgeSpacing.Bottom;
// Part fits in either orientation.
var fitsNormal = workW >= minWidth - Tolerance.Epsilon && workL >= minLength - Tolerance.Epsilon;
var fitsRotated = workW >= minLength - Tolerance.Epsilon && workL >= minWidth - Tolerance.Epsilon;
return fitsNormal || fitsRotated;
}
private static PlateOptimizerResult TryPlateSize(
PlateOption option,
List<NestItem> items,
double salvageRate,
Plate templatePlate,
IProgress<NestProgress> progress,
CancellationToken token)
{
// Create a temporary plate with candidate size + settings from template.
var tempPlate = new Plate(option.Width, option.Length)
{
PartSpacing = templatePlate.PartSpacing,
EdgeSpacing = new Spacing
{
Left = templatePlate.EdgeSpacing.Left,
Right = templatePlate.EdgeSpacing.Right,
Top = templatePlate.EdgeSpacing.Top,
Bottom = templatePlate.EdgeSpacing.Bottom,
},
};
// Clone items so the dry run doesn't mutate originals.
var clonedItems = items.Select(i => new NestItem
{
Drawing = i.Drawing, // share Drawing reference for BestFitCache compatibility
Priority = i.Priority,
Quantity = i.Quantity,
StepAngle = i.StepAngle,
RotationStart = i.RotationStart,
RotationEnd = i.RotationEnd,
}).ToList();
var engine = NestEngineRegistry.Create(tempPlate);
var parts = engine.Nest(clonedItems, progress, token);
if (parts == null || parts.Count == 0)
return null;
var workArea = tempPlate.WorkArea();
var plateArea = workArea.Width * workArea.Length;
var partsArea = 0.0;
foreach (var part in parts)
partsArea += part.BoundingBox.Area();
var remnantArea = plateArea - partsArea;
var costPerSqUnit = option.Cost / option.Area;
var netCost = option.Cost - (remnantArea * costPerSqUnit * salvageRate);
Debug.WriteLine($"[PlateOptimizer] {option.Width}x{option.Length} ${option.Cost}: " +
$"{parts.Count} parts, util={partsArea / plateArea:P1}, net=${netCost:F2}");
return new PlateOptimizerResult
{
Parts = parts,
ChosenSize = option,
NetCost = netCost,
Utilization = plateArea > 0 ? partsArea / plateArea : 0,
};
}
private static bool IsBetter(PlateOptimizerResult candidate, PlateOptimizerResult current)
{
if (current == null) return true;
// 1. More parts placed is always better.
if (candidate.Parts.Count != current.Parts.Count)
return candidate.Parts.Count > current.Parts.Count;
// 2. Lower net cost.
if (!candidate.NetCost.IsEqualTo(current.NetCost))
return candidate.NetCost < current.NetCost;
// 3. Smaller plate area as tiebreak.
return candidate.ChosenSize.Area < current.ChosenSize.Area;
}
}
}
@@ -11,6 +11,9 @@ public class ColumnFillStrategy : IFillStrategy
public List<Part> Fill(FillContext context)
{
if (context.PartType == PartType.Rectangle)
return null;
var filler = new StripeFiller(context, NestDirection.Vertical) { CompleteStripesOnly = true };
return filler.Fill();
}
+14 -1
View File
@@ -32,16 +32,29 @@ namespace OpenNest.Engine.Strategies
/// <summary>
/// Standard progress reporting for strategies and fillers. Reports intermediate
/// results using the current ActivePhase, PlateNumber, and WorkArea.
/// When the reported parts beat the current pipeline best, promotes the
/// result to IsOverallBest so the UI updates immediately.
/// </summary>
public void ReportProgress(List<Part> parts, string description)
{
var isNewBest = parts != null && parts.Count > 0
&& Policy.Comparer.IsBetter(parts, CurrentBest, WorkArea);
if (isNewBest)
{
CurrentBest = parts;
CurrentBestScore = FillScore.Compute(parts, WorkArea);
WinnerPhase = ActivePhase;
}
NestEngineBase.ReportProgress(Progress, new ProgressReport
{
Phase = ActivePhase,
PlateNumber = PlateNumber,
Parts = parts,
Parts = isNewBest ? parts : CurrentBest,
WorkArea = WorkArea,
Description = description,
IsOverallBest = isNewBest,
});
}
}
@@ -11,6 +11,9 @@ public class RowFillStrategy : IFillStrategy
public List<Part> Fill(FillContext context)
{
if (context.PartType == PartType.Rectangle)
return null;
var filler = new StripeFiller(context, NestDirection.Horizontal) { CompleteStripesOnly = true };
return filler.Fill();
}
+15
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using OpenNest.Engine;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -24,6 +25,20 @@ namespace OpenNest
public override NestDirection? PreferredDirection => NestDirection.Horizontal;
protected override BestFitResult SelectBestFitPair(List<BestFitResult> results)
{
BestFitResult best = null;
foreach (var r in results)
{
if (!r.Keep) continue;
if (best == null || r.BoundingHeight < best.BoundingHeight)
best = r;
}
return best;
}
public override List<double> BuildAngles(NestItem item, ClassificationResult classification, Box workArea)
{
var baseAngles = new List<double> { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI };
+9
View File
@@ -29,6 +29,8 @@ namespace OpenNest.IO
public PlateDefaultsDto PlateDefaults { get; init; } = new();
public List<DrawingDto> Drawings { get; init; } = new();
public List<PlateDto> Plates { get; init; } = new();
public List<PlateOptionDto> PlateOptions { get; init; } = new();
public double SalvageRate { get; init; } = 0.5;
}
public record PlateDefaultsDto
@@ -153,6 +155,13 @@ namespace OpenNest.IO
public string NoteText { get; init; } = "";
}
public record PlateOptionDto
{
public double Width { get; init; }
public double Length { get; init; }
public double Cost { get; init; }
}
public record BestFitSetDto
{
public double PlateWidth { get; init; }
+12
View File
@@ -192,6 +192,18 @@ namespace OpenNest.IO
nest.PlateDefaults.PartSpacing = pd.PartSpacing;
nest.PlateDefaults.EdgeSpacing = new Spacing(pd.EdgeSpacing.Left, pd.EdgeSpacing.Bottom, pd.EdgeSpacing.Right, pd.EdgeSpacing.Top);
// Plate optimizer settings
nest.SalvageRate = dto.SalvageRate;
if (dto.PlateOptions != null)
{
nest.PlateOptions = dto.PlateOptions.Select(o => new PlateOption
{
Width = o.Width,
Length = o.Length,
Cost = o.Cost,
}).ToList();
}
// Drawings
foreach (var d in drawingMap.OrderBy(k => k.Key))
nest.Drawings.Add(d.Value);
+15 -2
View File
@@ -88,7 +88,14 @@ namespace OpenNest.IO
},
PlateDefaults = BuildPlateDefaultsDto(),
Drawings = BuildDrawingDtos(),
Plates = BuildPlateDtos()
Plates = BuildPlateDtos(),
PlateOptions = nest.PlateOptions?.Select(o => new PlateOptionDto
{
Width = o.Width,
Length = o.Length,
Cost = o.Cost,
}).ToList() ?? new(),
SalvageRate = nest.SalvageRate,
};
}
@@ -168,9 +175,15 @@ namespace OpenNest.IO
private List<PlateDto> BuildPlateDtos()
{
var list = new List<PlateDto>();
var id = 0;
for (var i = 0; i < nest.Plates.Count; i++)
{
var plate = nest.Plates[i];
if (plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0 && plate.CutOffs.Count == 0)
continue;
id++;
var parts = new List<PartDto>();
foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff))
{
@@ -201,7 +214,7 @@ namespace OpenNest.IO
list.Add(new PlateDto
{
Id = i + 1,
Id = id,
Size = new SizeDto { Width = plate.Size.Width, Length = plate.Size.Length },
Quadrant = plate.Quadrant,
Quantity = plate.Quantity,
+3
View File
@@ -112,6 +112,7 @@ namespace OpenNest.Mcp.Tools
var drawingName = name ?? Path.GetFileNameWithoutExtension(path);
var drawing = new Drawing(drawingName, pgm);
drawing.Color = Drawing.GetNextColor();
_session.Drawings.Add(drawing);
var bbox = pgm.BoundingBox();
@@ -155,6 +156,7 @@ namespace OpenNest.Mcp.Tools
if (pgm == null)
return "Error: failed to parse G-code";
var gcodeDrawing = new Drawing(name, pgm);
gcodeDrawing.Color = Drawing.GetNextColor();
_session.Drawings.Add(gcodeDrawing);
var gcodeBbox = pgm.BoundingBox();
return $"Created drawing '{name}': bbox={gcodeBbox.Width:F2} x {gcodeBbox.Length:F2}";
@@ -164,6 +166,7 @@ namespace OpenNest.Mcp.Tools
}
var drawing = shapeDef.GetDrawing();
drawing.Color = Drawing.GetNextColor();
_session.Drawings.Add(drawing);
var bbox = drawing.Program.BoundingBox();
@@ -13,6 +13,9 @@ public class NestResponsePersistenceTests
{
var nest = new Nest("test-nest");
var plate = new Plate(new Size(60, 120));
var drawing = new Drawing("test-part");
nest.Drawings.Add(drawing);
plate.Parts.Add(new Part(drawing));
nest.Plates.Add(plate);
var request = new NestRequest
@@ -0,0 +1,127 @@
using OpenNest.Geometry;
namespace OpenNest.Tests.Engine;
public class PlateOptimizerTests
{
private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
return new Drawing(name, pgm);
}
[Fact]
public void PicksCheapestPlateThatFitsParts()
{
var options = new List<PlateOption>
{
new() { Width = 20, Length = 20, Cost = 100 },
new() { Width = 40, Length = 40, Cost = 400 },
};
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 }
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
Assert.NotNull(result);
Assert.Equal(20, result.ChosenSize.Width);
Assert.True(result.Parts.Count >= 1);
}
[Fact]
public void PrefersMorePartsOverCheaperPlate()
{
var options = new List<PlateOption>
{
new() { Width = 12, Length = 12, Cost = 50 },
new() { Width = 24, Length = 12, Cost = 100 },
};
var templatePlate = new Plate(24, 12) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 2 }
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
Assert.NotNull(result);
Assert.Equal(24, result.ChosenSize.Width);
Assert.Equal(2, result.Parts.Count);
}
[Fact]
public void SalvageRateReducesNetCost()
{
// Small: 20x20=400sqin, cost $400. Part=10x10=100sqin. Remnant=300.
// Net = 400 - 300*(400/400)*1.0 = 400-300 = 100
// Large: 40x40=1600sqin, cost $800. Part=10x10=100sqin. Remnant=1500.
// Net = 800 - 1500*(800/1600)*1.0 = 800-750 = 50
var options = new List<PlateOption>
{
new() { Width = 20, Length = 20, Cost = 400 },
new() { Width = 40, Length = 40, Cost = 800 },
};
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
templatePlate.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 }
};
var result = PlateOptimizer.Optimize(items, options, 1.0, templatePlate);
Assert.NotNull(result);
Assert.Equal(40, result.ChosenSize.Width);
}
[Fact]
public void SkipsPlatesThatAreTooSmall()
{
var options = new List<PlateOption>
{
new() { Width = 20, Length = 20, Cost = 100 },
new() { Width = 40, Length = 40, Cost = 400 },
};
var templatePlate = new Plate(40, 40) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(30, 30), Quantity = 1 }
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
Assert.NotNull(result);
Assert.Equal(40, result.ChosenSize.Width);
}
[Fact]
public void ReturnsNullWhenNoPlatesFit()
{
var options = new List<PlateOption>
{
new() { Width = 10, Length = 10, Cost = 50 },
};
var templatePlate = new Plate(10, 10) { PartSpacing = 0 };
var items = new List<NestItem>
{
new() { Drawing = MakeRectDrawing(20, 20), Quantity = 1 }
};
var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate);
Assert.Null(result);
}
}
+440
View File
@@ -0,0 +1,440 @@
using OpenNest.CNC;
using OpenNest.Collections;
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class PlateManagerTests
{
private static Nest CreateNest()
{
var nest = new Nest("test");
return nest;
}
private static Part MakePart()
{
var pgm = new Program();
pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
var drawing = new Drawing("test", pgm);
return new Part(drawing);
}
[Fact]
public void Constructor_EmptyNest_CurrentIndexZero()
{
var nest = CreateNest();
using var mgr = new PlateManager(nest);
Assert.Equal(0, mgr.CurrentIndex);
}
[Fact]
public void Constructor_NestWithPlates_CurrentIndexZero()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
Assert.Equal(0, mgr.CurrentIndex);
}
[Fact]
public void CurrentPlate_ReturnsPlateAtCurrentIndex()
{
var nest = CreateNest();
var plate = nest.CreatePlate();
using var mgr = new PlateManager(nest);
Assert.Same(plate, mgr.CurrentPlate);
}
[Fact]
public void CurrentPlate_EmptyNest_ReturnsNull()
{
var nest = CreateNest();
using var mgr = new PlateManager(nest);
Assert.Null(mgr.CurrentPlate);
}
[Fact]
public void Count_DelegatesToNestPlates()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
Assert.Equal(2, mgr.Count);
}
[Fact]
public void LoadFirst_SetsCurrentIndexToZero()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.LoadLast();
mgr.LoadFirst();
Assert.Equal(0, mgr.CurrentIndex);
}
[Fact]
public void LoadLast_SetsCurrentIndexToLastPlate()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.LoadLast();
Assert.Equal(2, mgr.CurrentIndex);
}
[Fact]
public void LoadNext_AdvancesIndex()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
var result = mgr.LoadNext();
Assert.True(result);
Assert.Equal(1, mgr.CurrentIndex);
}
[Fact]
public void LoadNext_AtEnd_ReturnsFalse()
{
var nest = CreateNest();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
var result = mgr.LoadNext();
Assert.False(result);
Assert.Equal(0, mgr.CurrentIndex);
}
[Fact]
public void LoadPrevious_DecrementsIndex()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.LoadLast();
var result = mgr.LoadPrevious();
Assert.True(result);
Assert.Equal(0, mgr.CurrentIndex);
}
[Fact]
public void LoadPrevious_AtStart_ReturnsFalse()
{
var nest = CreateNest();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
var result = mgr.LoadPrevious();
Assert.False(result);
Assert.Equal(0, mgr.CurrentIndex);
}
[Fact]
public void LoadAt_SetsExactIndex()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.LoadAt(2);
Assert.Equal(2, mgr.CurrentIndex);
}
[Fact]
public void IsFirst_WhenAtStart_ReturnsTrue()
{
var nest = CreateNest();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
Assert.True(mgr.IsFirst);
}
[Fact]
public void IsLast_WhenAtEnd_ReturnsTrue()
{
var nest = CreateNest();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
Assert.True(mgr.IsLast);
}
[Fact]
public void IsFirst_WhenNotAtStart_ReturnsFalse()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.LoadLast();
Assert.False(mgr.IsFirst);
}
[Fact]
public void Navigation_FiresCurrentPlateChanged()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
PlateChangedEventArgs received = null;
mgr.CurrentPlateChanged += (s, e) => received = e;
mgr.LoadNext();
Assert.NotNull(received);
Assert.Equal(1, received.Index);
Assert.Same(nest.Plates[1], received.Plate);
}
[Fact]
public void Dispose_UnsubscribesFromPlateEvents()
{
var nest = CreateNest();
using var mgr = new PlateManager(nest);
var eventFired = false;
mgr.PlateListChanged += (s, e) => eventFired = true;
mgr.Dispose();
nest.CreatePlate();
Assert.False(eventFired);
}
// Task 3: Sentinel plate invariant
[Fact]
public void EnsureSentinel_EmptyNest_CreatesOnePlate()
{
var nest = CreateNest();
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
Assert.Equal(1, nest.Plates.Count);
Assert.Equal(0, nest.Plates[0].Parts.Count);
}
[Fact]
public void EnsureSentinel_LastPlateHasParts_CreatesNewEmpty()
{
var nest = CreateNest();
var plate = nest.CreatePlate();
plate.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
Assert.Equal(2, nest.Plates.Count);
Assert.Equal(0, nest.Plates[^1].Parts.Count);
}
[Fact]
public void EnsureSentinel_TwoTrailingEmpties_TrimsToOne()
{
var nest = CreateNest();
nest.CreatePlate();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
Assert.Equal(1, nest.Plates.Count);
}
[Fact]
public void EnsureSentinel_PreservesCurrentIndex()
{
var nest = CreateNest();
var plate = nest.CreatePlate();
plate.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
Assert.Equal(0, mgr.CurrentIndex);
Assert.Same(plate, mgr.CurrentPlate);
}
// Task 4: Reactive tail-plate subscriptions
[Fact]
public void PartAddedToLastPlate_CreatesSentinel()
{
var nest = CreateNest();
nest.CreatePlate();
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
var initialCount = nest.Plates.Count;
nest.Plates[^1].Parts.Add(MakePart());
Assert.Equal(initialCount + 1, nest.Plates.Count);
Assert.Equal(0, nest.Plates[^1].Parts.Count);
}
[Fact]
public void PartRemovedFromSecondToLast_TrimsExtraEmpty()
{
var nest = CreateNest();
var plate = nest.CreatePlate();
plate.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
Assert.Equal(2, nest.Plates.Count);
nest.Plates[0].Parts.RemoveAt(0);
Assert.Equal(1, nest.Plates.Count);
}
[Fact]
public void ReactiveSubscription_ResubscribesAfterPlateListChange()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
nest.Plates[^1].Parts.Add(MakePart());
Assert.Equal(3, nest.Plates.Count);
nest.Plates[^1].Parts.Add(MakePart());
Assert.Equal(4, nest.Plates.Count);
Assert.Equal(0, nest.Plates[^1].Parts.Count);
}
// Task 5: Batch mode and plate operations
[Fact]
public void BeginBatch_DefersSentinelEnforcement()
{
var nest = CreateNest();
var plate = nest.CreatePlate();
plate.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
mgr.BeginBatch();
nest.Plates[^1].Parts.Add(MakePart());
Assert.Equal(2, nest.Plates.Count);
}
[Fact]
public void EndBatch_EnforcesSentinelAndFiresEvents()
{
var nest = CreateNest();
var plate = nest.CreatePlate();
plate.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
mgr.BeginBatch();
nest.Plates[^1].Parts.Add(MakePart());
var listChangedCount = 0;
mgr.PlateListChanged += (s, e) => listChangedCount++;
mgr.EndBatch();
Assert.Equal(3, nest.Plates.Count);
Assert.Equal(0, nest.Plates[^1].Parts.Count);
Assert.True(listChangedCount > 0);
}
[Fact]
public void GetOrCreateEmpty_ReturnsSentinelWhenEmpty()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
var result = mgr.GetOrCreateEmpty();
Assert.Same(nest.Plates[^1], result);
Assert.Equal(0, result.Parts.Count);
}
[Fact]
public void GetOrCreateEmpty_NoEmptyPlate_CreatesNew()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
var result = mgr.GetOrCreateEmpty();
Assert.Equal(0, result.Parts.Count);
}
[Fact]
public void RemoveCurrent_RemovesPlateAndClampsIndex()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
var plate2 = nest.CreatePlate();
plate2.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.LoadLast();
mgr.RemoveCurrent();
// plate1 + sentinel (created by reactive sentinel enforcement)
Assert.Equal(2, nest.Plates.Count);
Assert.Same(plate1, nest.Plates[0]);
Assert.Equal(0, nest.Plates[^1].Parts.Count);
Assert.Equal(0, mgr.CurrentIndex);
}
[Fact]
public void CanRemoveCurrent_OnlyIfMultiplePlatesAndHasParts()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
using var mgr = new PlateManager(nest);
Assert.False(mgr.CanRemoveCurrent);
plate1.Parts.Add(MakePart());
Assert.False(mgr.CanRemoveCurrent);
nest.CreatePlate();
// Auto-navigated to plate2 (empty), go back to plate1 (has parts)
mgr.LoadFirst();
Assert.True(mgr.CanRemoveCurrent);
}
[Fact]
public void RemoveEmptyPlates_SentinelIsRestored()
{
var nest = CreateNest();
var plate = nest.CreatePlate();
plate.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
Assert.Equal(2, nest.Plates.Count);
// RemoveEmptyPlates removes the sentinel
nest.Plates.RemoveEmptyPlates();
// Sentinel should be restored reactively by OnPlateRemoved
Assert.Equal(2, nest.Plates.Count);
Assert.Equal(0, nest.Plates[^1].Parts.Count);
}
[Fact]
public void PlateRemoval_RefreshesTailSubscriptions()
{
var nest = CreateNest();
var plate1 = nest.CreatePlate();
plate1.Parts.Add(MakePart());
var plate2 = nest.CreatePlate();
plate2.Parts.Add(MakePart());
using var mgr = new PlateManager(nest);
mgr.EnsureSentinel();
// plates: [plate1(parts), plate2(parts), sentinel(empty)]
// Remove plate2 — tail subscriptions should refresh
nest.Plates.Remove(plate2);
// plates: [plate1(parts), sentinel(empty)]
// Verify reactive subscriptions still work on the new tail
nest.Plates[^1].Parts.Add(MakePart());
Assert.Equal(0, nest.Plates[^1].Parts.Count);
}
}
+1 -15
View File
@@ -79,21 +79,7 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin
Console.WriteLine($"Using template: {template}");
}
var PartColors = new[]
{
Color.FromArgb(205, 92, 92),
Color.FromArgb(148, 103, 189),
Color.FromArgb(75, 180, 175),
Color.FromArgb(210, 190, 75),
Color.FromArgb(190, 85, 175),
Color.FromArgb(185, 115, 85),
Color.FromArgb(120, 100, 190),
Color.FromArgb(200, 100, 140),
Color.FromArgb(80, 175, 155),
Color.FromArgb(195, 160, 85),
Color.FromArgb(175, 95, 160),
Color.FromArgb(215, 130, 130),
};
var PartColors = Drawing.PartColors;
var sheetSuite = new[]
{
+1 -15
View File
@@ -13,21 +13,7 @@ namespace OpenNest
private Color edgeSpacingColor;
private Color previewPartColor;
public static readonly Color[] PartColors = new Color[]
{
Color.FromArgb(205, 92, 92), // Indian Red
Color.FromArgb(148, 103, 189), // Medium Purple
Color.FromArgb(75, 180, 175), // Teal
Color.FromArgb(210, 190, 75), // Goldenrod
Color.FromArgb(190, 85, 175), // Orchid
Color.FromArgb(185, 115, 85), // Sienna
Color.FromArgb(120, 100, 190), // Slate Blue
Color.FromArgb(200, 100, 140), // Rose
Color.FromArgb(80, 175, 155), // Sea Green
Color.FromArgb(195, 160, 85), // Dark Khaki
Color.FromArgb(175, 95, 160), // Plum
Color.FromArgb(215, 130, 130), // Light Coral
};
public static Color[] PartColors => Drawing.PartColors;
public static readonly ColorScheme Default = new ColorScheme
{
+39 -6
View File
@@ -1,4 +1,5 @@
using System.Drawing;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace OpenNest.Controls
@@ -22,8 +23,25 @@ namespace OpenNest.Controls
imageSize = new Size(ItemHeight, ItemHeight - 10);
nameFont = new Font(Font.FontFamily, 10, FontStyle.Bold);
var menu = new ContextMenuStrip();
var deleteItem = new ToolStripMenuItem("Delete");
deleteItem.Click += (s, e) =>
{
if (SelectedItem is Drawing drawing)
DeleteRequested?.Invoke(this, drawing);
};
menu.Opening += (s, e) =>
{
if (SelectedItem == null)
e.Cancel = true;
};
menu.Items.Add(deleteItem);
ContextMenuStrip = menu;
}
public event EventHandler<Drawing> DeleteRequested;
public Units Units { get; set; }
public bool HideDepletedParts { get; set; }
@@ -43,15 +61,30 @@ namespace OpenNest.Controls
var isSelected = (e.State & DrawItemState.Selected) != 0;
Brush bgBrush;
if (isSelected)
bgBrush = SystemBrushes.Highlight;
else if (!HideQuantity && dwg.Quantity.Nested > 0 && dwg.Quantity.Remaining == 0)
if (!HideQuantity && dwg.Quantity.Nested > 0 && dwg.Quantity.Remaining == 0)
bgBrush = SystemBrushes.Info;
else
bgBrush = Brushes.White;
e.Graphics.FillRectangle(bgBrush, e.Bounds);
if (isSelected)
{
var borderRect = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width - 1, e.Bounds.Height - 1);
using var borderPen = new Pen(SystemColors.Highlight, 2);
e.Graphics.DrawRectangle(borderPen, borderRect);
}
if (!HideQuantity && dwg.Quantity.Required > 0)
{
var barWidth = 4;
var barColor = dwg.Quantity.Nested >= dwg.Quantity.Required
? Color.FromArgb(76, 175, 80)
: Color.FromArgb(255, 152, 0);
using var barBrush = new SolidBrush(barColor);
e.Graphics.FillRectangle(barBrush, e.Bounds.X, e.Bounds.Y, barWidth, e.Bounds.Height);
}
var pt = new PointF(5, e.Bounds.Y + 5);
var brush = new SolidBrush(dwg.Color);
@@ -66,8 +99,8 @@ namespace OpenNest.Controls
pt.X += imageSize.Width + 10;
var textBrush = isSelected ? SystemBrushes.HighlightText : Brushes.Black;
var detailBrush = isSelected ? SystemBrushes.HighlightText : Brushes.Gray;
var textBrush = Brushes.Black;
var detailBrush = Brushes.Gray;
e.Graphics.DrawString(dwg.Name, nameFont, textBrush, pt);
+20 -12
View File
@@ -152,22 +152,14 @@ namespace OpenNest.Controls
UpdateScrollBar();
}
protected override bool IsInputKey(Keys keyData)
public void ProcessArrowKey(Keys keyData)
{
if (keyData == Keys.Up || keyData == Keys.Down)
return true;
return base.IsInputKey(keyData);
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (items.Count == 0) return;
var newIndex = selectedIndex;
if (e.KeyCode == Keys.Down)
if (keyData == Keys.Down)
newIndex = System.Math.Min(selectedIndex + 1, items.Count - 1);
else if (e.KeyCode == Keys.Up)
else if (keyData == Keys.Up)
newIndex = System.Math.Max(selectedIndex - 1, 0);
else
return;
@@ -179,7 +171,23 @@ namespace OpenNest.Controls
Invalidate();
SelectedIndexChanged?.Invoke(this, selectedIndex);
}
e.Handled = true;
}
protected override bool IsInputKey(Keys keyData)
{
if (keyData == Keys.Up || keyData == Keys.Down)
return true;
return base.IsInputKey(keyData);
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)
{
ProcessArrowKey(e.KeyCode);
e.Handled = true;
}
}
protected override void OnPaint(PaintEventArgs e)
+15 -7
View File
@@ -412,6 +412,16 @@ namespace OpenNest.Controls
}
}
public void ProcessEscapeKey()
{
if (currentAction.IsBusy())
currentAction.CancelAction();
else if (currentAction is ActionSelect && previousAction != null)
RestorePreviousAction();
else
SetAction(typeof(ActionSelect));
}
protected override bool ProcessDialogKey(Keys keyData)
{
// Only handle TAB, RETURN, ESC, and ARROW KEYS here.
@@ -420,12 +430,7 @@ namespace OpenNest.Controls
switch (keyData)
{
case Keys.Escape:
if (currentAction.IsBusy())
currentAction.CancelAction();
else if (currentAction is ActionSelect && previousAction != null)
RestorePreviousAction();
else
SetAction(typeof(ActionSelect));
ProcessEscapeKey();
break;
case Keys.Left:
@@ -791,9 +796,11 @@ namespace OpenNest.Controls
progressForm.UpdateProgress(p);
if (p.IsOverallBest)
{
progressForm.UpdatePreview(p.BestParts);
SetActiveParts(p.BestParts);
}
SetActiveParts(p.BestParts);
ActiveWorkArea = p.ActiveWorkArea;
});
@@ -837,6 +844,7 @@ namespace OpenNest.Controls
ActiveWorkArea = null;
progressForm.Close();
cts.Dispose();
Focus();
}
}
+4 -2
View File
@@ -1,4 +1,4 @@
using OpenNest.IO;
using OpenNest.IO;
using System;
using System.IO;
@@ -12,7 +12,9 @@ namespace OpenNest
public string LastSavePath { get; private set; }
public Units Units { get; set; }
public string Name => Nest?.Name;
public bool HasSavePath => File.Exists(LastSavePath);
public void SaveAs(string path)
{
+217 -77
View File
@@ -1,16 +1,9 @@
namespace OpenNest.Forms
namespace OpenNest.Forms
{
partial class AutoNestForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
@@ -22,106 +15,253 @@
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.bottomPanel1 = new OpenNest.Controls.BottomPanel();
this.acceptButton = new System.Windows.Forms.Button();
this.engineLabel = new System.Windows.Forms.Label();
this.engineComboBox = new System.Windows.Forms.ComboBox();
this.partsGroup = new System.Windows.Forms.GroupBox();
this.partsGrid = new System.Windows.Forms.DataGridView();
this.summaryLabel = new System.Windows.Forms.Label();
this.optionsGroup = new System.Windows.Forms.GroupBox();
this.createNewPlatesAsNeededBox = new System.Windows.Forms.CheckBox();
this.plateOptimizerGroup = new System.Windows.Forms.GroupBox();
this.optimizePlateSizeBox = new System.Windows.Forms.CheckBox();
this.plateGrid = new System.Windows.Forms.DataGridView();
this.salvageRateLabel = new System.Windows.Forms.Label();
this.salvageRateBox = new System.Windows.Forms.TextBox();
this.salvageRatePercentLabel = new System.Windows.Forms.Label();
this.buttonPanel = new System.Windows.Forms.Panel();
this.acceptButton = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.bottomPanel1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).BeginInit();
this.partsGroup.SuspendLayout();
this.optionsGroup.SuspendLayout();
this.plateOptimizerGroup.SuspendLayout();
this.buttonPanel.SuspendLayout();
this.SuspendLayout();
//
// dataGridView1
//
this.dataGridView1.AllowUserToDeleteRows = false;
this.dataGridView1.AllowUserToOrderColumns = true;
this.dataGridView1.AllowUserToResizeRows = false;
this.dataGridView1.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.dataGridView1.Location = new System.Drawing.Point(0, 0);
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.RowHeadersVisible = false;
this.dataGridView1.Size = new System.Drawing.Size(545, 385);
this.dataGridView1.TabIndex = 0;
//
// bottomPanel1
//
this.bottomPanel1.Controls.Add(this.acceptButton);
this.bottomPanel1.Controls.Add(this.createNewPlatesAsNeededBox);
this.bottomPanel1.Controls.Add(this.cancelButton);
this.bottomPanel1.Dock = System.Windows.Forms.DockStyle.Bottom;
this.bottomPanel1.Location = new System.Drawing.Point(0, 335);
this.bottomPanel1.Name = "bottomPanel1";
this.bottomPanel1.Size = new System.Drawing.Size(545, 50);
this.bottomPanel1.TabIndex = 9;
//
// acceptButton
//
this.acceptButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.acceptButton.DialogResult = System.Windows.Forms.DialogResult.OK;
this.acceptButton.Location = new System.Drawing.Point(344, 9);
this.acceptButton.Margin = new System.Windows.Forms.Padding(4);
this.acceptButton.Name = "acceptButton";
this.acceptButton.Size = new System.Drawing.Size(90, 28);
this.acceptButton.TabIndex = 6;
this.acceptButton.Text = "Accept";
this.acceptButton.UseVisualStyleBackColor = true;
//
//
// engineLabel
//
this.engineLabel.AutoSize = true;
this.engineLabel.Location = new System.Drawing.Point(12, 15);
this.engineLabel.Name = "engineLabel";
this.engineLabel.Size = new System.Drawing.Size(82, 16);
this.engineLabel.TabIndex = 0;
this.engineLabel.Text = "Nest Engine:";
//
// engineComboBox
//
this.engineComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.engineComboBox.Location = new System.Drawing.Point(100, 12);
this.engineComboBox.Name = "engineComboBox";
this.engineComboBox.Size = new System.Drawing.Size(200, 24);
this.engineComboBox.TabIndex = 1;
//
// partsGroup
//
this.partsGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.partsGroup.Controls.Add(this.partsGrid);
this.partsGroup.Controls.Add(this.summaryLabel);
this.partsGroup.Location = new System.Drawing.Point(12, 42);
this.partsGroup.Name = "partsGroup";
this.partsGroup.Size = new System.Drawing.Size(556, 210);
this.partsGroup.TabIndex = 2;
this.partsGroup.TabStop = false;
this.partsGroup.Text = "Parts";
//
// partsGrid
//
this.partsGrid.AllowUserToAddRows = false;
this.partsGrid.AllowUserToDeleteRows = false;
this.partsGrid.AllowUserToResizeRows = false;
this.partsGrid.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.partsGrid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.partsGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.partsGrid.Location = new System.Drawing.Point(10, 22);
this.partsGrid.Name = "partsGrid";
this.partsGrid.RowHeadersVisible = false;
this.partsGrid.AutoGenerateColumns = false;
this.partsGrid.Size = new System.Drawing.Size(536, 160);
this.partsGrid.TabIndex = 0;
//
// summaryLabel
//
this.summaryLabel.AutoSize = true;
this.summaryLabel.ForeColor = System.Drawing.SystemColors.GrayText;
this.summaryLabel.Location = new System.Drawing.Point(10, 188);
this.summaryLabel.Name = "summaryLabel";
this.summaryLabel.Size = new System.Drawing.Size(0, 16);
this.summaryLabel.TabIndex = 1;
//
// optionsGroup
//
this.optionsGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.optionsGroup.Controls.Add(this.createNewPlatesAsNeededBox);
this.optionsGroup.Location = new System.Drawing.Point(12, 258);
this.optionsGroup.Name = "optionsGroup";
this.optionsGroup.Size = new System.Drawing.Size(556, 48);
this.optionsGroup.TabIndex = 3;
this.optionsGroup.TabStop = false;
this.optionsGroup.Text = "Options";
//
// createNewPlatesAsNeededBox
//
this.createNewPlatesAsNeededBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
this.createNewPlatesAsNeededBox.AutoSize = true;
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(12, 15);
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 22);
this.createNewPlatesAsNeededBox.Name = "createNewPlatesAsNeededBox";
this.createNewPlatesAsNeededBox.Size = new System.Drawing.Size(202, 20);
this.createNewPlatesAsNeededBox.TabIndex = 8;
this.createNewPlatesAsNeededBox.TabIndex = 0;
this.createNewPlatesAsNeededBox.Text = "Create new plates as needed";
this.createNewPlatesAsNeededBox.UseVisualStyleBackColor = true;
//
//
// plateOptimizerGroup
//
this.plateOptimizerGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.plateOptimizerGroup.Controls.Add(this.optimizePlateSizeBox);
this.plateOptimizerGroup.Controls.Add(this.plateGrid);
this.plateOptimizerGroup.Controls.Add(this.salvageRateLabel);
this.plateOptimizerGroup.Controls.Add(this.salvageRateBox);
this.plateOptimizerGroup.Controls.Add(this.salvageRatePercentLabel);
this.plateOptimizerGroup.Location = new System.Drawing.Point(12, 312);
this.plateOptimizerGroup.Name = "plateOptimizerGroup";
this.plateOptimizerGroup.Size = new System.Drawing.Size(556, 188);
this.plateOptimizerGroup.TabIndex = 4;
this.plateOptimizerGroup.TabStop = false;
this.plateOptimizerGroup.Text = " Plate Optimizer";
//
// optimizePlateSizeBox
//
this.optimizePlateSizeBox.AutoSize = true;
this.optimizePlateSizeBox.Location = new System.Drawing.Point(10, 0);
this.optimizePlateSizeBox.Name = "optimizePlateSizeBox";
this.optimizePlateSizeBox.Size = new System.Drawing.Size(15, 14);
this.optimizePlateSizeBox.TabIndex = 0;
this.optimizePlateSizeBox.UseVisualStyleBackColor = true;
this.optimizePlateSizeBox.CheckedChanged += new System.EventHandler(this.optimizePlateSizeBox_CheckedChanged);
//
// plateGrid
//
this.plateGrid.AllowUserToResizeRows = false;
this.plateGrid.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.plateGrid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.plateGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.plateGrid.Location = new System.Drawing.Point(10, 22);
this.plateGrid.Name = "plateGrid";
this.plateGrid.RowHeadersVisible = false;
this.plateGrid.AutoGenerateColumns = false;
this.plateGrid.Size = new System.Drawing.Size(536, 130);
this.plateGrid.TabIndex = 1;
//
// salvageRateLabel
//
this.salvageRateLabel.AutoSize = true;
this.salvageRateLabel.Location = new System.Drawing.Point(10, 162);
this.salvageRateLabel.Name = "salvageRateLabel";
this.salvageRateLabel.Size = new System.Drawing.Size(96, 16);
this.salvageRateLabel.TabIndex = 2;
this.salvageRateLabel.Text = "Salvage Rate:";
//
// salvageRateBox
//
this.salvageRateBox.Location = new System.Drawing.Point(108, 159);
this.salvageRateBox.Name = "salvageRateBox";
this.salvageRateBox.Size = new System.Drawing.Size(50, 22);
this.salvageRateBox.TabIndex = 3;
this.salvageRateBox.Text = "50";
//
// salvageRatePercentLabel
//
this.salvageRatePercentLabel.AutoSize = true;
this.salvageRatePercentLabel.Location = new System.Drawing.Point(160, 162);
this.salvageRatePercentLabel.Name = "salvageRatePercentLabel";
this.salvageRatePercentLabel.Size = new System.Drawing.Size(21, 16);
this.salvageRatePercentLabel.TabIndex = 4;
this.salvageRatePercentLabel.Text = "%";
//
// buttonPanel
//
this.buttonPanel.Controls.Add(this.acceptButton);
this.buttonPanel.Controls.Add(this.cancelButton);
this.buttonPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.buttonPanel.Location = new System.Drawing.Point(0, 506);
this.buttonPanel.Name = "buttonPanel";
this.buttonPanel.Size = new System.Drawing.Size(580, 50);
this.buttonPanel.TabIndex = 5;
//
// acceptButton
//
this.acceptButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.acceptButton.DialogResult = System.Windows.Forms.DialogResult.OK;
this.acceptButton.Location = new System.Drawing.Point(376, 12);
this.acceptButton.Name = "acceptButton";
this.acceptButton.Size = new System.Drawing.Size(90, 28);
this.acceptButton.TabIndex = 0;
this.acceptButton.Text = "Accept";
this.acceptButton.UseVisualStyleBackColor = true;
//
// cancelButton
//
//
this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelButton.Location = new System.Drawing.Point(442, 9);
this.cancelButton.Margin = new System.Windows.Forms.Padding(4);
this.cancelButton.Location = new System.Drawing.Point(474, 12);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(90, 28);
this.cancelButton.TabIndex = 7;
this.cancelButton.TabIndex = 1;
this.cancelButton.Text = "Cancel";
this.cancelButton.UseVisualStyleBackColor = true;
//
//
// AutoNestForm
//
//
this.AcceptButton = this.acceptButton;
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.ClientSize = new System.Drawing.Size(545, 385);
this.Controls.Add(this.bottomPanel1);
this.Controls.Add(this.dataGridView1);
this.CancelButton = this.cancelButton;
this.ClientSize = new System.Drawing.Size(580, 556);
this.Controls.Add(this.engineLabel);
this.Controls.Add(this.engineComboBox);
this.Controls.Add(this.partsGroup);
this.Controls.Add(this.optionsGroup);
this.Controls.Add(this.plateOptimizerGroup);
this.Controls.Add(this.buttonPanel);
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "AutoNestForm";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "AutoNest";
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
this.bottomPanel1.ResumeLayout(false);
this.bottomPanel1.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).EndInit();
this.partsGroup.ResumeLayout(false);
this.partsGroup.PerformLayout();
this.optionsGroup.ResumeLayout(false);
this.optionsGroup.PerformLayout();
this.plateOptimizerGroup.ResumeLayout(false);
this.plateOptimizerGroup.PerformLayout();
this.buttonPanel.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.Label engineLabel;
private System.Windows.Forms.ComboBox engineComboBox;
private System.Windows.Forms.GroupBox partsGroup;
private System.Windows.Forms.DataGridView partsGrid;
private System.Windows.Forms.Label summaryLabel;
private System.Windows.Forms.GroupBox optionsGroup;
private System.Windows.Forms.CheckBox createNewPlatesAsNeededBox;
private System.Windows.Forms.GroupBox plateOptimizerGroup;
private System.Windows.Forms.CheckBox optimizePlateSizeBox;
private System.Windows.Forms.DataGridView plateGrid;
private System.Windows.Forms.Label salvageRateLabel;
private System.Windows.Forms.TextBox salvageRateBox;
private System.Windows.Forms.Label salvageRatePercentLabel;
private System.Windows.Forms.Panel buttonPanel;
private System.Windows.Forms.Button acceptButton;
private System.Windows.Forms.Button cancelButton;
private System.Windows.Forms.CheckBox createNewPlatesAsNeededBox;
private Controls.BottomPanel bottomPanel1;
}
}
}
+262 -16
View File
@@ -1,17 +1,34 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using OpenNest.Engine;
namespace OpenNest.Forms
{
public partial class AutoNestForm : Form
{
private static readonly Regex SizePattern = new(@"^(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)$");
public AutoNestForm(Nest nest)
{
InitializeComponent();
SetupPartsGrid();
SetupPlateGrid();
LoadEngines();
LoadDrawings(nest);
LoadDefaultPlateOptions();
SetPlateOptimizerVisible(false);
dataGridView1.DataError += dataGridView1_DataError;
partsGrid.DataError += PartsGrid_DataError;
}
public string EngineName
{
get { return engineComboBox.SelectedItem as string; }
set { engineComboBox.SelectedItem = value; }
}
public bool AllowPlateCreation
@@ -20,21 +37,119 @@ namespace OpenNest.Forms
set { createNewPlatesAsNeededBox.Checked = value; }
}
public bool OptimizePlateSize
{
get { return optimizePlateSizeBox.Checked; }
set { optimizePlateSizeBox.Checked = value; }
}
public double SalvageRate
{
get
{
if (double.TryParse(salvageRateBox.Text, out var val))
return System.Math.Clamp(val / 100.0, 0, 1);
return 0.5;
}
set { salvageRateBox.Text = (value * 100).ToString("F0"); }
}
private void LoadEngines()
{
foreach (var engine in NestEngineRegistry.AvailableEngines)
engineComboBox.Items.Add(engine.Name);
engineComboBox.SelectedItem = NestEngineRegistry.ActiveEngineName;
}
private void SetupPartsGrid()
{
partsGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "DrawingName",
HeaderText = "Drawing Name",
Width = 160,
ReadOnly = true,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
partsGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "Quantity",
HeaderText = "Qty",
Width = 50,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
partsGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "Priority",
HeaderText = "Priority",
Width = 55,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
partsGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "RotationStart",
HeaderText = "Rot Start",
Width = 65,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
partsGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "RotationEnd",
HeaderText = "Rot End",
Width = 60,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
partsGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "StepAngle",
HeaderText = "Step",
Width = 55,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
partsGrid.CellValueChanged += PartsGrid_CellValueChanged;
partsGrid.CurrentCellDirtyStateChanged += (s, e) =>
{
if (partsGrid.IsCurrentCellDirty)
partsGrid.CommitEdit(DataGridViewDataErrorContexts.Commit);
};
}
private void SetupPlateGrid()
{
plateGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "Size",
HeaderText = "Size",
Width = 120,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
plateGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "Cost",
HeaderText = "Cost",
Width = 70,
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
});
plateGrid.CellValidating += PlateGrid_CellValidating;
}
private void LoadDrawings(Nest nest)
{
var items = new List<DataGridViewItem>();
dataGridView1.Rows.Clear();
foreach (var drawing in nest.Drawings)
items.Add(GetDataGridViewItem(drawing));
dataGridView1.DataSource = items;
partsGrid.DataSource = items;
UpdateSummary();
}
public List<NestItem> GetNestItems()
{
var nestItems = new List<NestItem>();
var gridItems = dataGridView1.DataSource as List<DataGridViewItem>;
var gridItems = partsGrid.DataSource as List<DataGridViewItem>;
if (gridItems == null)
return nestItems;
@@ -58,6 +173,139 @@ namespace OpenNest.Forms
return nestItems;
}
public List<PlateOption> GetPlateOptions()
{
var result = new List<PlateOption>();
var gridItems = plateGrid.DataSource as List<PlateOptionItem>;
if (gridItems == null) return result;
foreach (var item in gridItems)
{
if (!TryParseSize(item.Size, out var width, out var length))
continue;
if (width <= 0 || length <= 0)
continue;
result.Add(new PlateOption
{
Width = width,
Length = length,
Cost = item.Cost,
});
}
return result;
}
public void LoadPlateOptions(List<PlateOption> options, double salvageRate)
{
if (options != null && options.Count > 0)
{
var items = options.Select(o => new PlateOptionItem
{
Size = FormatSize(o.Width, o.Length),
Cost = o.Cost,
}).ToList();
plateGrid.DataSource = items;
optimizePlateSizeBox.Checked = true;
}
SalvageRate = salvageRate;
}
private void LoadDefaultPlateOptions()
{
var items = new List<PlateOptionItem>
{
new() { Size = "48 x 96", Cost = 0 },
new() { Size = "48 x 120", Cost = 0 },
new() { Size = "48 x 144", Cost = 0 },
new() { Size = "60 x 96", Cost = 0 },
new() { Size = "60 x 120", Cost = 0 },
new() { Size = "60 x 144", Cost = 0 },
new() { Size = "72 x 96", Cost = 0 },
new() { Size = "72 x 120", Cost = 0 },
new() { Size = "72 x 144", Cost = 0 },
};
plateGrid.DataSource = items;
}
private void optimizePlateSizeBox_CheckedChanged(object sender, EventArgs e)
{
SetPlateOptimizerVisible(optimizePlateSizeBox.Checked);
}
private void SetPlateOptimizerVisible(bool visible)
{
plateGrid.Visible = visible;
salvageRateLabel.Visible = visible;
salvageRateBox.Visible = visible;
salvageRatePercentLabel.Visible = visible;
}
private void UpdateSummary()
{
var gridItems = partsGrid.DataSource as List<DataGridViewItem>;
if (gridItems == null)
{
summaryLabel.Text = "";
return;
}
var totalQty = gridItems.Sum(i => System.Math.Max(0, i.Quantity));
var drawingCount = gridItems.Count(i => i.Quantity > 0);
summaryLabel.Text = $"{totalQty} parts across {drawingCount} drawings";
}
private void PartsGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0) return;
if (partsGrid.Columns[e.ColumnIndex].DataPropertyName == "Quantity")
UpdateSummary();
}
private void PlateGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
if (plateGrid.Columns[e.ColumnIndex].DataPropertyName != "Size")
return;
var value = e.FormattedValue?.ToString();
if (string.IsNullOrWhiteSpace(value))
return;
if (!TryParseSize(value, out _, out _))
{
e.Cancel = true;
plateGrid.Rows[e.RowIndex].ErrorText = "Enter size as W x L (e.g. 48 x 96)";
}
else
{
plateGrid.Rows[e.RowIndex].ErrorText = "";
}
}
private static bool TryParseSize(string value, out double width, out double length)
{
width = 0;
length = 0;
if (string.IsNullOrWhiteSpace(value)) return false;
var match = SizePattern.Match(value.Trim());
if (!match.Success) return false;
width = double.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture);
length = double.Parse(match.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture);
return true;
}
private static string FormatSize(double width, double length)
{
return $"{width:G} x {length:G}";
}
private void PartsGrid_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
MessageBox.Show("Invalid input. Expected input type is " +
partsGrid[e.ColumnIndex, e.RowIndex].ValueType.Name);
}
private DataGridViewItem GetDataGridViewItem(Drawing dwg)
{
var item = new DataGridViewItem();
@@ -71,11 +319,6 @@ namespace OpenNest.Forms
return item;
}
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
MessageBox.Show("Invalid input. Expected input type is " + dataGridView1[e.ColumnIndex, e.RowIndex].ValueType.Name);
}
private class DataGridViewItem
{
internal Drawing RefDrawing { get; set; }
@@ -92,17 +335,20 @@ namespace OpenNest.Forms
public int Priority { get; set; }
[Browsable(false)] // hide until implemented
[DisplayName("Rotation Start")]
[DisplayName("Rot Start")]
public double RotationStart { get; set; }
[Browsable(false)] // hide until implemented
[DisplayName("Rotation End")]
[DisplayName("Rot End")]
public double RotationEnd { get; set; }
[Browsable(false)] // hide until implemented
[DisplayName("Step Angle")]
[DisplayName("Step")]
public double StepAngle { get; set; }
}
private class PlateOptionItem
{
public string Size { get; set; }
public double Cost { get; set; }
}
}
}
+1
View File
@@ -420,6 +420,7 @@ namespace OpenNest.Forms
var drawingName = Path.GetFileNameWithoutExtension(part.DxfPath);
var drawing = new Drawing(drawingName);
drawing.Color = Drawing.GetNextColor();
drawing.Source.Path = part.DxfPath;
drawing.Quantity.Required = part.Qty ?? 1;
drawing.Material = new Material(material);
+14 -8
View File
@@ -19,10 +19,21 @@ namespace OpenNest.Forms
{
public partial class CadConverterForm : Form
{
private static int colorIndex;
private SimplifierViewerForm simplifierViewer;
private bool staleProgram = true;
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if ((keyData == Keys.Up || keyData == Keys.Down) && !fileList.Focused)
{
fileList.ProcessArrowKey(keyData);
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
public CadConverterForm()
{
InitializeComponent();
@@ -611,7 +622,7 @@ namespace OpenNest.Forms
continue;
var drawing = new Drawing(item.Name);
drawing.Color = GetNextColor();
drawing.Color = Drawing.GetNextColor();
drawing.Customer = item.Customer;
drawing.Source.Path = item.Path;
drawing.Quantity.Required = item.Quantity;
@@ -660,12 +671,7 @@ namespace OpenNest.Forms
}
private static Color GetNextColor()
{
var color = ColorScheme.PartColors[colorIndex % ColorScheme.PartColors.Length];
colorIndex++;
return color;
}
private static Color GetNextColor() => Drawing.GetNextColor();
private static bool IsDirectoryWritable(string path)
{
+222 -169
View File
@@ -28,224 +28,271 @@
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(EditNestForm));
this.splitContainer = new System.Windows.Forms.SplitContainer();
this.tabControl1 = new System.Windows.Forms.TabControl();
this.tabPage1 = new System.Windows.Forms.TabPage();
this.platesListView = new System.Windows.Forms.ListView();
this.plateNumColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.sizeColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.qtyColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
this.tabPage2 = new System.Windows.Forms.TabPage();
this.drawingListBox1 = new OpenNest.Controls.DrawingListBox();
this.toolStrip2 = new System.Windows.Forms.ToolStrip();
this.toolStripButton2 = new System.Windows.Forms.ToolStripButton();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.toolStripButton3 = new System.Windows.Forms.ToolStripButton();
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
this.splitContainer.Panel1.SuspendLayout();
this.splitContainer.SuspendLayout();
this.tabControl1.SuspendLayout();
this.tabPage1.SuspendLayout();
this.toolStrip1.SuspendLayout();
this.tabPage2.SuspendLayout();
this.toolStrip2.SuspendLayout();
this.SuspendLayout();
splitContainer = new System.Windows.Forms.SplitContainer();
tabControl1 = new System.Windows.Forms.TabControl();
tabPage1 = new System.Windows.Forms.TabPage();
platesListView = new System.Windows.Forms.ListView();
plateNumColumn = new System.Windows.Forms.ColumnHeader();
sizeColumn = new System.Windows.Forms.ColumnHeader();
qtyColumn = new System.Windows.Forms.ColumnHeader();
partsColumn = new System.Windows.Forms.ColumnHeader();
utilColumn = new System.Windows.Forms.ColumnHeader();
toolStrip1 = new System.Windows.Forms.ToolStrip();
toolStripLabel1 = new System.Windows.Forms.ToolStripButton();
toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
toolStripLabel2 = new System.Windows.Forms.ToolStripButton();
tabPage2 = new System.Windows.Forms.TabPage();
drawingListBox1 = new OpenNest.Controls.DrawingListBox();
toolStrip2 = new System.Windows.Forms.ToolStrip();
toolStripButton2 = new System.Windows.Forms.ToolStripButton();
toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
toolStripButton3 = new System.Windows.Forms.ToolStripButton();
toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
hideNestedButton = new System.Windows.Forms.ToolStripButton();
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
splitContainer.Panel1.SuspendLayout();
splitContainer.SuspendLayout();
tabControl1.SuspendLayout();
tabPage1.SuspendLayout();
toolStrip1.SuspendLayout();
tabPage2.SuspendLayout();
toolStrip2.SuspendLayout();
SuspendLayout();
//
// splitContainer
//
this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
this.splitContainer.Location = new System.Drawing.Point(0, 0);
this.splitContainer.Name = "splitContainer";
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1;
splitContainer.Location = new System.Drawing.Point(0, 0);
splitContainer.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
this.splitContainer.Panel1.Controls.Add(this.tabControl1);
this.splitContainer.Size = new System.Drawing.Size(735, 396);
this.splitContainer.SplitterDistance = 241;
this.splitContainer.TabIndex = 8;
splitContainer.Panel1.Controls.Add(tabControl1);
splitContainer.Size = new System.Drawing.Size(858, 457);
splitContainer.SplitterDistance = 281;
splitContainer.SplitterWidth = 5;
splitContainer.TabIndex = 8;
//
// tabControl1
//
this.tabControl1.Controls.Add(this.tabPage1);
this.tabControl1.Controls.Add(this.tabPage2);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.ItemSize = new System.Drawing.Size(100, 22);
this.tabControl1.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0;
this.tabControl1.Size = new System.Drawing.Size(241, 396);
this.tabControl1.SizeMode = System.Windows.Forms.TabSizeMode.Fixed;
this.tabControl1.TabIndex = 1;
tabControl1.Controls.Add(tabPage1);
tabControl1.Controls.Add(tabPage2);
tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
tabControl1.ItemSize = new System.Drawing.Size(100, 22);
tabControl1.Location = new System.Drawing.Point(0, 0);
tabControl1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
tabControl1.Name = "tabControl1";
tabControl1.SelectedIndex = 0;
tabControl1.Size = new System.Drawing.Size(281, 457);
tabControl1.SizeMode = System.Windows.Forms.TabSizeMode.Fixed;
tabControl1.TabIndex = 1;
//
// tabPage1
//
this.tabPage1.Controls.Add(this.platesListView);
this.tabPage1.Controls.Add(this.toolStrip1);
this.tabPage1.Location = new System.Drawing.Point(4, 22);
this.tabPage1.Name = "tabPage1";
this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
this.tabPage1.Size = new System.Drawing.Size(233, 370);
this.tabPage1.TabIndex = 0;
this.tabPage1.Text = "Plates";
this.tabPage1.UseVisualStyleBackColor = true;
tabPage1.Controls.Add(platesListView);
tabPage1.Controls.Add(toolStrip1);
tabPage1.Location = new System.Drawing.Point(4, 26);
tabPage1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
tabPage1.Name = "tabPage1";
tabPage1.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
tabPage1.Size = new System.Drawing.Size(273, 427);
tabPage1.TabIndex = 0;
tabPage1.Text = "Plates";
tabPage1.UseVisualStyleBackColor = true;
//
// platesListView
//
this.platesListView.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.platesListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.plateNumColumn,
this.sizeColumn,
this.qtyColumn});
this.platesListView.Dock = System.Windows.Forms.DockStyle.Fill;
this.platesListView.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.platesListView.FullRowSelect = true;
this.platesListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
this.platesListView.HideSelection = false;
this.platesListView.Location = new System.Drawing.Point(3, 34);
this.platesListView.MultiSelect = false;
this.platesListView.Name = "platesListView";
this.platesListView.Size = new System.Drawing.Size(227, 333);
this.platesListView.TabIndex = 1;
this.platesListView.UseCompatibleStateImageBehavior = false;
this.platesListView.View = System.Windows.Forms.View.Details;
this.platesListView.DoubleClick += new System.EventHandler(this.EditSelectedPlate_Click);
platesListView.BorderStyle = System.Windows.Forms.BorderStyle.None;
platesListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { plateNumColumn, sizeColumn, qtyColumn, partsColumn, utilColumn });
platesListView.Dock = System.Windows.Forms.DockStyle.Fill;
platesListView.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
platesListView.FullRowSelect = true;
platesListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
platesListView.Location = new System.Drawing.Point(4, 30);
platesListView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
platesListView.MultiSelect = false;
platesListView.Name = "platesListView";
platesListView.Size = new System.Drawing.Size(265, 394);
platesListView.TabIndex = 1;
platesListView.UseCompatibleStateImageBehavior = false;
platesListView.View = System.Windows.Forms.View.Details;
platesListView.DoubleClick += EditSelectedPlate_Click;
//
// plateNumColumn
//
this.plateNumColumn.Text = "";
this.plateNumColumn.Width = 30;
plateNumColumn.Text = "";
plateNumColumn.Width = 30;
//
// sizeColumn
//
this.sizeColumn.Text = "Size";
this.sizeColumn.Width = 80;
sizeColumn.Text = "Size";
sizeColumn.Width = 80;
//
// qtyColumn
//
this.qtyColumn.Text = "Qty";
qtyColumn.Text = "Qty";
//
// partsColumn
//
partsColumn.Text = "Parts";
partsColumn.Width = 45;
//
// utilColumn
//
utilColumn.Text = "Util";
utilColumn.Width = 45;
//
// toolStrip1
//
this.toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.toolStrip1.ImageScalingSize = new System.Drawing.Size(20, 20);
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripButton1});
this.toolStrip1.Location = new System.Drawing.Point(3, 3);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(227, 31);
this.toolStrip1.TabIndex = 2;
this.toolStrip1.Text = "toolStrip1";
toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
toolStrip1.ImageScalingSize = new System.Drawing.Size(20, 20);
toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripLabel1, toolStripSeparator3, toolStripLabel2 });
toolStrip1.Location = new System.Drawing.Point(4, 3);
toolStrip1.Name = "toolStrip1";
toolStrip1.Size = new System.Drawing.Size(265, 27);
toolStrip1.TabIndex = 2;
toolStrip1.Text = "toolStrip1";
//
// toolStripButton1
// toolStripLabel1
//
toolStripLabel1.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
toolStripLabel1.Image = Properties.Resources.plus;
toolStripLabel1.Name = "toolStripLabel1";
toolStripLabel1.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
toolStripLabel1.Size = new System.Drawing.Size(34, 24);
toolStripLabel1.Text = "Remove Selected Plate";
toolStripLabel1.Click += toolStripLabel1_Click;
//
// toolStripSeparator3
//
toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new System.Drawing.Size(6, 27);
//
// toolStripLabel2
//
toolStripLabel2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
toolStripLabel2.Image = Properties.Resources.delete;
toolStripLabel2.Name = "toolStripLabel2";
toolStripLabel2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
toolStripLabel2.Size = new System.Drawing.Size(34, 24);
toolStripLabel2.Text = "Remove Selected";
toolStripLabel2.Click += toolStripLabel2_Click;
//
this.toolStripButton1.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
this.toolStripButton1.Image = global::OpenNest.Properties.Resources.clock;
this.toolStripButton1.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.toolStripButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripButton1.Name = "toolStripButton1";
this.toolStripButton1.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
this.toolStripButton1.Size = new System.Drawing.Size(38, 28);
this.toolStripButton1.Text = "Calculate Cut Time";
this.toolStripButton1.Click += new System.EventHandler(this.CalculateSelectedPlateCutTime_Click);
//
// tabPage2
//
this.tabPage2.Controls.Add(this.drawingListBox1);
this.tabPage2.Controls.Add(this.toolStrip2);
this.tabPage2.Location = new System.Drawing.Point(4, 26);
this.tabPage2.Name = "tabPage2";
this.tabPage2.Padding = new System.Windows.Forms.Padding(3);
this.tabPage2.Size = new System.Drawing.Size(233, 366);
this.tabPage2.TabIndex = 1;
this.tabPage2.Text = "Drawings";
this.tabPage2.UseVisualStyleBackColor = true;
tabPage2.Controls.Add(drawingListBox1);
tabPage2.Controls.Add(toolStrip2);
tabPage2.Location = new System.Drawing.Point(4, 26);
tabPage2.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
tabPage2.Name = "tabPage2";
tabPage2.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3);
tabPage2.Size = new System.Drawing.Size(273, 427);
tabPage2.TabIndex = 1;
tabPage2.Text = "Drawings";
tabPage2.UseVisualStyleBackColor = true;
//
// drawingListBox1
//
this.drawingListBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.drawingListBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.drawingListBox1.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
this.drawingListBox1.FormattingEnabled = true;
this.drawingListBox1.ItemHeight = 85;
this.drawingListBox1.Location = new System.Drawing.Point(3, 34);
this.drawingListBox1.Name = "drawingListBox1";
this.drawingListBox1.Size = new System.Drawing.Size(227, 329);
this.drawingListBox1.TabIndex = 1;
this.drawingListBox1.Units = OpenNest.Units.Inches;
this.drawingListBox1.Click += new System.EventHandler(this.drawingListBox1_Click);
this.drawingListBox1.DoubleClick += new System.EventHandler(this.drawingListBox1_DoubleClick);
drawingListBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
drawingListBox1.Dock = System.Windows.Forms.DockStyle.Fill;
drawingListBox1.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
drawingListBox1.FormattingEnabled = true;
drawingListBox1.HideDepletedParts = false;
drawingListBox1.HideQuantity = false;
drawingListBox1.ItemHeight = 85;
drawingListBox1.Location = new System.Drawing.Point(4, 30);
drawingListBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
drawingListBox1.Name = "drawingListBox1";
drawingListBox1.Size = new System.Drawing.Size(265, 394);
drawingListBox1.TabIndex = 1;
drawingListBox1.Units = Units.Inches;
drawingListBox1.Click += drawingListBox1_Click;
drawingListBox1.DoubleClick += drawingListBox1_DoubleClick;
//
// toolStrip2
//
this.toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
this.toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripButton2,
this.toolStripSeparator1,
this.toolStripButton3});
this.toolStrip2.Location = new System.Drawing.Point(3, 3);
this.toolStrip2.Name = "toolStrip2";
this.toolStrip2.Size = new System.Drawing.Size(227, 31);
this.toolStrip2.TabIndex = 3;
this.toolStrip2.Text = "toolStrip2";
toolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
toolStrip2.ImageScalingSize = new System.Drawing.Size(20, 20);
toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripButton2, toolStripSeparator1, toolStripButton3, toolStripSeparator2, hideNestedButton });
toolStrip2.Location = new System.Drawing.Point(4, 3);
toolStrip2.Name = "toolStrip2";
toolStrip2.Size = new System.Drawing.Size(265, 27);
toolStrip2.TabIndex = 3;
toolStrip2.Text = "toolStrip2";
//
// toolStripButton2
//
this.toolStripButton2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
this.toolStripButton2.Image = global::OpenNest.Properties.Resources.import;
this.toolStripButton2.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.toolStripButton2.Name = "toolStripButton2";
this.toolStripButton2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
this.toolStripButton2.RightToLeft = System.Windows.Forms.RightToLeft.No;
this.toolStripButton2.Size = new System.Drawing.Size(38, 28);
this.toolStripButton2.Text = "Import Drawings";
this.toolStripButton2.Click += new System.EventHandler(this.ImportDrawings_Click);
toolStripButton2.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
toolStripButton2.Image = (System.Drawing.Image)resources.GetObject("toolStripButton2.Image");
toolStripButton2.Name = "toolStripButton2";
toolStripButton2.Overflow = System.Windows.Forms.ToolStripItemOverflow.Never;
toolStripButton2.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
toolStripButton2.RightToLeft = System.Windows.Forms.RightToLeft.No;
toolStripButton2.Size = new System.Drawing.Size(34, 24);
toolStripButton2.Text = "Import Drawings";
toolStripButton2.Click += ImportDrawings_Click;
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(6, 31);
toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new System.Drawing.Size(6, 27);
//
// toolStripButton3
//
this.toolStripButton3.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
this.toolStripButton3.Image = global::OpenNest.Properties.Resources.clear;
this.toolStripButton3.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.toolStripButton3.Name = "toolStripButton3";
this.toolStripButton3.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
this.toolStripButton3.RightToLeft = System.Windows.Forms.RightToLeft.No;
this.toolStripButton3.Size = new System.Drawing.Size(38, 28);
this.toolStripButton3.Text = "Cleanup unused Drawings";
this.toolStripButton3.Click += new System.EventHandler(this.CleanUnusedDrawings_Click);
toolStripButton3.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
toolStripButton3.Image = (System.Drawing.Image)resources.GetObject("toolStripButton3.Image");
toolStripButton3.Name = "toolStripButton3";
toolStripButton3.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
toolStripButton3.RightToLeft = System.Windows.Forms.RightToLeft.No;
toolStripButton3.Size = new System.Drawing.Size(34, 24);
toolStripButton3.Text = "Cleanup unused Drawings";
toolStripButton3.Click += CleanUnusedDrawings_Click;
//
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new System.Drawing.Size(6, 27);
//
// hideNestedButton
//
hideNestedButton.CheckOnClick = true;
hideNestedButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
hideNestedButton.Image = (System.Drawing.Image)resources.GetObject("hideNestedButton.Image");
hideNestedButton.Name = "hideNestedButton";
hideNestedButton.Padding = new System.Windows.Forms.Padding(5, 0, 5, 0);
hideNestedButton.Size = new System.Drawing.Size(34, 24);
hideNestedButton.Text = "Hide fully nested drawings";
hideNestedButton.CheckedChanged += HideNestedButton_CheckedChanged;
//
// EditNestForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(735, 396);
this.Controls.Add(this.splitContainer);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MinimumSize = new System.Drawing.Size(200, 198);
this.Name = "EditNestForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "EditForm";
this.splitContainer.Panel1.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
this.splitContainer.ResumeLayout(false);
this.tabControl1.ResumeLayout(false);
this.tabPage1.ResumeLayout(false);
this.tabPage1.PerformLayout();
this.toolStrip1.ResumeLayout(false);
this.toolStrip1.PerformLayout();
this.tabPage2.ResumeLayout(false);
this.tabPage2.PerformLayout();
this.toolStrip2.ResumeLayout(false);
this.toolStrip2.PerformLayout();
this.ResumeLayout(false);
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(858, 457);
Controls.Add(splitContainer);
Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon");
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MinimumSize = new System.Drawing.Size(231, 222);
Name = "EditNestForm";
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "EditForm";
splitContainer.Panel1.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
splitContainer.ResumeLayout(false);
tabControl1.ResumeLayout(false);
tabPage1.ResumeLayout(false);
tabPage1.PerformLayout();
toolStrip1.ResumeLayout(false);
toolStrip1.PerformLayout();
tabPage2.ResumeLayout(false);
tabPage2.PerformLayout();
toolStrip2.ResumeLayout(false);
toolStrip2.PerformLayout();
ResumeLayout(false);
}
@@ -258,13 +305,19 @@
private System.Windows.Forms.ColumnHeader sizeColumn;
private System.Windows.Forms.ColumnHeader qtyColumn;
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripButton toolStripButton1;
private System.Windows.Forms.TabPage tabPage2;
private Controls.DrawingListBox drawingListBox1;
private System.Windows.Forms.ColumnHeader plateNumColumn;
private System.Windows.Forms.ColumnHeader partsColumn;
private System.Windows.Forms.ColumnHeader utilColumn;
private System.Windows.Forms.ToolStrip toolStrip2;
private System.Windows.Forms.ToolStripButton toolStripButton2;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripButton toolStripButton3;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripButton hideNestedButton;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
private System.Windows.Forms.ToolStripButton toolStripLabel1;
private System.Windows.Forms.ToolStripButton toolStripLabel2;
}
}
+129 -142
View File
@@ -23,15 +23,18 @@ namespace OpenNest.Forms
{
public event EventHandler PlateChanged;
public readonly Nest Nest;
public readonly Document Document;
public readonly PlateView PlateView;
public readonly PlateManager PlateManager;
public Nest Nest => Document.Nest;
private readonly Timer updateDrawingListTimer;
private bool updatingPlateList;
private Panel plateHeaderPanel;
private Label plateInfoLabel;
private Button btnFirstPlate;
private Button btnRemovePlate;
private Button btnPreviousPlate;
private Button btnNextPlate;
@@ -67,13 +70,10 @@ namespace OpenNest.Forms
platesListView.SelectedIndexChanged += (sender, e) =>
{
if (platesListView.SelectedIndices.Count == 0)
if (updatingPlateList || platesListView.SelectedIndices.Count == 0)
return;
CurrentPlateIndex = platesListView.SelectedIndices[0];
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged(false);
PlateManager.LoadAt(platesListView.SelectedIndices[0]);
};
}
@@ -100,16 +100,16 @@ namespace OpenNest.Forms
var btnSize = new System.Drawing.Size(28, 28);
btnFirstPlate = CreateNavButton(Resources.move_first);
btnFirstPlate.Click += (s, e) => LoadFirstPlate();
btnFirstPlate.Click += (s, e) => PlateManager.LoadFirst();
btnPreviousPlate = CreateNavButton(Resources.move_previous);
btnPreviousPlate.Click += (s, e) => LoadPreviousPlate();
btnPreviousPlate.Click += (s, e) => PlateManager.LoadPrevious();
btnNextPlate = CreateNavButton(Resources.move_next);
btnNextPlate.Click += (s, e) => LoadNextPlate();
btnNextPlate.Click += (s, e) => PlateManager.LoadNext();
btnLastPlate = CreateNavButton(Resources.move_last);
btnLastPlate.Click += (s, e) => LoadLastPlate();
btnLastPlate.Click += (s, e) => PlateManager.LoadLast();
// Panel that holds the nav buttons and centers itself in the header
var navPanel = new Panel
@@ -126,18 +126,8 @@ namespace OpenNest.Forms
navPanel.Controls.AddRange(new Control[] { btnFirstPlate, btnPreviousPlate, btnNextPlate, btnLastPlate });
btnRemovePlate = CreateNavButton(Resources.remove);
btnRemovePlate.Dock = DockStyle.Right;
btnRemovePlate.Click += (s, e) => RemoveCurrentPlate();
var btnAddPlate = CreateNavButton(Resources.add);
btnAddPlate.Dock = DockStyle.Right;
btnAddPlate.Click += (s, e) => Nest.CreatePlate();
plateHeaderPanel.Controls.Add(navPanel);
plateHeaderPanel.Controls.Add(plateInfoLabel);
plateHeaderPanel.Controls.Add(btnRemovePlate);
plateHeaderPanel.Controls.Add(btnAddPlate);
// Center the nav panel on resize
CenterNavPanel(navPanel);
@@ -210,94 +200,32 @@ namespace OpenNest.Forms
};
updateDrawingListTimer.Elapsed += drawingListUpdateTimer_Elapsed;
Nest = nest;
Nest.Plates.ItemAdded += Plates_PlateAdded;
Nest.Plates.ItemRemoved += Plates_PlateRemoved;
Document = new Document { Nest = nest };
if (Nest.Plates.Count == 0)
Nest.CreatePlate();
PlateManager = new PlateManager(nest);
PlateManager.CurrentPlateChanged += PlateManager_CurrentPlateChanged;
PlateManager.PlateListChanged += PlateManager_PlateListChanged;
PlateManager.EnsureSentinel();
UpdatePlateList();
UpdateDrawingList();
UpdateRemovePlateButton();
LoadFirstPlate();
PlateManager.LoadFirst();
Text = Nest.Name;
drawingListBox1.Units = Nest.Units;
drawingListBox1.DeleteRequested += drawingListBox1_DeleteRequested;
}
public string LastSavePath { get; private set; }
public DateTime LastSaveDate { get; private set; }
public int CurrentPlateIndex { get; private set; }
public int PlateCount
{
get { return Nest.Plates.Count; }
}
public void LoadFirstPlate()
{
if (Nest.Plates.Count > 0)
{
CurrentPlateIndex = 0;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
}
}
public void LoadLastPlate()
{
if (Nest.Plates.Count > 0)
{
CurrentPlateIndex = Nest.Plates.Count - 1;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
}
}
public bool LoadNextPlate()
{
if (CurrentPlateIndex + 1 >= Nest.Plates.Count)
return false;
CurrentPlateIndex++;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
return true;
}
public bool LoadPreviousPlate()
{
if (Nest.Plates.Count == 0 || CurrentPlateIndex - 1 < 0)
return false;
CurrentPlateIndex--;
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
PlateView.ZoomToFit();
FirePlateChanged();
return true;
}
public bool IsFirstPlate()
{
return (Nest.Plates.Count == 0 || (CurrentPlateIndex - 1) < 0);
}
public bool IsLastPlate()
{
return CurrentPlateIndex + 1 >= Nest.Plates.Count;
}
public void UpdatePlateList()
{
updatingPlateList = true;
var focused = ContainsFocus ? GetFocusedControl() : null;
platesListView.BeginUpdate();
platesListView.Items.Clear();
var items = new ListViewItem[Nest.Plates.Count];
@@ -310,6 +238,23 @@ namespace OpenNest.Forms
}
platesListView.Items.AddRange(items);
if (PlateManager.CurrentIndex < platesListView.Items.Count)
platesListView.Items[PlateManager.CurrentIndex].Selected = true;
platesListView.EndUpdate();
updatingPlateList = false;
if (focused != null && focused != platesListView)
focused.Focus();
}
private Control GetFocusedControl()
{
var ctrl = this;
while (ctrl is ContainerControl container && container.ActiveControl != null)
return container.ActiveControl;
return ctrl;
}
public void UpdateDrawingList()
@@ -317,13 +262,18 @@ namespace OpenNest.Forms
drawingListBox1.Items.Clear();
foreach (var dwg in Nest.Drawings.OrderBy(d => d.Name).ToList())
{
if (hideNestedButton.Checked && dwg.Quantity.Required > 0 && dwg.Quantity.Remaining == 0)
continue;
drawingListBox1.Items.Add(dwg);
}
}
public void Save()
{
if (File.Exists(LastSavePath))
SaveAs(LastSavePath);
if (Document.HasSavePath)
SaveAs(Document.LastSavePath);
else
SaveAs();
}
@@ -345,14 +295,8 @@ namespace OpenNest.Forms
public void SaveAs(string path)
{
var name = Path.GetFileNameWithoutExtension(path);
Nest.Name = name;
Text = name;
LastSaveDate = DateTime.Now;
LastSavePath = path;
var writer = new NestWriter(Nest);
writer.Write(path);
Document.SaveAs(path);
Text = Document.Name;
}
public void SaveTemplate(string path)
@@ -397,7 +341,7 @@ namespace OpenNest.Forms
"Image as displayed (*.jpg)|*.jpg|" +
"Locations and rotations (*.txt)|*.txt";
dlg.FileName = string.Format("{0}-P{1}", Nest.Name, CurrentPlateIndex + 1);
dlg.FileName = string.Format("{0}-P{1}", Nest.Name, PlateManager.CurrentIndex + 1);
dlg.AddExtension = true;
dlg.DefaultExt = ".";
@@ -454,13 +398,13 @@ namespace OpenNest.Forms
public void ExportAll()
{
LoadFirstPlate();
PlateManager.LoadFirst();
do
{
if (!Export()) return;
}
while (LoadNextPlate());
while (PlateManager.LoadNext());
}
public void RotateCw()
@@ -581,7 +525,7 @@ namespace OpenNest.Forms
public void OpenCurrentPlate()
{
var plate = PlateView.Plate;
var name = string.Format("{0}-P{1}.dxf", Nest.Name, CurrentPlateIndex + 1);
var name = string.Format("{0}-P{1}.dxf", Nest.Name, PlateManager.CurrentIndex + 1);
var path = Path.Combine(Path.GetTempPath(), name);
var exporter = new DxfExporter();
exporter.ExportPlate(plate, path);
@@ -591,10 +535,7 @@ namespace OpenNest.Forms
public void RemoveCurrentPlate()
{
if (Nest.Plates.Count < 2)
return;
Nest.Plates.RemoveAt(CurrentPlateIndex);
PlateManager.RemoveCurrent();
}
public void AutoSequenceCurrentPlate()
@@ -672,33 +613,31 @@ namespace OpenNest.Forms
private void FirePlateChanged(bool updateListView = true)
{
if (updateListView)
platesListView.Items[CurrentPlateIndex].Selected = true;
if (updateListView && PlateManager.CurrentIndex < platesListView.Items.Count)
platesListView.Items[PlateManager.CurrentIndex].Selected = true;
UpdatePlateHeader();
if (PlateChanged != null)
PlateChanged.Invoke(this, EventArgs.Empty);
PlateChanged?.Invoke(this, EventArgs.Empty);
}
private void UpdatePlateHeader()
{
var plate = Nest.Plates.Count > 0 ? Nest.Plates[CurrentPlateIndex] : null;
var plate = PlateManager.CurrentPlate;
if (plate != null)
{
plateInfoLabel.Text = string.Format("Plate {0} of {1} | {2}",
CurrentPlateIndex + 1, Nest.Plates.Count, plate.Size);
PlateManager.CurrentIndex + 1, PlateManager.Count, plate.Size);
}
else
{
plateInfoLabel.Text = "No plates";
}
btnFirstPlate.Enabled = !IsFirstPlate();
btnPreviousPlate.Enabled = !IsFirstPlate();
btnNextPlate.Enabled = !IsLastPlate();
btnLastPlate.Enabled = !IsLastPlate();
btnFirstPlate.Enabled = !PlateManager.IsFirst;
btnPreviousPlate.Enabled = !PlateManager.IsFirst;
btnNextPlate.Enabled = !PlateManager.IsLast;
btnLastPlate.Enabled = !PlateManager.IsLast;
}
#region Overrides
@@ -722,6 +661,8 @@ namespace OpenNest.Forms
{
base.OnClosing(e);
PlateManager.Dispose();
Settings.Default.PlateViewDrawBounds = PlateView.DrawBounds;
Settings.Default.PlateViewDrawRapid = PlateView.DrawRapid;
Settings.Default.SplitterDistance = splitContainer.SplitterDistance;
@@ -926,34 +867,34 @@ namespace OpenNest.Forms
}
}
#endregion
#region Plate Collection Events
private void Plates_PlateRemoved(object sender, ItemRemovedEventArgs<Plate> e)
private void HideNestedButton_CheckedChanged(object sender, EventArgs e)
{
if (Nest.Plates.Count <= CurrentPlateIndex)
LoadLastPlate();
else
PlateView.Plate = Nest.Plates[CurrentPlateIndex];
UpdatePlateList();
UpdateRemovePlateButton();
PlateView.ZoomToFit();
UpdateDrawingList();
}
private void Plates_PlateAdded(object sender, ItemAddedEventArgs<Plate> e)
#endregion
#region PlateManager Events
private void PlateManager_CurrentPlateChanged(object sender, PlateChangedEventArgs e)
{
PlateView.Plate = PlateManager.CurrentPlate;
PlateView.ZoomToFit();
UpdatePlateHeader();
PlateChanged?.Invoke(this, EventArgs.Empty);
}
private void PlateManager_PlateListChanged(object sender, EventArgs e)
{
tabControl1.SelectedIndex = 0;
UpdatePlateList();
UpdatePlateHeader();
UpdateRemovePlateButton();
LoadLastPlate();
PlateView.ZoomToFit();
}
private void UpdateRemovePlateButton()
{
btnRemovePlate.Enabled = Nest.Plates.Count > 1;
toolStripLabel2.Enabled = PlateManager.CanRemoveCurrent;
}
#endregion
@@ -964,6 +905,13 @@ namespace OpenNest.Forms
item.Text = id.ToString();
item.SubItems.Add(plate.Size.ToString());
item.SubItems.Add(plate.Quantity.ToString());
var partCount = plate.Parts.Count(p => !p.BaseDrawing.IsCutOff);
item.SubItems.Add(partCount.ToString());
var util = plate.Utilization();
item.SubItems.Add(partCount > 0 ? $"{util:P0}" : "");
return item;
}
@@ -986,7 +934,10 @@ namespace OpenNest.Forms
drawingListBox1.Invoke(new MethodInvoker(() =>
{
drawingListBox1.Refresh();
if (hideNestedButton.Checked)
UpdateDrawingList();
else
drawingListBox1.Refresh();
}));
}
@@ -1013,6 +964,32 @@ namespace OpenNest.Forms
}
}
private void drawingListBox1_DeleteRequested(object sender, Drawing drawing)
{
var result = MessageBox.Show(
$"Delete drawing '{drawing.Name}' and all its parts from every plate?",
"Delete Drawing",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (result != DialogResult.Yes)
return;
foreach (var plate in Nest.Plates)
{
for (var i = plate.Parts.Count - 1; i >= 0; i--)
{
if (plate.Parts[i].BaseDrawing == drawing)
plate.Parts.RemoveAt(i);
}
}
Nest.Drawings.Remove(drawing);
UpdateDrawingList();
PlateView.Invalidate();
}
private void drawingListBox1_Click(object sender, EventArgs e)
{
addPart = true;
@@ -1032,5 +1009,15 @@ namespace OpenNest.Forms
addPart = false;
}
private void toolStripLabel2_Click(object sender, EventArgs e)
{
RemoveSelectedPlate_Click(sender, e);
}
private void toolStripLabel1_Click(object sender, EventArgs e)
{
AddPlate_Click(sender, e);
}
}
}
+632 -27
View File
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -124,6 +124,611 @@
<value>122, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="toolStripButton2.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
BlC5AAAAAElFTkSuQmCC
</value>
</data>
<data name="toolStripButton3.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
3Sz9f1mSMXfnTj0pAAAAAElFTkSuQmCC
</value>
</data>
<data name="hideNestedButton.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
</value>
</data>
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAUAQEAAAAEAIAAoQgAAVgAAADAwAAABACAAqCUAAH5CAAAgIAAAAQAgAKgQAAAmaAAAGBgAAAEA
+87 -30
View File
@@ -29,6 +29,17 @@ namespace OpenNest.Forms
private const float ZoomInFactor = 1.5f;
private const float ZoomOutFactor = 1.0f / ZoomInFactor;
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape && activeForm?.PlateView != null)
{
activeForm.PlateView.ProcessEscapeKey();
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
public MainForm()
{
InitializeComponent();
@@ -109,6 +120,7 @@ namespace OpenNest.Forms
{
NavigationEnableCheck();
UpdatePlateStatus();
mnuPlateRemove.Enabled = activeForm?.PlateManager.CanRemoveCurrent ?? false;
};
editForm.WindowState = windowState;
editForm.Show();
@@ -179,10 +191,10 @@ namespace OpenNest.Forms
}
else
{
mnuNestPreviousPlate.Enabled = !activeForm.IsFirstPlate();
mnuNestNextPlate.Enabled = !activeForm.IsLastPlate();
mnuNestFirstPlate.Enabled = activeForm.PlateCount > 0 && !activeForm.IsFirstPlate();
mnuNestLastPlate.Enabled = activeForm.PlateCount > 0 && !activeForm.IsLastPlate();
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;
}
}
@@ -195,10 +207,10 @@ namespace OpenNest.Forms
mnuPlate.Enabled = !locked;
// Lock plate navigation
mnuNestPreviousPlate.Enabled = !locked && activeForm != null && !activeForm.IsFirstPlate();
mnuNestNextPlate.Enabled = !locked && activeForm != null && !activeForm.IsLastPlate();
mnuNestFirstPlate.Enabled = !locked && activeForm != null && activeForm.PlateCount > 0 && !activeForm.IsFirstPlate();
mnuNestLastPlate.Enabled = !locked && activeForm != null && activeForm.PlateCount > 0 && !activeForm.IsLastPlate();
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()
@@ -227,8 +239,8 @@ namespace OpenNest.Forms
plateIndexStatusLabel.Text = string.Format(
"Plate: {0} of {1}",
activeForm.CurrentPlateIndex + 1,
activeForm.PlateCount);
activeForm.PlateManager.CurrentIndex + 1,
activeForm.PlateManager.Count);
plateSizeStatusLabel.Text = string.Format(
"Size: {0}",
@@ -712,7 +724,7 @@ namespace OpenNest.Forms
foreach (var part in result.Parts)
plate.Parts.Add(part);
activeForm.LoadLastPlate();
activeForm.PlateManager.LoadLast();
}
activeForm.Nest.UpdateDrawingQuantities();
@@ -832,25 +844,25 @@ namespace OpenNest.Forms
private void LoadFirstPlate_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
activeForm.LoadFirstPlate();
activeForm.PlateManager.LoadFirst();
}
private void LoadLastPlate_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
activeForm.LoadLastPlate();
activeForm.PlateManager.LoadLast();
}
private void LoadPreviousPlate_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
activeForm.LoadPreviousPlate();
activeForm.PlateManager.LoadPrevious();
}
private void LoadNextPlate_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
activeForm.LoadNextPlate();
activeForm.PlateManager.LoadNext();
}
private RemnantViewerForm remnantViewer;
@@ -900,14 +912,33 @@ namespace OpenNest.Forms
var form = new AutoNestForm(activeForm.Nest);
form.AllowPlateCreation = true;
if (activeForm.Nest.PlateOptions.Count > 0)
form.LoadPlateOptions(activeForm.Nest.PlateOptions, activeForm.Nest.SalvageRate);
if (form.ShowDialog() != System.Windows.Forms.DialogResult.OK)
return;
if (form.EngineName != null)
{
NestEngineRegistry.ActiveEngineName = form.EngineName;
engineComboBox.SelectedItem = form.EngineName;
}
var items = form.GetNestItems();
if (!items.Any(it => it.Quantity > 0))
return;
var optimizePlateSize = form.OptimizePlateSize;
var plateOptions = optimizePlateSize ? form.GetPlateOptions() : null;
var salvageRate = form.SalvageRate;
if (optimizePlateSize)
{
activeForm.Nest.PlateOptions = plateOptions;
activeForm.Nest.SalvageRate = salvageRate;
}
nestingCts = new CancellationTokenSource();
var progressForm = new NestProgressForm(nestingCts, showPlateRow: true);
progressForm.PreviewPlate = CreatePreviewPlate(activeForm.PlateView.Plate);
@@ -928,7 +959,8 @@ namespace OpenNest.Forms
try
{
await RunAutoNestAsync(items, progressForm, progress, nestingCts.Token);
await RunAutoNestAsync(items, progressForm, progress, nestingCts.Token,
plateOptions, salvageRate);
}
catch (Exception ex)
{
@@ -950,7 +982,9 @@ namespace OpenNest.Forms
List<NestItem> items,
NestProgressForm progressForm,
IProgress<NestProgress> progress,
CancellationToken token)
CancellationToken token,
List<PlateOption> plateOptions = null,
double salvageRate = 0.5)
{
const int maxPlates = 100;
@@ -964,7 +998,8 @@ namespace OpenNest.Forms
var plate = GetOrCreatePlate(progressForm);
var placed = await NestSinglePlateAsync(
plate, plateIndex, remaining, progressForm, progress, token);
plate, plateIndex, remaining, progressForm, progress, token,
plateOptions, salvageRate);
if (!placed)
break;
@@ -976,13 +1011,8 @@ namespace OpenNest.Forms
private Plate GetOrCreatePlate(NestProgressForm progressForm)
{
var currentPlate = activeForm.PlateView.Plate;
if (currentPlate.Parts.Count == 0)
return currentPlate;
var plate = activeForm.Nest.CreatePlate();
activeForm.LoadLastPlate();
var plate = activeForm.PlateManager.GetOrCreateEmpty();
activeForm.PlateManager.LoadLast();
progressForm.PreviewPlate = CreatePreviewPlate(plate);
return plate;
}
@@ -993,13 +1023,40 @@ namespace OpenNest.Forms
List<NestItem> items,
NestProgressForm progressForm,
IProgress<NestProgress> progress,
CancellationToken token)
CancellationToken token,
List<PlateOption> plateOptions = null,
double salvageRate = 0.5)
{
var engine = NestEngineRegistry.Create(plate);
engine.PlateNumber = plateIndex;
List<Part> nestParts;
var nestParts = await Task.Run(() =>
engine.Nest(items, progress, token));
if (plateOptions != null && plateOptions.Count > 0)
{
var result = await Task.Run(() =>
PlateOptimizer.Optimize(items, plateOptions, salvageRate, plate, progress, token));
if (result == null || result.Parts.Count == 0 ||
(token.IsCancellationRequested && !progressForm.Accepted))
return false;
plate.Size = new Geometry.Size(result.ChosenSize.Width, result.ChosenSize.Length);
nestParts = result.Parts;
// Deduct placed quantities — the optimizer clones items internally
// so the originals are untouched after dry runs.
foreach (var item in items)
{
var placed = nestParts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
}
else
{
var engine = NestEngineRegistry.Create(plate);
engine.PlateNumber = plateIndex;
nestParts = await Task.Run(() =>
engine.Nest(items, progress, token));
}
activeForm.PlateView.ClearPreviewParts();
+25 -5
View File
@@ -19,7 +19,7 @@ namespace OpenNest.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@@ -80,6 +80,16 @@ namespace OpenNest.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap clock {
get {
object obj = ResourceManager.GetObject("clock", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
@@ -89,13 +99,13 @@ namespace OpenNest.Properties {
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap clock {
internal static System.Drawing.Bitmap delete {
get {
object obj = ResourceManager.GetObject("clock", resourceCulture);
object obj = ResourceManager.GetObject("delete", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
@@ -170,6 +180,16 @@ namespace OpenNest.Properties {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap plus {
get {
object obj = ResourceManager.GetObject("plus", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
@@ -179,7 +199,7 @@ namespace OpenNest.Properties {
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
+50 -44
View File
@@ -118,67 +118,73 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="add" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="clear" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\clear.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
<data name="remove" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\remove.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="clock" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\clock.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="doc_new" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\doc_new.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="doc_open" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\doc_open.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="import" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\import.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="move_first" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_first.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="move_last" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_last.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="move_next" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_next.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="move_previous" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_previous.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="remove" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\remove.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="rotate_ccw" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\rotate_ccw.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="rotate_cw" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\rotate_cw.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="save" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\save.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="save_as" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\save_as.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
<data name="zoom_in" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\zoom_in.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="watermark" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\watermark.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="clear" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\clear.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="save_as" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\save_as.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="move_last" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_last.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="import" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\import.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="zoom_all" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\zoom_all.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="zoom_in" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\zoom_in.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
<data name="move_first" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_first.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="save" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\save.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="zoom_out" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\zoom_out.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="cutoff" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\cutoff.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
<data name="move_next" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_next.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="remnants" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\remnants.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="move_previous" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\move_previous.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="add" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="rotate_cw" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\rotate_cw.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="rotate_ccw" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\rotate_ccw.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="cutoff" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\cutoff.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="doc_open" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\doc_open.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="doc_new" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\doc_new.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="plus" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\plus.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="delete" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\delete.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB