feat: add plate creation and upgrade-vs-new evaluation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Math;
|
||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
@@ -92,5 +93,76 @@ namespace OpenNest
|
|||||||
|
|
||||||
return viable;
|
return viable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Find smallest option that fits the part (by cost ascending).
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No option fits — use template size.
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,4 +146,61 @@ public class MultiPlateNesterTests
|
|||||||
$"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12");
|
$"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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user