merge: integrate NFP-based autonesting from feature/nfp-autonest

Brings in the full NFP implementation: ConvexDecomposition, NoFitPolygon,
InnerFitPolygon, NfpCache, BottomLeftFill, SimulatedAnnealing optimizer,
and INestOptimizer interface. Resolves conflicts by keeping master's
progress reporting infrastructure alongside the new AutoNest methods,
and adapting RunAutoNest_Click to use NFP AutoNest with async/cancellation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 09:40:38 -04:00
14 changed files with 1610 additions and 56 deletions

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using ModelContextProtocol.Server;
using OpenNest.Geometry;
@@ -190,5 +191,62 @@ namespace OpenNest.Mcp.Tools
return sb.ToString();
}
[McpServerTool(Name = "autonest_plate")]
[Description("NFP-based mixed-part autonesting. Places multiple different drawings on a plate with geometry-aware collision avoidance and simulated annealing optimization. Produces tighter layouts than pack_plate by allowing parts to interlock.")]
public string AutoNestPlate(
[Description("Index of the plate")] int plateIndex,
[Description("Comma-separated drawing names")] string drawingNames,
[Description("Comma-separated quantities for each drawing")] string quantities)
{
var plate = _session.GetPlate(plateIndex);
if (plate == null)
return $"Error: plate {plateIndex} not found";
if (string.IsNullOrWhiteSpace(drawingNames))
return "Error: drawingNames is required";
if (string.IsNullOrWhiteSpace(quantities))
return "Error: quantities is required";
var names = drawingNames.Split(',').Select(n => n.Trim()).ToArray();
var qtyStrings = quantities.Split(',').Select(q => q.Trim()).ToArray();
var qtys = new int[qtyStrings.Length];
for (var i = 0; i < qtyStrings.Length; i++)
{
if (!int.TryParse(qtyStrings[i], out qtys[i]))
return $"Error: '{qtyStrings[i]}' is not a valid quantity";
}
if (names.Length != qtys.Length)
return $"Error: drawing names count ({names.Length}) does not match quantities count ({qtys.Length})";
var items = new List<NestItem>();
for (var i = 0; i < names.Length; i++)
{
var drawing = _session.GetDrawing(names[i]);
if (drawing == null)
return $"Error: drawing '{names[i]}' not found";
items.Add(new NestItem { Drawing = drawing, Quantity = qtys[i] });
}
var parts = NestEngine.AutoNest(items, plate);
plate.Parts.AddRange(parts);
var sb = new StringBuilder();
sb.AppendLine($"AutoNest plate {plateIndex}: {(parts.Count > 0 ? "success" : "no parts placed")}");
sb.AppendLine($" Parts placed: {parts.Count}");
sb.AppendLine($" Total parts: {plate.Parts.Count}");
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
var groups = parts.GroupBy(p => p.BaseDrawing.Name);
foreach (var group in groups)
sb.AppendLine($" {group.Key}: {group.Count()}");
return sb.ToString();
}
}
}