Files
OpenNest/OpenNest.Mcp/Tools/InputTools.cs
T
ajandClaude Opus 5.5 3a8994c8fe feat(io): import and export SVG drawings
Customers and design tools hand over SVGs, and getting them in meant a
trip through another CAD tool. CadImporter now reads them like any other
drawing, and plates, programs and converter entities can be written out.

An SVG's numbers are not drawing units, so import converts to the nest's
units and never guesses: only an absolute width or height counts as a
stated size, since px means 1/96 in to browsers but 1/72 in to
Illustrator. Without one the desktop asks, showing the size each choice
gives, and headless callers must pass the scale or the import fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:50:41 -04:00

229 lines
8.8 KiB
C#

using System.ComponentModel;
using System.IO;
using System.Text;
using ModelContextProtocol.Server;
using OpenNest.Engine.BestFit;
using OpenNest.IO;
using OpenNest.Shapes;
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 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) { BestFitStore = BestFitCacheStore.Shared };
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.Length:F1}, "
+ $"parts={plate.Parts.Count}, "
+ $"utilization={plate.Utilization():P1}, "
+ $"work area={work.Width:F1} x {work.Length: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.Length:F2}, "
+ $"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}"
);
}
return sb.ToString();
}
[McpServerTool(Name = "save_nest")]
[Description("Save the current session (all drawings and plates) to a .nest file.")]
public string SaveNest(
[Description("Absolute path for the output .nest file")] string path,
[Description("Name for the nest (optional)")] string name = null
)
{
var source = _session.Nest;
var nest = new Nest
{
Name = name ?? (string.IsNullOrWhiteSpace(source?.Name) ? Path.GetFileNameWithoutExtension(path) : source.Name),
Customer = source?.Customer ?? "",
Notes = source?.Notes ?? "",
AssistGas = source?.AssistGas ?? "",
Status = source?.Status ?? NestStatus.Quote,
MadeBy = source?.MadeBy ?? "",
Thickness = source?.Thickness ?? 0,
Material = source?.Material ?? new Material(),
Units = source?.Units ?? default,
DateCreated = source?.DateCreated ?? default,
DateLastModified = source?.DateLastModified ?? default,
PlateDefaults = source?.PlateDefaults ?? new Nest.PlateSettings(),
PlateOptions = source?.PlateOptions ?? new(),
SalvageRate = source?.SalvageRate ?? 0.5,
};
foreach (var drawing in _session.AllDrawings())
nest.Drawings.Add(drawing);
foreach (var plate in _session.AllPlates())
nest.Plates.Add(plate);
if (nest.Drawings.Count == 0)
return "Error: no drawings in session to save";
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var writer = new NestWriter(nest) { BestFitStore = BestFitCacheStore.Shared };
if (!writer.Write(path))
return "Error: failed to write nest file";
return $"Saved nest to {path}\n Drawings: {nest.Drawings.Count}\n Plates: {nest.Plates.Count}";
}
[McpServerTool(Name = "import_dxf")]
[Description("Import a DXF, DWG or SVG file as a new drawing. Returns drawing name and bounding box.")]
public string ImportDxf(
[Description("Absolute path to the drawing file")] string path,
[Description("Name for the drawing (defaults to filename without extension)")]
string name = null,
[Description("How many of an SVG's units make an inch; required only when the file does not state its real-world size")]
double? svgUnitsPerInch = null
)
{
if (!File.Exists(path))
return $"Error: file not found: {path}";
try
{
var drawing = CadImporter.ImportDrawing(
path,
new CadImportOptions
{
Name = name,
DrawingUnits = _session.Nest?.Units ?? default,
SvgUserUnitsPerInch = svgUnitsPerInch,
}
);
_session.Drawings.Add(drawing);
var bbox = drawing.Program.BoundingBox();
return $"Imported drawing '{drawing.Name}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
}
catch (System.Exception ex)
{
return $"Error: failed to import '{path}': {ex.Message}";
}
}
[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("Length of the shape (not used for circle or gcode)")] double length = 10,
[Description("Radius for circle shape")] double radius = 5,
[Description("G-code string (only used when shape is 'gcode')")] string gcode = null
)
{
ShapeDefinition shapeDef;
switch (shape.ToLower())
{
case "rectangle":
shapeDef = new RectangleShape
{
Name = name,
Width = width,
Length = length,
};
break;
case "circle":
shapeDef = new CircleShape { Name = name, Diameter = radius * 2 };
break;
case "l_shape":
shapeDef = new LShape
{
Name = name,
Width = width,
Height = length,
};
break;
case "t_shape":
shapeDef = new TShape
{
Name = name,
Width = width,
Height = length,
};
break;
case "gcode":
if (string.IsNullOrWhiteSpace(gcode))
return "Error: gcode parameter is required when shape is 'gcode'";
var pgm = ParseGcode(gcode);
if (pgm == null)
return "Error: failed to parse G-code";
var gcodeDrawing = new Drawing(name, pgm);
gcodeDrawing.Color = Drawing.GetNextColor();
_session.Drawings.Add(gcodeDrawing);
var gcodeBbox = pgm.BoundingBox();
return $"Created drawing '{name}': bbox={gcodeBbox.Width:F2} x {gcodeBbox.Length:F2}";
default:
return $"Error: unknown shape '{shape}'. Use: rectangle, circle, l_shape, t_shape, gcode";
}
var drawing = shapeDef.GetDrawing();
drawing.Color = Drawing.GetNextColor();
_session.Drawings.Add(drawing);
var bbox = drawing.Program.BoundingBox();
return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
}
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;
}
}
}