Files
OpenNest/OpenNest.Mcp/Tools/NestingTools.cs
AJ Isaacs ae010212ac refactor(engine): extract AutoNester and reorganize NestEngine
Move NFP-based AutoNest logic (polygon extraction, rotation computation,
simulated annealing) into dedicated AutoNester class. Consolidate duplicate
FillWithPairs overloads, extract BuildCandidateAngles and BuildProgressSummary,
reorganize NestEngine into logical sections. Update callers in Console,
MCP tools, and MainForm to use AutoNester.Nest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 22:51:57 -04:00

253 lines
11 KiB
C#

using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using ModelContextProtocol.Server;
using OpenNest.Geometry;
namespace OpenNest.Mcp.Tools
{
[McpServerToolType]
public class NestingTools
{
private readonly NestSession _session;
public NestingTools(NestSession session)
{
_session = session;
}
[McpServerTool(Name = "fill_plate")]
[Description("Fill an entire plate with a single drawing. Returns parts added and utilization.")]
public string FillPlate(
[Description("Index of the plate to fill")] int plateIndex,
[Description("Name of the drawing to fill with")] string drawingName,
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0)
{
var plate = _session.GetPlate(plateIndex);
if (plate == null)
return $"Error: plate {plateIndex} not found";
var drawing = _session.GetDrawing(drawingName);
if (drawing == null)
return $"Error: drawing '{drawingName}' not found";
var countBefore = plate.Parts.Count;
var engine = new NestEngine(plate);
var item = new NestItem { Drawing = drawing, Quantity = quantity };
var success = engine.Fill(item);
var countAfter = plate.Parts.Count;
var added = countAfter - countBefore;
var sb = new StringBuilder();
sb.AppendLine($"Fill plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}");
sb.AppendLine($" Parts added: {added}");
sb.AppendLine($" Total parts: {countAfter}");
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
return sb.ToString();
}
[McpServerTool(Name = "fill_area")]
[Description("Fill a specific rectangular area on a plate with a single drawing.")]
public string FillArea(
[Description("Index of the plate")] int plateIndex,
[Description("Name of the drawing to fill with")] string drawingName,
[Description("X origin of the area")] double x,
[Description("Y origin of the area")] double y,
[Description("Width of the area")] double width,
[Description("Height of the area")] double height,
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0)
{
var plate = _session.GetPlate(plateIndex);
if (plate == null)
return $"Error: plate {plateIndex} not found";
var drawing = _session.GetDrawing(drawingName);
if (drawing == null)
return $"Error: drawing '{drawingName}' not found";
var countBefore = plate.Parts.Count;
var engine = new NestEngine(plate);
var item = new NestItem { Drawing = drawing, Quantity = quantity };
var area = new Box(x, y, width, height);
var success = engine.Fill(item, area);
var countAfter = plate.Parts.Count;
var added = countAfter - countBefore;
var sb = new StringBuilder();
sb.AppendLine($"Fill area ({x:F1},{y:F1} {width:F1}x{height:F1}) on plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}");
sb.AppendLine($" Parts added: {added}");
sb.AppendLine($" Total parts: {countAfter}");
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
return sb.ToString();
}
[McpServerTool(Name = "fill_remnants")]
[Description("Find empty remnant regions on a plate and fill each with a drawing.")]
public string FillRemnants(
[Description("Index of the plate")] int plateIndex,
[Description("Name of the drawing to fill with")] string drawingName,
[Description("Maximum quantity per remnant (0 = unlimited)")] int quantity = 0)
{
var plate = _session.GetPlate(plateIndex);
if (plate == null)
return $"Error: plate {plateIndex} not found";
var drawing = _session.GetDrawing(drawingName);
if (drawing == null)
return $"Error: drawing '{drawingName}' not found";
var remnants = plate.GetRemnants();
if (remnants.Count == 0)
return $"No remnant areas found on plate {plateIndex}";
var sb = new StringBuilder();
sb.AppendLine($"Found {remnants.Count} remnant area(s) on plate {plateIndex}");
var totalAdded = 0;
var engine = new NestEngine(plate);
for (var i = 0; i < remnants.Count; i++)
{
var remnant = remnants[i];
var countBefore = plate.Parts.Count;
var item = new NestItem { Drawing = drawing, Quantity = quantity };
var success = engine.Fill(item, remnant);
var added = plate.Parts.Count - countBefore;
totalAdded += added;
sb.AppendLine($" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Length:F1}) -> {added} parts {(success ? "" : "(no fit)")}");
}
sb.AppendLine($"Total parts added: {totalAdded}");
sb.AppendLine($"Utilization: {plate.Utilization():P1}");
return sb.ToString();
}
[McpServerTool(Name = "pack_plate")]
[Description("Pack multiple drawings onto a plate using bin-packing. Specify drawings and quantities as comma-separated lists.")]
public string PackPlate(
[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 countBefore = plate.Parts.Count;
var engine = new NestEngine(plate);
var success = engine.Pack(items);
var countAfter = plate.Parts.Count;
var added = countAfter - countBefore;
var sb = new StringBuilder();
sb.AppendLine($"Pack plate {plateIndex}: {(success ? "success" : "failed")}");
sb.AppendLine($" Parts added: {added}");
sb.AppendLine($" Total parts: {countAfter}");
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
// Breakdown by drawing
var groups = plate.Parts.GroupBy(p => p.BaseDrawing.Name);
foreach (var group in groups)
sb.AppendLine($" {group.Key}: {group.Count()}");
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 = AutoNester.Nest(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();
}
}
}