feat(mcp): delete unused session plates

This commit is contained in:
aj committed 2026-10-09 10:45:28 -04:00
1 parent 2623517219
commit 8b94e49711
4 files changed
+124

No files matched your search

@@ -1,3 +1,4 @@
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.IO;
using OpenNest.Mcp;
@@ -35,4 +36,77 @@ public class McpPlateToolsTests : IDisposable
Assert.Same(appended, sessionOnly.GetPlate(0));
Assert.Null(sessionOnly.GetPlate(1));
}
private static string Delete(SetupTools tools, int index) => tools.DeletePlate(index);
[Fact]
public void DeleteEmptyLoadedMiddlePlateRenumbersAppendedAndPersists()
{
var session = new NestSession { Nest = new Nest("fixture") };
var drawing = new Drawing("square", new Program());
session.Nest.Drawings.Add(drawing);
var occupied = new Plate(20, 30) { Quantity = 2 };
occupied.Parts.Add(new Part(drawing));
var unused = new Plate(40, 50);
var appended = new Plate(60, 70) { PartSpacing = 1.25, Quantity = 3 };
session.Nest.Plates.Add(occupied);
session.Nest.Plates.Add(unused);
session.Plates.Add(appended);
var tools = new SetupTools(session);
var result = Delete(tools, 1);
Assert.Contains("Deleted plate 1", result);
Assert.Contains("2 -> 1", result);
Assert.Single(session.Nest.Plates);
Assert.Same(appended, session.GetPlate(1));
Assert.Null(session.GetPlate(2));
Assert.Contains("60.0 x 70.0", new InspectionTools(session).GetPlateInfo(1));
Assert.Equal(2, drawing.Quantity.Nested);
var path = Path.Combine(directory, "cleaned.nest");
Assert.Contains("Saved nest", new InputTools(session).SaveNest(path));
var reloaded = new NestReader(path).Read();
// NestWriter already omits empty plates; the deletion is session cleanup,
// and the saved file must still retain the occupied plate and its counts.
var saved = Assert.Single(reloaded.Plates);
Assert.Single(saved.Parts);
Assert.Equal(20, saved.Size.Width);
Assert.Equal(2, saved.Quantity);
Assert.Equal(2, reloaded.Drawings.Single().Quantity.Nested);
}
[Fact]
public void DeleteRefusesOccupiedCutoffAndInvalidIndicesWithoutMutation()
{
var session = new NestSession { Nest = new Nest("fixture") };
var drawing = new Drawing("square", new Program());
session.Nest.Drawings.Add(drawing);
var occupied = new Plate(20, 30) { Quantity = 4 };
var part = new Part(drawing);
occupied.Parts.Add(part);
var cutoff = new Plate(40, 50);
var definition = new CutOff(new Vector(10, 10), CutOffAxis.Vertical);
cutoff.CutOffs.Add(definition);
session.Nest.Plates.Add(occupied);
session.Plates.Add(cutoff);
var tools = new SetupTools(session);
Assert.Contains("occupied", Delete(tools, 0));
Assert.Contains("cutoff", Delete(tools, 1), StringComparison.OrdinalIgnoreCase);
Assert.Contains("not found", Delete(tools, -1));
Assert.Contains("not found", Delete(tools, 2));
Assert.Same(occupied, session.GetPlate(0));
Assert.Same(cutoff, session.GetPlate(1));
Assert.Same(part, Assert.Single(occupied.Parts));
Assert.Same(definition, Assert.Single(cutoff.CutOffs));
Assert.Equal(4, drawing.Quantity.Nested);
Assert.Single(session.Nest.Plates);
Assert.Single(session.Plates);
// The session-only collection must also delete without requiring a loaded nest.
var standalone = new NestSession();
standalone.Plates.Add(new Plate(12, 13));
Assert.Contains("Deleted plate 0", Delete(new SetupTools(standalone), 0));
Assert.Empty(standalone.Plates);
}
}
+32
View File
@@ -30,6 +30,38 @@ namespace OpenNest.Mcp
return null;
}
/// <summary>Remove only a genuinely unused sheet; loaded sheets precede appended sheets.</summary>
public bool TryDeleteEmptyPlate(int index, out string reason)
{
var plate = GetPlate(index);
if (plate == null)
{
reason = $"plate {index} not found";
return false;
}
if (plate.Parts.Count != 0)
{
reason = $"plate {index} is occupied";
return false;
}
if (plate.CutOffs.Count != 0)
{
reason = $"plate {index} has cutoff definitions";
return false;
}
var loadedCount = Nest?.Plates.Count ?? 0;
if (index < loadedCount)
Nest.Plates.RemoveAt(index);
else
Plates.RemoveAt(index - loadedCount);
reason = null;
return true;
}
public Drawing GetDrawing(string name)
{
if (Nest != null)
+16
View File
@@ -58,6 +58,22 @@ namespace OpenNest.Mcp.Tools
return sb.ToString();
}
[McpServerTool(Name = "delete_plate")]
[Description("Delete an empty plate without parts or cutoff definitions. Later plate indices shift down by one.")]
public string DeletePlate([Description("Index of the empty plate to delete")] int plateIndex)
{
var oldCount = _session.AllPlates().Count;
if (!_session.TryDeleteEmptyPlate(plateIndex, out var reason))
return $"Error: {reason}; no plates changed";
var sb = new StringBuilder();
sb.AppendLine($"Deleted plate {plateIndex}. Plates: {oldCount} -> {oldCount - 1}.");
for (var oldIndex = plateIndex + 1; oldIndex < oldCount; oldIndex++)
sb.AppendLine($" {oldIndex} -> {oldIndex - 1}");
sb.AppendLine("Use get_plate_info with the new indices before further operations.");
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)
+2
View File
@@ -48,6 +48,8 @@ Invalid output is printed and rejected with exit code 2 without saving or postin
MCP `autonest_plate` requires an empty target. The stdio server serializes all tool calls sharing its mutable session, so another request cannot change drawings or occupy a target during a solve. `allow_invalid` defaults to false. It reports violations and makes no changes on rejection, including with an override when the output is unrepresentable or contains multiple sheets. `engine` defaults to Default, and its description lists the built-in engines with what each suits. Fill tools (`fill_plate`, `fill_area`, `fill_remnants`, `pack_plate`) remain separate and default to the Fill strategy. Console and MCP load jobs plug-ins from `Engines/` beside their executable.
MCP `delete_plate(plateIndex)` removes only a sheet with no parts and no cutoff definitions, from either loaded or newly created session plates. Occupied, cutoff-bearing and invalid indices are refused without changing the session. After deletion every later index shifts down by one; inspect `get_plate_info` before further edits. This is session cleanup, not a data purge or a save-time option. `save_nest` lists all session plates in its response, but the existing `.nest` writer omits empty cutoff-free plates from the serialized file; reloading may therefore have fewer plates than the response says. There is no opt-in switch to preserve empty plates in the file.
### MCP engine development harness
`test_engine` builds and runs `OpenNest.Console` in a configured, trusted checkout.