Compare commits

...

17 Commits

Author SHA1 Message Date
aj cca70db547 fix: consolidate tail plates by upgrading instead of creating new plates
Three fixes to TryUpgradeOrNewPlate and a new post-pass:

1. Change ShouldUpgrade from < to <= so upgrade wins when costs are
   tied (e.g., all zero) — previously 0 < 0 was always false

2. Guard against "upgrades" that shrink a dimension — when options are
   sorted by cost and costs are equal, the next option may have a
   smaller length despite higher width (e.g., 72x96 after 60x144)

3. Revert plate size when upgrade fill fails — the plate was being
   resized before confirming parts fit, leaving it at the wrong size

4. Add TryConsolidateTailPlates post-pass: after all nesting, find the
   lowest-utilization new plate and try to absorb its parts into
   another plate via upgrade. Eliminates wasteful tail plates (e.g.,
   a 48x96 plate at 21% util for 2 parts that fit in upgraded space).

Real nest file: 6 plates → 5 plates, all 43 parts placed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:41:49 -04:00
aj 62d9dce0b1 refactor: simplify MultiPlateNester by converting to instance class
- Convert static class to instance with private constructor; shared
  parameters (template, plateOptions, salvageRate, minRemnantSize,
  progress, token) become fields, eliminating parameter threading
  across all private methods (10→3 params on Nest entry point stays
  unchanged; private methods drop from 7-9 params to 1-2)
- Extract FillAndPlace helper consolidating the repeated
  clone→fill→add-to-plate→deduct-quantity pattern (was duplicated
  in 4 call sites)
- Merge FindScrapZones/FindViableRemnants (98% duplicate) into single
  FindRemnants(plate, minRemnantSize, scrapOnly) method
- Extract ScoreZone helper and collapse duplicate normal/rotated
  orientation checks into single conditional
- Extract CreateNewPlateResult helper for repeated PlateResult
  construction + PlateOption lookup pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:08:44 -04:00
aj 1f88453d4c fix: recalculate remnants after each fill to prevent overlaps
The consolidation pass was iterating stale remnant lists after placing
parts, causing overlapping placements. Now recalculates remnants from
the plate after each fill operation. Also added plate options to the
real nest file integration test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:55:48 -04:00
aj 0697bebbc2 fix: defer small parts to consolidation pass for shared plates
Small parts no longer create their own plates during the main pass.
Instead they're deferred to the consolidation pass which fills them
into remaining space on existing plates, packing multiple drawing
types together. Drops from 9 plates to 4 on the test nest file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:47:50 -04:00
aj beadb14acc fix: consolidation pass packs medium/small parts onto shared plates
After the main single-pass placement, leftover items are now packed
together using the engine's multi-item Nest()/PackArea() methods
instead of creating one plate per drawing. First tries packing into
remaining space on existing plates, then creates shared plates for
anything still remaining.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:35:12 -04:00
aj 09f1140f54 fix: allow large parts to use remnant space on existing plates
Previously, parts classified as "Large" skipped all existing plates
and always created new ones. This caused one-unique-part-per-plate
behavior since most parts exceed half the plate dimension. Now large
parts search viable remnants on existing plates before creating new
ones, matching the intended part-first behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:24:17 -04:00
aj 7c918a2378 feat: integrate MultiPlateNester into MainForm auto-nest workflow
Wires part-first mode from AutoNestForm into RunAutoNestAsync: reads
PartFirstMode, SortOrder, MinRemnantSize, and AllowPlateCreation from
the form, passes them through to a new part-first branch that delegates
to MultiPlateNester.Nest instead of the plate-first loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:16:10 -04:00
aj feb08a5f60 feat: refactor AutoNestForm into Parts/Plates tabs with part-first controls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:13:50 -04:00
aj f1fd211ba5 fix: small parts use FindScrapZones not FindAllRemnants
Small parts must only go into scrap zones (both dims < minRemnantSize)
to preserve viable remnants. The implementer had inverted this, giving
small parts access to all remnants. Also fixed the test to verify
remnant preservation behavior and removed unused FindAllRemnants helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:11:10 -04:00
aj fd3c2462df feat: add MultiPlateNester.Nest orchestration method
Implements the main Nest() method that ties together sorting,
classification, and placement across multiple plates. The method
processes items largest-first, placing medium/small parts into
remnant zones on existing plates before creating new ones. Includes
private helpers: TryPlaceOnExistingPlates, PlaceOnNewPlates,
TryUpgradeOrNewPlate, FindAllRemnants, and CloneItem.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:07:24 -04:00
aj a4773748a1 feat: add plate creation and upgrade-vs-new evaluation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:56:08 -04:00
aj af57153269 feat: add scrap zone identification to MultiPlateNester
Adds IsScrapRemnant(), FindScrapZones(), and FindViableRemnants() to
MultiPlateNester. A remnant is scrap only when both dimensions fall
below the minimum remnant size threshold (AND logic, not OR).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:53:32 -04:00
aj 35e89600d0 feat: add part classification (large/medium/small) to MultiPlateNester
Introduces PartClass enum and Classify() static method that categorizes
parts as Large (exceeds half work area in either dimension), Medium
(area > 1/9 work area), or Small.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:52:27 -04:00
aj 89a4e6b981 feat: add MultiPlateNester with sorting logic
Implements static MultiPlateNester.SortItems with BoundingBoxArea and Size sort orders, covered by two passing xUnit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:48:57 -04:00
aj ebad3577dd feat: add MultiPlateResult type for part-first nesting 2026-04-06 13:46:51 -04:00
aj a8dc275da4 feat: add PartSortOrder enum for part-first nesting 2026-04-06 13:46:49 -04:00
aj d84becdaee fix: add bend detection and etch lines to BOM import path
BOM import was skipping BendDetectorRegistry.AutoDetect and
Bend.UpdateEtchEntities, so parts imported via BOM had no etch
or bend lines. Now matches the CadConverterForm import behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:19:17 -04:00
8 changed files with 1303 additions and 63 deletions
+597
View File
@@ -0,0 +1,597 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
{
public enum PartClass
{
Large,
Medium,
Small,
}
public class MultiPlateNester
{
private readonly Plate _template;
private readonly List<PlateOption> _plateOptions;
private readonly double _salvageRate;
private readonly double _minRemnantSize;
private readonly List<PlateResult> _platePool;
private readonly IProgress<NestProgress> _progress;
private readonly CancellationToken _token;
private MultiPlateNester(
Plate template, List<PlateOption> plateOptions,
double salvageRate, double minRemnantSize,
List<Plate> existingPlates,
IProgress<NestProgress> progress, CancellationToken token)
{
_template = template;
_plateOptions = plateOptions;
_salvageRate = salvageRate;
_minRemnantSize = minRemnantSize;
_platePool = InitializePlatePool(existingPlates);
_progress = progress;
_token = token;
}
// --- Static Utility Methods ---
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
{
switch (sortOrder)
{
case PartSortOrder.BoundingBoxArea:
return items
.OrderByDescending(i =>
{
var bb = i.Drawing.Program.BoundingBox();
return bb.Width * bb.Length;
})
.ToList();
case PartSortOrder.Size:
return items
.OrderByDescending(i =>
{
var bb = i.Drawing.Program.BoundingBox();
return System.Math.Max(bb.Width, bb.Length);
})
.ToList();
default:
return items.ToList();
}
}
public static PartClass Classify(Box partBounds, Box workArea)
{
var halfWidth = workArea.Width / 2.0;
var halfLength = workArea.Length / 2.0;
if (partBounds.Width > halfWidth || partBounds.Length > halfLength)
return PartClass.Large;
var workAreaArea = workArea.Width * workArea.Length;
var partArea = partBounds.Width * partBounds.Length;
if (partArea > workAreaArea / 9.0)
return PartClass.Medium;
return PartClass.Small;
}
public static bool IsScrapRemnant(Box remnant, double minRemnantSize)
{
return remnant.Width < minRemnantSize && remnant.Length < minRemnantSize;
}
public static List<Box> FindRemnants(Plate plate, double minRemnantSize, bool scrapOnly)
{
var remnants = RemnantFinder.FromPlate(plate).FindRemnants();
return remnants.Where(r => IsScrapRemnant(r, minRemnantSize) == scrapOnly).ToList();
}
public struct UpgradeDecision
{
public bool ShouldUpgrade;
public double UpgradeCost;
public double NewPlateCost;
}
public static Plate CreatePlate(Plate template, List<PlateOption> options, Box minBounds)
{
var plate = new Plate(template.Size)
{
PartSpacing = template.PartSpacing,
Quadrant = template.Quadrant,
};
plate.EdgeSpacing = new Spacing
{
Left = template.EdgeSpacing.Left,
Right = template.EdgeSpacing.Right,
Top = template.EdgeSpacing.Top,
Bottom = template.EdgeSpacing.Bottom,
};
if (options == null || options.Count == 0 || minBounds == null)
return plate;
var sorted = options.OrderBy(o => o.Cost).ToList();
foreach (var option in sorted)
{
var workW = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right;
var workL = option.Length - template.EdgeSpacing.Top - template.EdgeSpacing.Bottom;
var fitsNormal = workW >= minBounds.Width - Tolerance.Epsilon
&& workL >= minBounds.Length - Tolerance.Epsilon;
var fitsRotated = workW >= minBounds.Length - Tolerance.Epsilon
&& workL >= minBounds.Width - Tolerance.Epsilon;
if (fitsNormal || fitsRotated)
{
plate.Size = new Size(option.Width, option.Length);
return plate;
}
}
return plate;
}
public static UpgradeDecision EvaluateUpgradeVsNew(
PlateOption currentSize,
PlateOption upgradeSize,
PlateOption newPlateSize,
double salvageRate,
double estimatedNewPlateUtilization)
{
var upgradeCost = upgradeSize.Cost - currentSize.Cost;
var newPlateCost = newPlateSize.Cost;
var remnantFraction = 1.0 - estimatedNewPlateUtilization;
var salvageCredit = remnantFraction * newPlateSize.Cost * salvageRate;
var netNewCost = newPlateCost - salvageCredit;
return new UpgradeDecision
{
ShouldUpgrade = upgradeCost <= netNewCost,
UpgradeCost = upgradeCost,
NewPlateCost = netNewCost,
};
}
// --- Main Entry Point ---
public static MultiPlateResult Nest(
List<NestItem> items,
Plate template,
List<PlateOption> plateOptions,
double salvageRate,
PartSortOrder sortOrder,
double minRemnantSize,
bool allowPlateCreation,
List<Plate> existingPlates,
IProgress<NestProgress> progress,
CancellationToken token)
{
var nester = new MultiPlateNester(template, plateOptions, salvageRate,
minRemnantSize, existingPlates, progress, token);
return nester.Run(items, sortOrder, allowPlateCreation);
}
// --- Private Helpers ---
private static double ScoreZone(Box zone, Box partBounds)
{
var fitsNormal = zone.Length >= partBounds.Length && zone.Width >= partBounds.Width;
var fitsRotated = zone.Length >= partBounds.Width && zone.Width >= partBounds.Length;
if (!fitsNormal && !fitsRotated)
return -1;
return (partBounds.Length * partBounds.Width) / zone.Area();
}
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
{
var engine = NestEngineRegistry.Create(pr.Plate);
var clonedItem = CloneItem(item);
var parts = engine.Fill(clonedItem, zone, _progress, _token);
if (parts.Count > 0)
{
pr.Plate.Parts.AddRange(parts);
pr.Parts.AddRange(parts);
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
}
return parts.Count;
}
private PlateResult CreateNewPlateResult(Plate plate)
{
var pr = new PlateResult { Plate = plate, IsNew = true };
if (_plateOptions != null)
{
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
}
return pr;
}
private static NestItem CloneItem(NestItem item)
{
return new NestItem
{
Drawing = item.Drawing,
Priority = item.Priority,
Quantity = item.Quantity,
StepAngle = item.StepAngle,
RotationStart = item.RotationStart,
RotationEnd = item.RotationEnd,
};
}
private static List<PlateResult> InitializePlatePool(List<Plate> existingPlates)
{
var pool = new List<PlateResult>();
if (existingPlates != null)
{
foreach (var plate in existingPlates)
pool.Add(new PlateResult { Plate = plate, IsNew = false });
}
return pool;
}
// --- Orchestration ---
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
{
var result = new MultiPlateResult();
if (items == null || items.Count == 0)
return result;
var sorted = SortItems(items.Where(i => i.Quantity > 0).ToList(), sortOrder);
foreach (var item in sorted)
{
if (_token.IsCancellationRequested || item.Quantity <= 0)
continue;
var bb = item.Drawing.Program.BoundingBox();
TryPlaceOnExistingPlates(item, bb);
var templateClass = Classify(bb, _template.WorkArea());
if (item.Quantity > 0 && allowPlateCreation && templateClass != PartClass.Small)
{
PlaceOnNewPlates(item, bb);
if (item.Quantity > 0 && _plateOptions != null && _plateOptions.Count > 0)
TryUpgradeOrNewPlate(item, bb);
}
}
var leftovers = sorted.Where(i => i.Quantity > 0).ToList();
if (leftovers.Count > 0 && allowPlateCreation && !_token.IsCancellationRequested)
{
PackIntoExistingRemnants(leftovers);
CreateSharedPlates(leftovers);
}
if (_plateOptions != null && _plateOptions.Count > 0 && !_token.IsCancellationRequested)
TryConsolidateTailPlates();
foreach (var item in sorted.Where(i => i.Quantity > 0))
result.UnplacedItems.Add(item);
result.Plates.AddRange(_platePool.Where(p => p.Parts.Count > 0 || p.IsNew));
return result;
}
private void PackIntoExistingRemnants(List<NestItem> leftovers)
{
foreach (var pr in _platePool)
{
if (_token.IsCancellationRequested)
break;
var anyPlaced = true;
while (anyPlaced && !_token.IsCancellationRequested)
{
anyPlaced = false;
var remaining = leftovers.Where(i => i.Quantity > 0).ToList();
if (remaining.Count == 0)
break;
var remnants = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
if (remnants.Count == 0)
break;
var engine = NestEngineRegistry.Create(pr.Plate);
var cloned = remaining.Select(CloneItem).ToList();
var parts = engine.PackArea(remnants[0], cloned, _progress, _token);
if (parts.Count > 0)
{
pr.Plate.Parts.AddRange(parts);
pr.Parts.AddRange(parts);
anyPlaced = true;
foreach (var item in remaining)
{
var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
}
}
}
}
private void CreateSharedPlates(List<NestItem> leftovers)
{
leftovers.RemoveAll(i => i.Quantity <= 0);
while (leftovers.Count > 0 && !_token.IsCancellationRequested)
{
var plate = CreatePlate(_template, _plateOptions, null);
var placedAny = false;
foreach (var item in leftovers)
{
if (item.Quantity <= 0 || _token.IsCancellationRequested)
continue;
var remnants = !placedAny
? new List<Box> { plate.WorkArea() }
: RemnantFinder.FromPlate(plate).FindRemnants();
if (remnants.Count == 0)
break;
var engine = NestEngineRegistry.Create(plate);
var clonedItem = CloneItem(item);
var parts = engine.Fill(clonedItem, remnants[0], _progress, _token);
if (parts.Count > 0)
{
plate.Parts.AddRange(parts);
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
placedAny = true;
}
}
if (!placedAny)
break;
var pr = CreateNewPlateResult(plate);
pr.Parts.AddRange(plate.Parts);
_platePool.Add(pr);
leftovers.RemoveAll(i => i.Quantity <= 0);
}
}
private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds)
{
var anyPlaced = false;
while (item.Quantity > 0 && !_token.IsCancellationRequested)
{
PlateResult bestPlate = null;
Box bestZone = null;
var bestScore = double.MinValue;
foreach (var pr in _platePool)
{
if (_token.IsCancellationRequested)
break;
var workArea = pr.Plate.WorkArea();
var classification = Classify(partBounds, workArea);
var remnants = classification == PartClass.Small
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
foreach (var zone in remnants)
{
var score = ScoreZone(zone, partBounds);
if (score > bestScore)
{
bestPlate = pr;
bestZone = zone;
bestScore = score;
}
}
}
if (bestPlate == null || bestZone == null)
break;
if (FillAndPlace(bestPlate, bestZone, item) == 0)
break;
anyPlaced = true;
}
return anyPlaced;
}
private bool PlaceOnNewPlates(NestItem item, Box partBounds)
{
var anyPlaced = false;
while (item.Quantity > 0 && !_token.IsCancellationRequested)
{
var plate = CreatePlate(_template, _plateOptions, partBounds);
var workArea = plate.WorkArea();
if (partBounds.Length > workArea.Length && partBounds.Length > workArea.Width)
break;
if (partBounds.Width > workArea.Width && partBounds.Width > workArea.Length)
break;
var pr = CreateNewPlateResult(plate);
if (FillAndPlace(pr, workArea, item) == 0)
break;
_platePool.Add(pr);
anyPlaced = true;
}
return anyPlaced;
}
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
{
if (_plateOptions == null || _plateOptions.Count == 0)
return false;
var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList();
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
{
var currentOption = pr.ChosenSize;
var currentIdx = sortedOptions.FindIndex(o =>
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
if (currentIdx < 0 || currentIdx >= sortedOptions.Count - 1)
continue;
for (var i = currentIdx + 1; i < sortedOptions.Count; i++)
{
var upgradeOption = sortedOptions[i];
// Only consider options that are at least as large in both dimensions.
if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
continue;
var smallestNew = sortedOptions.FirstOrDefault(o =>
{
var ww = o.Width - _template.EdgeSpacing.Left - _template.EdgeSpacing.Right;
var wl = o.Length - _template.EdgeSpacing.Top - _template.EdgeSpacing.Bottom;
return (ww >= partBounds.Width && wl >= partBounds.Length)
|| (ww >= partBounds.Length && wl >= partBounds.Width);
});
if (smallestNew == null)
continue;
var utilEst = pr.Plate.Utilization();
var decision = EvaluateUpgradeVsNew(currentOption, upgradeOption, smallestNew,
_salvageRate, utilEst);
if (decision.ShouldUpgrade)
{
var oldSize = pr.Plate.Size;
var oldChosenSize = pr.ChosenSize;
pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
pr.ChosenSize = upgradeOption;
var remainingArea = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
if (remainingArea.Count > 0 && FillAndPlace(pr, remainingArea[0], item) > 0)
return true;
// Revert if nothing was placed.
pr.Plate.Size = oldSize;
pr.ChosenSize = oldChosenSize;
}
break;
}
}
return false;
}
private void TryConsolidateTailPlates()
{
var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList();
if (activePlates.Count < 2)
return;
var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList();
// Try to absorb the smallest-utilization new plate into another plate via upgrade.
var donor = activePlates.OrderBy(p => p.Plate.Utilization()).First();
var donorParts = donor.Parts.ToList();
foreach (var target in activePlates)
{
if (target == donor || target.ChosenSize == null)
continue;
var currentOption = target.ChosenSize;
// Try each larger option that doesn't shrink any dimension.
foreach (var upgradeOption in sortedOptions.Where(o =>
o.Width >= currentOption.Width - Tolerance.Epsilon
&& o.Length >= currentOption.Length - Tolerance.Epsilon
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
{
var oldSize = target.Plate.Size;
var oldChosenSize = target.ChosenSize;
target.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
target.ChosenSize = upgradeOption;
var remnants = RemnantFinder.FromPlate(target.Plate).FindRemnants();
if (remnants.Count == 0)
{
target.Plate.Size = oldSize;
target.ChosenSize = oldChosenSize;
continue;
}
// Try to pack all donor parts into the remnant space.
var engine = NestEngineRegistry.Create(target.Plate);
var tempItems = donorParts
.GroupBy(p => p.BaseDrawing.Name)
.Select(g => new NestItem
{
Drawing = g.First().BaseDrawing,
Quantity = g.Count(),
})
.ToList();
var placed = engine.PackArea(remnants[0], tempItems, _progress, _token);
if (placed.Count >= donorParts.Count)
{
// All donor parts fit — absorb them.
target.Plate.Parts.AddRange(placed);
target.Parts.AddRange(placed);
foreach (var p in donorParts)
donor.Plate.Parts.Remove(p);
donor.Parts.Clear();
_platePool.Remove(donor);
return;
}
// Didn't fit all parts — revert.
target.Plate.Size = oldSize;
target.ChosenSize = oldChosenSize;
}
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace OpenNest
{
public class MultiPlateResult
{
public List<PlateResult> Plates { get; set; } = new();
public List<NestItem> UnplacedItems { get; set; } = new();
}
public class PlateResult
{
public Plate Plate { get; set; }
public List<Part> Parts { get; set; } = new();
public PlateOption ChosenSize { get; set; }
public bool IsNew { get; set; }
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace OpenNest
{
public enum PartSortOrder
{
BoundingBoxArea,
Size,
}
}
@@ -0,0 +1,437 @@
using OpenNest.Geometry;
using OpenNest.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Xunit;
using Xunit.Abstractions;
namespace OpenNest.Tests.Engine;
public class MultiPlateNesterTests
{
private readonly ITestOutputHelper _output;
public MultiPlateNesterTests(ITestOutputHelper output)
{
_output = output;
}
private static Drawing MakeDrawing(string name, double width, double length)
{
var program = new OpenNest.CNC.Program();
program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(width, 0)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(width, length)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, length)));
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
var drawing = new Drawing(name, program);
drawing.UpdateArea();
return drawing;
}
private static NestItem MakeItem(string name, double width, double length, int qty = 1)
{
return new NestItem
{
Drawing = MakeDrawing(name, width, length),
Quantity = qty,
};
}
[Fact]
public void SortByBoundingBoxArea_OrdersLargestFirst()
{
var items = new List<NestItem>
{
MakeItem("small", 10, 10),
MakeItem("large", 40, 60),
MakeItem("medium", 20, 30),
};
var sorted = MultiPlateNester.SortItems(items, PartSortOrder.BoundingBoxArea);
Assert.Equal("large", sorted[0].Drawing.Name);
Assert.Equal("medium", sorted[1].Drawing.Name);
Assert.Equal("small", sorted[2].Drawing.Name);
}
[Fact]
public void SortBySize_OrdersByLongestDimension()
{
var items = new List<NestItem>
{
MakeItem("short-wide", 50, 20), // longest = 50
MakeItem("tall-narrow", 10, 80), // longest = 80
MakeItem("square", 30, 30), // longest = 30
};
var sorted = MultiPlateNester.SortItems(items, PartSortOrder.Size);
Assert.Equal("tall-narrow", sorted[0].Drawing.Name);
Assert.Equal("short-wide", sorted[1].Drawing.Name);
Assert.Equal("square", sorted[2].Drawing.Name);
}
// --- Task 4: Part Classification ---
[Fact]
public void Classify_LargePart_WhenWidthExceedsHalfWorkArea()
{
var workArea = new Box(0, 0, 96, 48);
var bb = new Box(0, 0, 50, 20); // width 50 > half of 96 = 48
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Large, result);
}
[Fact]
public void Classify_LargePart_WhenLengthExceedsHalfWorkArea()
{
var workArea = new Box(0, 0, 96, 48);
var bb = new Box(0, 0, 20, 30); // length 30 > half of 48 = 24
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Large, result);
}
[Fact]
public void Classify_MediumPart_NotLargeButAreaAboveThreshold()
{
var workArea = new Box(0, 0, 96, 48);
// workArea = 4608, 1/9 = 512. bb = 40*15 = 600 > 512
// 40 < 48 (half of 96), 15 < 24 (half of 48) — not Large
var bb = new Box(0, 0, 40, 15);
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Medium, result);
}
[Fact]
public void Classify_SmallPart()
{
var workArea = new Box(0, 0, 96, 48);
// workArea = 4608, 1/9 = 512. bb = 10*10 = 100 < 512
var bb = new Box(0, 0, 10, 10);
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Small, result);
}
// --- Task 5: Scrap Zone Identification ---
[Fact]
public void IsScrapRemnant_BothDimensionsBelowThreshold_ReturnsTrue()
{
var remnant = new Box(0, 0, 10, 8);
Assert.True(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void IsScrapRemnant_OneDimensionAboveThreshold_ReturnsFalse()
{
// 11 x 120 — narrow but long, should be preserved
var remnant = new Box(0, 0, 11, 120);
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void IsScrapRemnant_BothDimensionsAboveThreshold_ReturnsFalse()
{
var remnant = new Box(0, 0, 20, 30);
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void FindRemnants_ScrapOnly_ReturnsOnlyScrapRemnants()
{
// 96x48 plate with a 70x40 part placed at origin
var plate = new Plate(96, 48) { PartSpacing = 0.25 };
var drawing = MakeDrawing("big", 70, 40);
var part = new Part(drawing);
plate.Parts.Add(part);
var scrap = MultiPlateNester.FindRemnants(plate, 12.0, scrapOnly: true);
// All returned zones should have both dims < 12
foreach (var zone in scrap)
{
Assert.True(zone.Width < 12.0 && zone.Length < 12.0,
$"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12");
}
}
// --- Task 6: Plate Creation Helper ---
[Fact]
public void CreatePlate_UsesTemplateWhenNoOptions()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
var plate = MultiPlateNester.CreatePlate(template, null, null);
Assert.Equal(96, plate.Size.Width);
Assert.Equal(48, plate.Size.Length);
Assert.Equal(0.25, plate.PartSpacing);
Assert.Equal(1, plate.Quadrant);
}
[Fact]
public void CreatePlate_PicksSmallestFittingOption()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 };
var options = new List<PlateOption>
{
new() { Width = 48, Length = 96, Cost = 100 },
new() { Width = 60, Length = 120, Cost = 200 },
new() { Width = 72, Length = 144, Cost = 300 },
};
// Part needs 50x50 work area — 48x96 (after edge spacing: 46x94) — 46 < 50, doesn't fit.
// 60x120 (58x118) does fit.
var minBounds = new Box(0, 0, 50, 50);
var plate = MultiPlateNester.CreatePlate(template, options, minBounds);
Assert.Equal(60, plate.Size.Width);
Assert.Equal(120, plate.Size.Length);
}
[Fact]
public void EvaluateUpgrade_PrefersCheaperOption()
{
var currentOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
var upgradeOption = new PlateOption { Width = 60, Length = 120, Cost = 160 };
var newPlateOption = new PlateOption { Width = 48, Length = 96, Cost = 100 };
// Upgrade cost = 160 - 100 = 60
// New plate cost with 50% utilization, 50% salvage:
// remnantFraction = 0.5, salvageCredit = 0.5 * 100 * 0.5 = 25
// netNewCost = 100 - 25 = 75
// Upgrade (60) < new plate (75), so upgrade wins
var decision = MultiPlateNester.EvaluateUpgradeVsNew(
currentOption, upgradeOption, newPlateOption, 0.5, 0.5);
Assert.True(decision.ShouldUpgrade);
}
// --- Task 7: Main Orchestration ---
[Fact]
public void Nest_LargePartsGetOwnPlates()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big1", 80, 40, 1),
MakeItem("big2", 70, 35, 1),
};
var result = MultiPlateNester.Nest(
items, template,
plateOptions: null,
salvageRate: 0.5,
sortOrder: PartSortOrder.BoundingBoxArea,
minRemnantSize: 12.0,
allowPlateCreation: true,
existingPlates: null,
progress: null,
token: CancellationToken.None);
// Each large part should be on its own plate.
Assert.True(result.Plates.Count >= 2,
$"Expected at least 2 plates, got {result.Plates.Count}");
}
[Fact]
public void Nest_SmallPartsConsolidateOntoSharedPlates()
{
// Small parts should be packed together on shared plates rather than
// each drawing getting its own plate. The consolidation pass fills
// small parts into remaining space on existing plates.
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big", 80, 40, 1),
MakeItem("tinyA", 5, 5, 3),
MakeItem("tinyB", 4, 4, 3),
};
var result = MultiPlateNester.Nest(
items, template,
plateOptions: null,
salvageRate: 0.5,
sortOrder: PartSortOrder.BoundingBoxArea,
minRemnantSize: 12.0,
allowPlateCreation: true,
existingPlates: null,
progress: null,
token: CancellationToken.None);
// Both small drawing types should share space — not each on their own plate.
// With consolidation, they pack into remaining space alongside the big part.
Assert.True(result.Plates.Count <= 2,
$"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}");
Assert.Equal(0, result.UnplacedItems.Count);
}
[Fact]
public void Nest_RespectsAllowPlateCreation()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var items = new List<NestItem>
{
MakeItem("big1", 80, 40, 1),
MakeItem("big2", 70, 35, 1),
};
var result = MultiPlateNester.Nest(
items, template,
plateOptions: null,
salvageRate: 0.5,
sortOrder: PartSortOrder.BoundingBoxArea,
minRemnantSize: 12.0,
allowPlateCreation: false,
existingPlates: null,
progress: null,
token: CancellationToken.None);
// No existing plates and no plate creation — nothing can be placed.
Assert.Empty(result.Plates);
Assert.Equal(2, result.UnplacedItems.Count);
}
[Fact]
public void Nest_UsesExistingPlates()
{
var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
template.EdgeSpacing = new Spacing();
var existingPlate = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 };
existingPlate.EdgeSpacing = new Spacing();
// Use a part small enough to be classified as Medium on a 96x48 plate.
// Plate WorkArea: Width=96, Length=48. Half: 48, 24.
// Part 24x22: Length=24 (not > 24), Width=22 (not > 48) — not Large.
// Area = 528 > 4608/9 = 512 — Medium.
var items = new List<NestItem>
{
MakeItem("medium", 24, 22, 1),
};
var result = MultiPlateNester.Nest(
items, template,
plateOptions: null,
salvageRate: 0.5,
sortOrder: PartSortOrder.BoundingBoxArea,
minRemnantSize: 12.0,
allowPlateCreation: true,
existingPlates: new List<Plate> { existingPlate },
progress: null,
token: CancellationToken.None);
// Part should be placed on the existing plate, not a new one.
Assert.Single(result.Plates);
Assert.False(result.Plates[0].IsNew);
}
[Fact]
public void Nest_RealNestFile_PartFirst()
{
var nestPath = @"C:\Users\aisaacs\Desktop\4526 A14 - 0.188 AISI 304.nest";
if (!File.Exists(nestPath))
{
_output.WriteLine("SKIP: nest file not found");
return;
}
var nest = new NestReader(nestPath).Read();
var template = nest.PlateDefaults.CreateNew();
_output.WriteLine($"Plate: {template.Size.Width}x{template.Size.Length}, " +
$"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})");
var wa = template.WorkArea();
_output.WriteLine($"Work area: {wa.Width:F1}x{wa.Length:F1}");
_output.WriteLine($"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, " +
$"Medium if area > {wa.Width * wa.Length / 9:F0}");
_output.WriteLine("---");
var items = new List<NestItem>();
foreach (var d in nest.Drawings)
{
var qty = d.Quantity.Required > 0 ? d.Quantity.Required : d.Quantity.Remaining;
if (qty <= 0) qty = 1;
var bb = d.Program.BoundingBox();
var classification = MultiPlateNester.Classify(bb, wa);
_output.WriteLine($" {d.Name,-25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}");
items.Add(new NestItem
{
Drawing = d,
Quantity = qty,
StepAngle = d.Constraints.StepAngle,
RotationStart = d.Constraints.StartAngle,
RotationEnd = d.Constraints.EndAngle,
});
}
_output.WriteLine("---");
_output.WriteLine($"Total: {items.Count} drawings, {items.Sum(i => i.Quantity)} parts");
var plateOptions = new List<PlateOption>
{
new() { Width = 48, Length = 96, Cost = 0 },
new() { Width = 48, Length = 120, Cost = 0 },
new() { Width = 48, Length = 144, Cost = 0 },
new() { Width = 60, Length = 96, Cost = 0 },
new() { Width = 60, Length = 120, Cost = 0 },
new() { Width = 60, Length = 144, Cost = 0 },
new() { Width = 72, Length = 96, Cost = 0 },
new() { Width = 72, Length = 120, Cost = 0 },
new() { Width = 72, Length = 144, Cost = 0 },
};
_output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
_output.WriteLine("");
var result = MultiPlateNester.Nest(
items, template,
plateOptions: plateOptions,
salvageRate: 0.5,
sortOrder: PartSortOrder.BoundingBoxArea,
minRemnantSize: 12.0,
allowPlateCreation: true,
existingPlates: null,
progress: null,
token: CancellationToken.None);
_output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ===");
for (var i = 0; i < result.Plates.Count; i++)
{
var pr = result.Plates[i];
var groups = pr.Parts.GroupBy(p => p.BaseDrawing.Name)
.Select(g => $"{g.Key} x{g.Count()}")
.ToList();
_output.WriteLine($" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): " +
$"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]");
}
if (result.UnplacedItems.Count > 0)
{
_output.WriteLine($" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}");
}
_output.WriteLine($"\nTotal parts placed: {result.Plates.Sum(p => p.Parts.Count)}");
_output.WriteLine($"Total plates used: {result.Plates.Count}");
}
}
+149 -61
View File
@@ -17,13 +17,20 @@ namespace OpenNest.Forms
private void InitializeComponent()
{
this.engineLabel = new System.Windows.Forms.Label();
this.engineComboBox = new System.Windows.Forms.ComboBox();
this.partsGroup = new System.Windows.Forms.GroupBox();
this.tabControl = new System.Windows.Forms.TabControl();
this.partsTab = new System.Windows.Forms.TabPage();
this.platesTab = new System.Windows.Forms.TabPage();
this.partsGrid = new System.Windows.Forms.DataGridView();
this.summaryLabel = new System.Windows.Forms.Label();
this.optionsGroup = new System.Windows.Forms.GroupBox();
this.engineLabel = new System.Windows.Forms.Label();
this.engineComboBox = new System.Windows.Forms.ComboBox();
this.createNewPlatesAsNeededBox = new System.Windows.Forms.CheckBox();
this.partFirstGroup = new System.Windows.Forms.GroupBox();
this.partFirstCheckBox = new System.Windows.Forms.CheckBox();
this.sortOrderLabel = new System.Windows.Forms.Label();
this.sortOrderComboBox = new System.Windows.Forms.ComboBox();
this.minRemnantLabel = new System.Windows.Forms.Label();
this.minRemnantBox = new System.Windows.Forms.TextBox();
this.plateOptimizerGroup = new System.Windows.Forms.GroupBox();
this.optimizePlateSizeBox = new System.Windows.Forms.CheckBox();
this.plateGrid = new System.Windows.Forms.DataGridView();
@@ -33,42 +40,53 @@ namespace OpenNest.Forms
this.buttonPanel = new System.Windows.Forms.Panel();
this.acceptButton = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button();
this.tabControl.SuspendLayout();
this.partsTab.SuspendLayout();
this.platesTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.partsGrid)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.plateGrid)).BeginInit();
this.partsGroup.SuspendLayout();
this.optionsGroup.SuspendLayout();
this.partFirstGroup.SuspendLayout();
this.plateOptimizerGroup.SuspendLayout();
this.buttonPanel.SuspendLayout();
this.SuspendLayout();
//
// engineLabel
// tabControl
//
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:";
this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.tabControl.Controls.Add(this.partsTab);
this.tabControl.Controls.Add(this.platesTab);
this.tabControl.Location = new System.Drawing.Point(12, 12);
this.tabControl.Name = "tabControl";
this.tabControl.SelectedIndex = 0;
this.tabControl.Size = new System.Drawing.Size(556, 490);
this.tabControl.TabIndex = 0;
//
// engineComboBox
// partsTab
//
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;
this.partsTab.Controls.Add(this.partsGrid);
this.partsTab.Controls.Add(this.summaryLabel);
this.partsTab.Location = new System.Drawing.Point(4, 25);
this.partsTab.Name = "partsTab";
this.partsTab.Padding = new System.Windows.Forms.Padding(6);
this.partsTab.Size = new System.Drawing.Size(548, 461);
this.partsTab.TabIndex = 0;
this.partsTab.Text = "Parts";
this.partsTab.UseVisualStyleBackColor = true;
//
// partsGroup
// platesTab
//
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";
this.platesTab.Controls.Add(this.engineLabel);
this.platesTab.Controls.Add(this.engineComboBox);
this.platesTab.Controls.Add(this.createNewPlatesAsNeededBox);
this.platesTab.Controls.Add(this.partFirstGroup);
this.platesTab.Controls.Add(this.plateOptimizerGroup);
this.platesTab.Location = new System.Drawing.Point(4, 25);
this.platesTab.Name = "platesTab";
this.platesTab.Padding = new System.Windows.Forms.Padding(6);
this.platesTab.Size = new System.Drawing.Size(548, 461);
this.platesTab.TabIndex = 1;
this.platesTab.Text = "Plates";
this.platesTab.UseVisualStyleBackColor = true;
//
// partsGrid
//
@@ -78,43 +96,108 @@ namespace OpenNest.Forms
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.Location = new System.Drawing.Point(10, 10);
this.partsGrid.Name = "partsGrid";
this.partsGrid.RowHeadersVisible = false;
this.partsGrid.AutoGenerateColumns = false;
this.partsGrid.Size = new System.Drawing.Size(536, 160);
this.partsGrid.Size = new System.Drawing.Size(528, 420);
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.Location = new System.Drawing.Point(10, 436);
this.summaryLabel.Name = "summaryLabel";
this.summaryLabel.Size = new System.Drawing.Size(0, 16);
this.summaryLabel.TabIndex = 1;
//
// optionsGroup
// engineLabel
//
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";
this.engineLabel.AutoSize = true;
this.engineLabel.Location = new System.Drawing.Point(10, 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(98, 12);
this.engineComboBox.Name = "engineComboBox";
this.engineComboBox.Size = new System.Drawing.Size(200, 24);
this.engineComboBox.TabIndex = 1;
//
// createNewPlatesAsNeededBox
//
this.createNewPlatesAsNeededBox.AutoSize = true;
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 22);
this.createNewPlatesAsNeededBox.Location = new System.Drawing.Point(10, 44);
this.createNewPlatesAsNeededBox.Name = "createNewPlatesAsNeededBox";
this.createNewPlatesAsNeededBox.Size = new System.Drawing.Size(202, 20);
this.createNewPlatesAsNeededBox.TabIndex = 0;
this.createNewPlatesAsNeededBox.TabIndex = 2;
this.createNewPlatesAsNeededBox.Text = "Create new plates as needed";
this.createNewPlatesAsNeededBox.UseVisualStyleBackColor = true;
//
// partFirstGroup
//
this.partFirstGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
this.partFirstGroup.Controls.Add(this.partFirstCheckBox);
this.partFirstGroup.Controls.Add(this.sortOrderLabel);
this.partFirstGroup.Controls.Add(this.sortOrderComboBox);
this.partFirstGroup.Controls.Add(this.minRemnantLabel);
this.partFirstGroup.Controls.Add(this.minRemnantBox);
this.partFirstGroup.Location = new System.Drawing.Point(10, 72);
this.partFirstGroup.Name = "partFirstGroup";
this.partFirstGroup.Size = new System.Drawing.Size(528, 80);
this.partFirstGroup.TabIndex = 3;
this.partFirstGroup.TabStop = false;
this.partFirstGroup.Text = " Part-First Mode";
//
// partFirstCheckBox
//
this.partFirstCheckBox.AutoSize = true;
this.partFirstCheckBox.Location = new System.Drawing.Point(10, 0);
this.partFirstCheckBox.Name = "partFirstCheckBox";
this.partFirstCheckBox.Size = new System.Drawing.Size(15, 14);
this.partFirstCheckBox.TabIndex = 0;
this.partFirstCheckBox.UseVisualStyleBackColor = true;
this.partFirstCheckBox.CheckedChanged += new System.EventHandler(this.partFirstCheckBox_CheckedChanged);
//
// sortOrderLabel
//
this.sortOrderLabel.AutoSize = true;
this.sortOrderLabel.Location = new System.Drawing.Point(10, 26);
this.sortOrderLabel.Name = "sortOrderLabel";
this.sortOrderLabel.Size = new System.Drawing.Size(75, 16);
this.sortOrderLabel.TabIndex = 1;
this.sortOrderLabel.Text = "Sort Order:";
//
// sortOrderComboBox
//
this.sortOrderComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.sortOrderComboBox.Location = new System.Drawing.Point(100, 23);
this.sortOrderComboBox.Name = "sortOrderComboBox";
this.sortOrderComboBox.Size = new System.Drawing.Size(180, 24);
this.sortOrderComboBox.TabIndex = 2;
//
// minRemnantLabel
//
this.minRemnantLabel.AutoSize = true;
this.minRemnantLabel.Location = new System.Drawing.Point(10, 54);
this.minRemnantLabel.Name = "minRemnantLabel";
this.minRemnantLabel.Size = new System.Drawing.Size(117, 16);
this.minRemnantLabel.TabIndex = 3;
this.minRemnantLabel.Text = "Min Remnant Size:";
//
// minRemnantBox
//
this.minRemnantBox.Location = new System.Drawing.Point(133, 51);
this.minRemnantBox.Name = "minRemnantBox";
this.minRemnantBox.Size = new System.Drawing.Size(60, 22);
this.minRemnantBox.TabIndex = 4;
this.minRemnantBox.Text = "12";
//
// plateOptimizerGroup
//
this.plateOptimizerGroup.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
@@ -123,9 +206,9 @@ namespace OpenNest.Forms
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.Location = new System.Drawing.Point(10, 158);
this.plateOptimizerGroup.Name = "plateOptimizerGroup";
this.plateOptimizerGroup.Size = new System.Drawing.Size(556, 188);
this.plateOptimizerGroup.Size = new System.Drawing.Size(528, 188);
this.plateOptimizerGroup.TabIndex = 4;
this.plateOptimizerGroup.TabStop = false;
this.plateOptimizerGroup.Text = " Plate Optimizer";
@@ -150,7 +233,7 @@ namespace OpenNest.Forms
this.plateGrid.Name = "plateGrid";
this.plateGrid.RowHeadersVisible = false;
this.plateGrid.AutoGenerateColumns = false;
this.plateGrid.Size = new System.Drawing.Size(536, 130);
this.plateGrid.Size = new System.Drawing.Size(508, 130);
this.plateGrid.TabIndex = 1;
//
// salvageRateLabel
@@ -187,7 +270,7 @@ namespace OpenNest.Forms
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;
this.buttonPanel.TabIndex = 1;
//
// acceptButton
//
@@ -217,11 +300,7 @@ namespace OpenNest.Forms
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
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.tabControl);
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;
@@ -232,28 +311,37 @@ namespace OpenNest.Forms
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "AutoNest";
this.tabControl.ResumeLayout(false);
this.partsTab.ResumeLayout(false);
this.partsTab.PerformLayout();
this.platesTab.ResumeLayout(false);
this.platesTab.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.partFirstGroup.ResumeLayout(false);
this.partFirstGroup.PerformLayout();
this.plateOptimizerGroup.ResumeLayout(false);
this.plateOptimizerGroup.PerformLayout();
this.buttonPanel.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label engineLabel;
private System.Windows.Forms.ComboBox engineComboBox;
private System.Windows.Forms.GroupBox partsGroup;
private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.TabPage partsTab;
private System.Windows.Forms.TabPage platesTab;
private System.Windows.Forms.DataGridView partsGrid;
private System.Windows.Forms.Label summaryLabel;
private System.Windows.Forms.GroupBox optionsGroup;
private System.Windows.Forms.Label engineLabel;
private System.Windows.Forms.ComboBox engineComboBox;
private System.Windows.Forms.CheckBox createNewPlatesAsNeededBox;
private System.Windows.Forms.GroupBox partFirstGroup;
private System.Windows.Forms.CheckBox partFirstCheckBox;
private System.Windows.Forms.Label sortOrderLabel;
private System.Windows.Forms.ComboBox sortOrderComboBox;
private System.Windows.Forms.Label minRemnantLabel;
private System.Windows.Forms.TextBox minRemnantBox;
private System.Windows.Forms.GroupBox plateOptimizerGroup;
private System.Windows.Forms.CheckBox optimizePlateSizeBox;
private System.Windows.Forms.DataGridView plateGrid;
+44
View File
@@ -22,6 +22,11 @@ namespace OpenNest.Forms
LoadDefaultPlateOptions();
SetPlateOptimizerVisible(false);
sortOrderComboBox.Items.Add("Bounding Box Area");
sortOrderComboBox.Items.Add("Size");
sortOrderComboBox.SelectedIndex = 0;
SetPartFirstVisible(false);
partsGrid.DataError += PartsGrid_DataError;
}
@@ -54,6 +59,32 @@ namespace OpenNest.Forms
set { salvageRateBox.Text = (value * 100).ToString("F0"); }
}
public bool PartFirstMode
{
get { return partFirstCheckBox.Checked; }
set { partFirstCheckBox.Checked = value; }
}
public PartSortOrder SortOrder
{
get
{
if (sortOrderComboBox.SelectedItem is string s && s == "Size")
return PartSortOrder.Size;
return PartSortOrder.BoundingBoxArea;
}
}
public double MinRemnantSize
{
get
{
if (double.TryParse(minRemnantBox.Text, out var val) && val > 0)
return val;
return 12.0;
}
}
private void LoadEngines()
{
foreach (var engine in NestEngineRegistry.AvailableEngines)
@@ -242,6 +273,19 @@ namespace OpenNest.Forms
salvageRatePercentLabel.Visible = visible;
}
private void partFirstCheckBox_CheckedChanged(object sender, EventArgs e)
{
SetPartFirstVisible(partFirstCheckBox.Checked);
}
private void SetPartFirstVisible(bool visible)
{
sortOrderLabel.Visible = visible;
sortOrderComboBox.Visible = visible;
minRemnantLabel.Visible = visible;
minRemnantBox.Visible = visible;
}
private void UpdateSummary()
{
var gridItems = partsGrid.DataSource as List<DataGridViewItem>;
+9
View File
@@ -1,7 +1,9 @@
using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
using OpenNest.IO.Bending;
using OpenNest.IO.Bom;
using System;
using System.Collections.Generic;
@@ -470,12 +472,19 @@ namespace OpenNest.Forms
{
var result = Dxf.Import(part.DxfPath);
var bends = new List<Bend>();
if (result.Document != null)
bends = BendDetectorRegistry.AutoDetect(result.Document);
Bend.UpdateEtchEntities(result.Entities, bends);
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);
if (bends.Count > 0)
drawing.Bends.AddRange(bends);
var normalized = ShapeProfile.NormalizeEntities(result.Entities);
var pgm = ConvertGeometry.ToProgram(normalized);
+41 -2
View File
@@ -932,6 +932,10 @@ namespace OpenNest.Forms
var optimizePlateSize = form.OptimizePlateSize;
var plateOptions = optimizePlateSize ? form.GetPlateOptions() : null;
var salvageRate = form.SalvageRate;
var partFirstMode = form.PartFirstMode;
var sortOrder = form.SortOrder;
var minRemnantSize = form.MinRemnantSize;
var allowPlateCreation = form.AllowPlateCreation;
if (optimizePlateSize)
{
@@ -960,7 +964,7 @@ namespace OpenNest.Forms
try
{
await RunAutoNestAsync(items, progressForm, progress, nestingCts.Token,
plateOptions, salvageRate);
plateOptions, salvageRate, partFirstMode, sortOrder, minRemnantSize, allowPlateCreation);
}
catch (Exception ex)
{
@@ -984,8 +988,43 @@ namespace OpenNest.Forms
IProgress<NestProgress> progress,
CancellationToken token,
List<PlateOption> plateOptions = null,
double salvageRate = 0.5)
double salvageRate = 0.5,
bool partFirstMode = false,
PartSortOrder sortOrder = PartSortOrder.BoundingBoxArea,
double minRemnantSize = 12.0,
bool allowPlateCreation = true)
{
if (partFirstMode)
{
var existingPlates = new List<Plate>();
for (var i = 0; i < activeForm.Nest.Plates.Count; i++)
{
var p = activeForm.Nest.Plates[i];
if (p.Parts.Count > 0)
existingPlates.Add(p);
}
var template = activeForm.PlateView.Plate;
var result = await Task.Run(() =>
MultiPlateNester.Nest(items, template, plateOptions, salvageRate,
sortOrder, minRemnantSize, allowPlateCreation, existingPlates, progress, token));
foreach (var pr in result.Plates)
{
if (pr.IsNew)
{
var plate = GetOrCreatePlate(progressForm);
plate.Size = pr.Plate.Size;
plate.Parts.AddRange(pr.Parts);
}
}
activeForm.Nest.UpdateDrawingQuantities();
progressForm.ShowCompleted();
return;
}
const int maxPlates = 100;
for (var plateIndex = 0; plateIndex < maxPlates; plateIndex++)