Merge branch 'feature/mcp-service'
Add OpenNest.IO class library and OpenNest.Mcp server for Claude Code integration. Extract IO classes from WinForms project, add Plate.GetRemnants(), and expose 12 MCP tools for loading nests, running nesting algorithms, and inspecting results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
8
.mcp.json
Normal file
8
.mcp.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"opennest": {
|
||||
"command": "C:/Users/AJ/.claude/mcp/OpenNest.Mcp/OpenNest.Mcp.exe",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
24
CLAUDE.md
24
CLAUDE.md
@@ -14,13 +14,13 @@ This is a .NET 8 solution using SDK-style `.csproj` files targeting `net8.0-wind
|
||||
dotnet build OpenNest.sln
|
||||
```
|
||||
|
||||
NuGet dependency: `ACadSharp` 3.1.32 (DXF/DWG import/export), `System.Drawing.Common` 8.0.10.
|
||||
NuGet dependencies: `ACadSharp` 3.1.32 (DXF/DWG import/export, in OpenNest.IO), `System.Drawing.Common` 8.0.10, `ModelContextProtocol` + `Microsoft.Extensions.Hosting` (in OpenNest.Mcp).
|
||||
|
||||
No test projects exist in this solution.
|
||||
|
||||
## Architecture
|
||||
|
||||
Three projects form a layered architecture:
|
||||
Five projects form a layered architecture:
|
||||
|
||||
### OpenNest.Core (class library)
|
||||
Domain model, geometry, and CNC primitives organized into namespaces:
|
||||
@@ -41,13 +41,29 @@ Nesting algorithms. `NestEngine` orchestrates filling plates with parts.
|
||||
- `NestItem`: Input to the engine — wraps a `Drawing` with quantity, priority, and rotation constraints.
|
||||
- `BestCombination`: Finds optimal mix of normal/rotated columns for grid fills.
|
||||
|
||||
### OpenNest (WinForms WinExe, depends on Core + Engine)
|
||||
### OpenNest.IO (class library, depends on Core)
|
||||
File I/O and format conversion. Uses ACadSharp for DXF/DWG support.
|
||||
|
||||
- `DxfImporter`/`DxfExporter` — DXF file import/export via ACadSharp.
|
||||
- `NestReader`/`NestWriter` — custom ZIP-based nest format (XML metadata + G-code programs).
|
||||
- `ProgramReader` — G-code text parser.
|
||||
- `Extensions` — conversion helpers between ACadSharp and OpenNest geometry types.
|
||||
|
||||
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
||||
MCP server for Claude Code integration. Exposes nesting operations as MCP tools over stdio transport. Published to `~/.claude/mcp/OpenNest.Mcp/`.
|
||||
|
||||
- **Tools/InputTools**: `load_nest`, `import_dxf`, `create_drawing` (built-in shapes or G-code).
|
||||
- **Tools/SetupTools**: `create_plate`, `clear_plate`.
|
||||
- **Tools/NestingTools**: `fill_plate`, `fill_area`, `fill_remnants`, `pack_plate`.
|
||||
- **Tools/InspectionTools**: `get_plate_info`, `get_parts`, `check_overlaps`.
|
||||
- `NestSession` — in-memory state across tool calls (current Nest, standalone plates/drawings).
|
||||
|
||||
### OpenNest (WinForms WinExe, depends on Core + Engine + IO)
|
||||
The UI application with MDI interface.
|
||||
|
||||
- **Forms/**: `MainForm` (MDI parent), `EditNestForm` (MDI child per nest), plus dialogs for plate editing, auto-nesting, DXF conversion, cut parameters, etc.
|
||||
- **Controls/**: `PlateView` (2D plate renderer with zoom/pan), `DrawingListBox`, `DrawControl`, `QuadrantSelect`.
|
||||
- **Actions/**: User interaction modes — `ActionSelect`, `ActionAddPart`, `ActionClone`, `ActionFillArea`, `ActionZoomWindow`, `ActionSetSequence`.
|
||||
- **IO/**: `DxfImporter`/`DxfExporter` (via ACadSharp library), `NestReader`/`NestWriter` (custom ZIP-based format with XML metadata + G-code programs), `ProgramReader`.
|
||||
- **Post-processing**: `IPostProcessor` plugin interface loaded from DLLs in a `Posts/` directory at runtime.
|
||||
|
||||
## File Format
|
||||
|
||||
@@ -473,5 +473,87 @@ namespace OpenNest
|
||||
|
||||
return pts.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds rectangular remnant (empty) regions on the plate.
|
||||
/// Returns strips along edges that are clear of parts.
|
||||
/// </summary>
|
||||
public List<Box> GetRemnants()
|
||||
{
|
||||
var work = WorkArea();
|
||||
var results = new List<Box>();
|
||||
|
||||
if (Parts.Count == 0)
|
||||
{
|
||||
results.Add(work);
|
||||
return results;
|
||||
}
|
||||
|
||||
var obstacles = new List<Box>();
|
||||
foreach (var part in Parts)
|
||||
obstacles.Add(part.BoundingBox.Offset(PartSpacing));
|
||||
|
||||
// Right strip: from the rightmost part edge to the work area right edge
|
||||
var maxRight = double.MinValue;
|
||||
foreach (var box in obstacles)
|
||||
{
|
||||
if (box.Right > maxRight)
|
||||
maxRight = box.Right;
|
||||
}
|
||||
|
||||
if (maxRight < work.Right)
|
||||
{
|
||||
var strip = new Box(maxRight, work.Bottom, work.Right - maxRight, work.Height);
|
||||
if (strip.Area() > 1.0)
|
||||
results.Add(strip);
|
||||
}
|
||||
|
||||
// Top strip: from the topmost part edge to the work area top edge
|
||||
var maxTop = double.MinValue;
|
||||
foreach (var box in obstacles)
|
||||
{
|
||||
if (box.Top > maxTop)
|
||||
maxTop = box.Top;
|
||||
}
|
||||
|
||||
if (maxTop < work.Top)
|
||||
{
|
||||
var strip = new Box(work.Left, maxTop, work.Width, work.Top - maxTop);
|
||||
if (strip.Area() > 1.0)
|
||||
results.Add(strip);
|
||||
}
|
||||
|
||||
// Bottom strip: from work area bottom to the lowest part edge
|
||||
var minBottom = double.MaxValue;
|
||||
foreach (var box in obstacles)
|
||||
{
|
||||
if (box.Bottom < minBottom)
|
||||
minBottom = box.Bottom;
|
||||
}
|
||||
|
||||
if (minBottom > work.Bottom)
|
||||
{
|
||||
var strip = new Box(work.Left, work.Bottom, work.Width, minBottom - work.Bottom);
|
||||
if (strip.Area() > 1.0)
|
||||
results.Add(strip);
|
||||
}
|
||||
|
||||
// Left strip: from work area left to the leftmost part edge
|
||||
var minLeft = double.MaxValue;
|
||||
foreach (var box in obstacles)
|
||||
{
|
||||
if (box.Left < minLeft)
|
||||
minLeft = box.Left;
|
||||
}
|
||||
|
||||
if (minLeft > work.Left)
|
||||
{
|
||||
var strip = new Box(work.Left, work.Bottom, minLeft - work.Left, work.Height);
|
||||
if (strip.Area() > 1.0)
|
||||
results.Add(strip);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
OpenNest.IO/OpenNest.IO.csproj
Normal file
11
OpenNest.IO/OpenNest.IO.csproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>OpenNest.IO</RootNamespace>
|
||||
<AssemblyName>OpenNest.IO</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -6,7 +6,7 @@ using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.IO
|
||||
{
|
||||
internal sealed class ProgramReader
|
||||
public sealed class ProgramReader
|
||||
{
|
||||
private const int BufferSize = 200;
|
||||
|
||||
61
OpenNest.Mcp/NestSession.cs
Normal file
61
OpenNest.Mcp/NestSession.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Mcp
|
||||
{
|
||||
public class NestSession
|
||||
{
|
||||
public Nest Nest { get; set; }
|
||||
public List<Plate> Plates { get; } = new();
|
||||
public List<Drawing> Drawings { get; } = new();
|
||||
|
||||
public Plate GetPlate(int index)
|
||||
{
|
||||
if (Nest != null && index < Nest.Plates.Count)
|
||||
return Nest.Plates[index];
|
||||
|
||||
var adjustedIndex = index - (Nest?.Plates.Count ?? 0);
|
||||
if (adjustedIndex >= 0 && adjustedIndex < Plates.Count)
|
||||
return Plates[adjustedIndex];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Drawing GetDrawing(string name)
|
||||
{
|
||||
if (Nest != null)
|
||||
{
|
||||
foreach (var d in Nest.Drawings)
|
||||
{
|
||||
if (d.Name == name)
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var d in Drawings)
|
||||
{
|
||||
if (d.Name == name)
|
||||
return d;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Plate> AllPlates()
|
||||
{
|
||||
var all = new List<Plate>();
|
||||
if (Nest != null)
|
||||
all.AddRange(Nest.Plates);
|
||||
all.AddRange(Plates);
|
||||
return all;
|
||||
}
|
||||
|
||||
public List<Drawing> AllDrawings()
|
||||
{
|
||||
var all = new List<Drawing>();
|
||||
if (Nest != null)
|
||||
all.AddRange(Nest.Drawings);
|
||||
all.AddRange(Drawings);
|
||||
return all;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
OpenNest.Mcp/OpenNest.Mcp.csproj
Normal file
18
OpenNest.Mcp/OpenNest.Mcp.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>OpenNest.Mcp</RootNamespace>
|
||||
<AssemblyName>OpenNest.Mcp</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="0.*-*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
15
OpenNest.Mcp/Program.cs
Normal file
15
OpenNest.Mcp/Program.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenNest.Mcp;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
builder.Services.AddSingleton<NestSession>();
|
||||
builder.Services
|
||||
.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithToolsFromAssembly(typeof(Program).Assembly);
|
||||
|
||||
var app = builder.Build();
|
||||
await app.RunAsync();
|
||||
214
OpenNest.Mcp/Tools/InputTools.cs
Normal file
214
OpenNest.Mcp/Tools/InputTools.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using CncProgram = OpenNest.CNC.Program;
|
||||
|
||||
namespace OpenNest.Mcp.Tools
|
||||
{
|
||||
[McpServerToolType]
|
||||
public class InputTools
|
||||
{
|
||||
private readonly NestSession _session;
|
||||
|
||||
public InputTools(NestSession session)
|
||||
{
|
||||
_session = session;
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "load_nest")]
|
||||
[Description("Load a .nest zip file into the session. Returns a summary of plates, parts, and drawings.")]
|
||||
public string LoadNest([Description("Absolute path to the .nest file")] string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return $"Error: file not found: {path}";
|
||||
|
||||
var reader = new NestReader(path);
|
||||
var nest = reader.Read();
|
||||
_session.Nest = nest;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Loaded nest: {nest.Name}");
|
||||
sb.AppendLine($"Units: {nest.Units}");
|
||||
sb.AppendLine($"Plates: {nest.Plates.Count}");
|
||||
|
||||
for (var i = 0; i < nest.Plates.Count; i++)
|
||||
{
|
||||
var plate = nest.Plates[i];
|
||||
var work = plate.WorkArea();
|
||||
sb.AppendLine($" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Height:F1}, " +
|
||||
$"parts={plate.Parts.Count}, " +
|
||||
$"utilization={plate.Utilization():P1}, " +
|
||||
$"work area={work.Width:F1} x {work.Height:F1}");
|
||||
}
|
||||
|
||||
sb.AppendLine($"Drawings: {nest.Drawings.Count}");
|
||||
|
||||
foreach (var dwg in nest.Drawings)
|
||||
{
|
||||
var bbox = dwg.Program.BoundingBox();
|
||||
sb.AppendLine($" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Height:F2}, " +
|
||||
$"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "import_dxf")]
|
||||
[Description("Import a DXF file as a new drawing. Returns drawing name and bounding box.")]
|
||||
public string ImportDxf(
|
||||
[Description("Absolute path to the DXF file")] string path,
|
||||
[Description("Name for the drawing (defaults to filename without extension)")] string name = null)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return $"Error: file not found: {path}";
|
||||
|
||||
var importer = new DxfImporter();
|
||||
|
||||
if (!importer.GetGeometry(path, out var geometry))
|
||||
return "Error: failed to read DXF file";
|
||||
|
||||
if (geometry.Count == 0)
|
||||
return "Error: no geometry found in DXF file";
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(geometry);
|
||||
|
||||
if (pgm == null)
|
||||
return "Error: failed to convert geometry to program";
|
||||
|
||||
var drawingName = name ?? Path.GetFileNameWithoutExtension(path);
|
||||
var drawing = new Drawing(drawingName, pgm);
|
||||
_session.Drawings.Add(drawing);
|
||||
|
||||
var bbox = pgm.BoundingBox();
|
||||
return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "create_drawing")]
|
||||
[Description("Create a drawing from a built-in shape or G-code string. Shape can be: rectangle, circle, l_shape, t_shape, gcode.")]
|
||||
public string CreateDrawing(
|
||||
[Description("Name for the drawing")] string name,
|
||||
[Description("Shape type: rectangle, circle, l_shape, t_shape, gcode")] string shape,
|
||||
[Description("Width of the shape (not used for circle or gcode)")] double width = 10,
|
||||
[Description("Height of the shape (not used for circle or gcode)")] double height = 10,
|
||||
[Description("Radius for circle shape")] double radius = 5,
|
||||
[Description("G-code string (only used when shape is 'gcode')")] string gcode = null)
|
||||
{
|
||||
CncProgram pgm;
|
||||
|
||||
switch (shape.ToLower())
|
||||
{
|
||||
case "rectangle":
|
||||
pgm = CreateRectangle(width, height);
|
||||
break;
|
||||
|
||||
case "circle":
|
||||
pgm = CreateCircle(radius);
|
||||
break;
|
||||
|
||||
case "l_shape":
|
||||
pgm = CreateLShape(width, height);
|
||||
break;
|
||||
|
||||
case "t_shape":
|
||||
pgm = CreateTShape(width, height);
|
||||
break;
|
||||
|
||||
case "gcode":
|
||||
if (string.IsNullOrWhiteSpace(gcode))
|
||||
return "Error: gcode parameter is required when shape is 'gcode'";
|
||||
pgm = ParseGcode(gcode);
|
||||
if (pgm == null)
|
||||
return "Error: failed to parse G-code";
|
||||
break;
|
||||
|
||||
default:
|
||||
return $"Error: unknown shape '{shape}'. Use: rectangle, circle, l_shape, t_shape, gcode";
|
||||
}
|
||||
|
||||
var drawing = new Drawing(name, pgm);
|
||||
_session.Drawings.Add(drawing);
|
||||
|
||||
var bbox = pgm.BoundingBox();
|
||||
return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
|
||||
}
|
||||
|
||||
private static CncProgram CreateRectangle(double width, double height)
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, width, 0),
|
||||
new Line(width, 0, width, height),
|
||||
new Line(width, height, 0, height),
|
||||
new Line(0, height, 0, 0)
|
||||
};
|
||||
|
||||
return ConvertGeometry.ToProgram(entities);
|
||||
}
|
||||
|
||||
private static CncProgram CreateCircle(double radius)
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Circle(0, 0, radius)
|
||||
};
|
||||
|
||||
return ConvertGeometry.ToProgram(entities);
|
||||
}
|
||||
|
||||
private static CncProgram CreateLShape(double width, double height)
|
||||
{
|
||||
var hw = width / 2;
|
||||
var hh = height / 2;
|
||||
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, width, 0),
|
||||
new Line(width, 0, width, hh),
|
||||
new Line(width, hh, hw, hh),
|
||||
new Line(hw, hh, hw, height),
|
||||
new Line(hw, height, 0, height),
|
||||
new Line(0, height, 0, 0)
|
||||
};
|
||||
|
||||
return ConvertGeometry.ToProgram(entities);
|
||||
}
|
||||
|
||||
private static CncProgram CreateTShape(double width, double height)
|
||||
{
|
||||
var stemWidth = width / 3;
|
||||
var topHeight = height / 3;
|
||||
var stemLeft = (width - stemWidth) / 2;
|
||||
var stemRight = stemLeft + stemWidth;
|
||||
var stemBottom = 0.0;
|
||||
var stemTop = height - topHeight;
|
||||
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(stemLeft, stemBottom, stemRight, stemBottom),
|
||||
new Line(stemRight, stemBottom, stemRight, stemTop),
|
||||
new Line(stemRight, stemTop, width, stemTop),
|
||||
new Line(width, stemTop, width, height),
|
||||
new Line(width, height, 0, height),
|
||||
new Line(0, height, 0, stemTop),
|
||||
new Line(0, stemTop, stemLeft, stemTop),
|
||||
new Line(stemLeft, stemTop, stemLeft, stemBottom)
|
||||
};
|
||||
|
||||
return ConvertGeometry.ToProgram(entities);
|
||||
}
|
||||
|
||||
private static CncProgram ParseGcode(string gcode)
|
||||
{
|
||||
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(gcode));
|
||||
var reader = new ProgramReader(stream);
|
||||
var pgm = reader.Read();
|
||||
reader.Close();
|
||||
return pgm;
|
||||
}
|
||||
}
|
||||
}
|
||||
136
OpenNest.Mcp/Tools/InspectionTools.cs
Normal file
136
OpenNest.Mcp/Tools/InspectionTools.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Mcp.Tools
|
||||
{
|
||||
[McpServerToolType]
|
||||
public class InspectionTools
|
||||
{
|
||||
private readonly NestSession _session;
|
||||
|
||||
public InspectionTools(NestSession session)
|
||||
{
|
||||
_session = session;
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "get_plate_info")]
|
||||
[Description("Get detailed information about a plate including dimensions, part count, utilization, remnants, and drawing breakdown.")]
|
||||
public string GetPlateInfo(
|
||||
[Description("Index of the plate")] int plateIndex)
|
||||
{
|
||||
var plate = _session.GetPlate(plateIndex);
|
||||
if (plate == null)
|
||||
return $"Error: plate {plateIndex} not found";
|
||||
|
||||
var work = plate.WorkArea();
|
||||
var remnants = plate.GetRemnants();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Plate {plateIndex}:");
|
||||
sb.AppendLine($" Size: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
|
||||
sb.AppendLine($" Quadrant: {plate.Quadrant}");
|
||||
sb.AppendLine($" Thickness: {plate.Thickness:F2}");
|
||||
sb.AppendLine($" Material: {plate.Material.Name}");
|
||||
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
|
||||
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
|
||||
sb.AppendLine($" Work area: {work.X:F1},{work.Y:F1} {work.Width:F1}x{work.Height:F1}");
|
||||
sb.AppendLine($" Parts: {plate.Parts.Count}");
|
||||
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
|
||||
sb.AppendLine($" Quantity: {plate.Quantity}");
|
||||
|
||||
// Drawing breakdown
|
||||
if (plate.Parts.Count > 0)
|
||||
{
|
||||
sb.AppendLine(" Drawings:");
|
||||
var groups = plate.Parts.GroupBy(p => p.BaseDrawing.Name);
|
||||
foreach (var group in groups)
|
||||
sb.AppendLine($" {group.Key}: {group.Count()}");
|
||||
}
|
||||
|
||||
// Remnants
|
||||
sb.AppendLine($" Remnants: {remnants.Count}");
|
||||
for (var i = 0; i < remnants.Count; i++)
|
||||
{
|
||||
var r = remnants[i];
|
||||
sb.AppendLine($" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Height:F1}, area={r.Area():F1}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "get_parts")]
|
||||
[Description("List placed parts on a plate with index, drawing name, location, rotation, and bounding box.")]
|
||||
public string GetParts(
|
||||
[Description("Index of the plate")] int plateIndex,
|
||||
[Description("Maximum number of parts to list (default 50)")] int limit = 50)
|
||||
{
|
||||
var plate = _session.GetPlate(plateIndex);
|
||||
if (plate == null)
|
||||
return $"Error: plate {plateIndex} not found";
|
||||
|
||||
if (plate.Parts.Count == 0)
|
||||
return $"Plate {plateIndex} has no parts";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Plate {plateIndex}: {plate.Parts.Count} parts (showing up to {limit})");
|
||||
|
||||
var count = System.Math.Min(plate.Parts.Count, limit);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var part = plate.Parts[i];
|
||||
var bbox = part.BoundingBox;
|
||||
var rotDeg = Angle.ToDegrees(part.Rotation);
|
||||
|
||||
sb.AppendLine($" [{i}] {part.BaseDrawing.Name}: " +
|
||||
$"loc=({part.Location.X:F2},{part.Location.Y:F2}), " +
|
||||
$"rot={rotDeg:F1} deg, " +
|
||||
$"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Height:F2})");
|
||||
}
|
||||
|
||||
if (plate.Parts.Count > limit)
|
||||
sb.AppendLine($" ... and {plate.Parts.Count - limit} more");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "check_overlaps")]
|
||||
[Description("Check a plate for overlapping parts. Reports collision points if any.")]
|
||||
public string CheckOverlaps(
|
||||
[Description("Index of the plate")] int plateIndex)
|
||||
{
|
||||
var plate = _session.GetPlate(plateIndex);
|
||||
if (plate == null)
|
||||
return $"Error: plate {plateIndex} not found";
|
||||
|
||||
if (plate.Parts.Count < 2)
|
||||
return $"Plate {plateIndex}: no overlaps possible (fewer than 2 parts)";
|
||||
|
||||
var hasOverlaps = plate.HasOverlappingParts(out var pts);
|
||||
|
||||
if (!hasOverlaps)
|
||||
return $"Plate {plateIndex}: no overlapping parts detected";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Plate {plateIndex}: {pts.Count} collision point(s) detected!");
|
||||
|
||||
var limit = System.Math.Min(pts.Count, 20);
|
||||
|
||||
for (var i = 0; i < limit; i++)
|
||||
{
|
||||
var pt = pts[i];
|
||||
sb.AppendLine($" Collision at ({pt.X:F2}, {pt.Y:F2})");
|
||||
}
|
||||
|
||||
if (pts.Count > limit)
|
||||
sb.AppendLine($" ... and {pts.Count - limit} more collision points");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
194
OpenNest.Mcp/Tools/NestingTools.cs
Normal file
194
OpenNest.Mcp/Tools/NestingTools.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
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.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";
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
69
OpenNest.Mcp/Tools/SetupTools.cs
Normal file
69
OpenNest.Mcp/Tools/SetupTools.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using ModelContextProtocol.Server;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Mcp.Tools
|
||||
{
|
||||
[McpServerToolType]
|
||||
public class SetupTools
|
||||
{
|
||||
private readonly NestSession _session;
|
||||
|
||||
public SetupTools(NestSession session)
|
||||
{
|
||||
_session = session;
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "create_plate")]
|
||||
[Description("Create a new plate with the given dimensions and spacing. Returns plate index and work area.")]
|
||||
public string CreatePlate(
|
||||
[Description("Plate width")] double width,
|
||||
[Description("Plate height")] double height,
|
||||
[Description("Spacing between parts (default 0)")] double partSpacing = 0,
|
||||
[Description("Edge spacing on all sides (default 0)")] double edgeSpacing = 0,
|
||||
[Description("Quadrant 1-4 (default 1). 1=TopRight, 2=TopLeft, 3=BottomLeft, 4=BottomRight")] int quadrant = 1,
|
||||
[Description("Material name (optional)")] string material = null)
|
||||
{
|
||||
var plate = new Plate(width, height);
|
||||
plate.PartSpacing = partSpacing;
|
||||
plate.EdgeSpacing = new Spacing(edgeSpacing, edgeSpacing);
|
||||
plate.Quadrant = quadrant;
|
||||
plate.Quantity = 1;
|
||||
|
||||
if (!string.IsNullOrEmpty(material))
|
||||
plate.Material.Name = material;
|
||||
|
||||
_session.Plates.Add(plate);
|
||||
|
||||
var allPlates = _session.AllPlates();
|
||||
var index = allPlates.Count - 1;
|
||||
var work = plate.WorkArea();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Created plate {index}: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
|
||||
sb.AppendLine($" Quadrant: {plate.Quadrant}");
|
||||
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
|
||||
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
|
||||
sb.AppendLine($" Work area: {work.Width:F1} x {work.Height:F1}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "clear_plate")]
|
||||
[Description("Remove all parts from a plate. Returns how many parts were removed.")]
|
||||
public string ClearPlate(
|
||||
[Description("Index of the plate to clear")] int plateIndex)
|
||||
{
|
||||
var plate = _session.GetPlate(plateIndex);
|
||||
|
||||
if (plate == null)
|
||||
return $"Error: plate {plateIndex} not found";
|
||||
|
||||
var count = plate.Parts.Count;
|
||||
plate.Parts.Clear();
|
||||
|
||||
return $"Cleared plate {plateIndex}: removed {count} parts";
|
||||
}
|
||||
}
|
||||
}
|
||||
28
OpenNest.sln
28
OpenNest.sln
@@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Engine", "OpenNest
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Gpu", "OpenNest.Gpu\OpenNest.Gpu.csproj", "{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.IO", "OpenNest.IO\OpenNest.IO.csproj", "{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNest.Mcp", "OpenNest.Mcp\OpenNest.Mcp.csproj", "{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -69,6 +73,30 @@ Global
|
||||
{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1F0DD58E-9E83-4F78-A9D9-0557C0B2D96F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1EFCF5FB-7ADE-4044-B55D-60F6F75C3A8B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x64.Build.0 = Release|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{61CC6F65-8B70-408A-B49A-F4E5F34FFD01}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest.Gpu\OpenNest.Gpu.csproj" />
|
||||
<PackageReference Include="ACadSharp" Version="3.1.32" />
|
||||
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user