From 8806bccb4e79d421ca14f5e916de8c9c8cd1d2bb Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 8 Mar 2026 15:48:30 -0400 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20add=20nesting=20tools=20=E2=80=94?= =?UTF-8?q?=20fill=5Fplate,=20fill=5Farea,=20fill=5Fremnants,=20pack=5Fpla?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- OpenNest.Mcp/Tools/NestingTools.cs | 180 +++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 OpenNest.Mcp/Tools/NestingTools.cs diff --git a/OpenNest.Mcp/Tools/NestingTools.cs b/OpenNest.Mcp/Tools/NestingTools.cs new file mode 100644 index 0000000..48c8517 --- /dev/null +++ b/OpenNest.Mcp/Tools/NestingTools.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using ModelContextProtocol.Server; +using OpenNest.Geometry; + +namespace OpenNest.Mcp.Tools +{ + 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.Height: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"; + + var names = drawingNames.Split(',').Select(n => n.Trim()).ToArray(); + var qtys = quantities.Split(',').Select(q => int.Parse(q.Trim())).ToArray(); + + if (names.Length != qtys.Length) + return $"Error: drawing names count ({names.Length}) does not match quantities count ({qtys.Length})"; + + var items = new List(); + + 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(); + } + } +}