Compare commits
17 Commits
9d40d78562
...
dde07fc256
| Author | SHA1 | Date | |
|---|---|---|---|
| dde07fc256 | |||
| 788996abcf | |||
| 224fbde19a | |||
| a0865405e2 | |||
| 6e5471271d | |||
| 0651f185e3 | |||
| 33377291a6 | |||
| dfd5a15274 | |||
| 09a7608bcb | |||
| 92d2d6d2bc | |||
| 641734ba70 | |||
| 5d0de4a1b1 | |||
| f92d09a863 | |||
| 5670ae79bf | |||
| aedbbbe0a6 | |||
| 5d9d48ebfc | |||
| eb6cb94893 |
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 AJ Isaacs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class CircleShape : ShapeDefinition
|
||||
{
|
||||
public double Diameter { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Circle(0, 0, Diameter / 2.0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class IsoscelesTriangleShape : ShapeDefinition
|
||||
{
|
||||
public double Base { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var midX = Base / 2.0;
|
||||
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, Base, 0),
|
||||
new Line(Base, 0, midX, Height),
|
||||
new Line(midX, Height, 0, 0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class LShape : ShapeDefinition
|
||||
{
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double LegWidth { get; set; }
|
||||
public double LegHeight { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var lw = LegWidth > 0 ? LegWidth : Width / 2.0;
|
||||
var lh = LegHeight > 0 ? LegHeight : Height / 2.0;
|
||||
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, Width, 0),
|
||||
new Line(Width, 0, Width, lh),
|
||||
new Line(Width, lh, lw, lh),
|
||||
new Line(lw, lh, lw, Height),
|
||||
new Line(lw, Height, 0, Height),
|
||||
new Line(0, Height, 0, 0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class OctagonShape : ShapeDefinition
|
||||
{
|
||||
public double Width { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var center = Width / 2.0;
|
||||
var circumRadius = Width / (2.0 * System.Math.Cos(System.Math.PI / 8.0));
|
||||
|
||||
var vertices = new Vector[8];
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var angle = System.Math.PI / 8.0 + i * System.Math.PI / 4.0;
|
||||
vertices[i] = new Vector(
|
||||
center + circumRadius * System.Math.Cos(angle),
|
||||
center + circumRadius * System.Math.Sin(angle));
|
||||
}
|
||||
|
||||
var entities = new List<Entity>();
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var next = (i + 1) % 8;
|
||||
entities.Add(new Line(vertices[i], vertices[next]));
|
||||
}
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class RectangleShape : ShapeDefinition
|
||||
{
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
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 CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class RightTriangleShape : ShapeDefinition
|
||||
{
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, Width, 0),
|
||||
new Line(Width, 0, 0, Height),
|
||||
new Line(0, Height, 0, 0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class RingShape : ShapeDefinition
|
||||
{
|
||||
public double OuterDiameter { get; set; }
|
||||
public double InnerDiameter { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Circle(0, 0, OuterDiameter / 2.0),
|
||||
new Circle(0, 0, InnerDiameter / 2.0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class RoundedRectangleShape : ShapeDefinition
|
||||
{
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double Radius { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var r = Radius;
|
||||
var entities = new List<Entity>();
|
||||
|
||||
if (r <= 0)
|
||||
{
|
||||
entities.Add(new Line(0, 0, Width, 0));
|
||||
entities.Add(new Line(Width, 0, Width, Height));
|
||||
entities.Add(new Line(Width, Height, 0, Height));
|
||||
entities.Add(new Line(0, Height, 0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bottom edge (left to right, above bottom-left arc to bottom-right arc)
|
||||
entities.Add(new Line(r, 0, Width - r, 0));
|
||||
|
||||
// Bottom-right corner arc: center at (Width-r, r), from 270deg to 360deg
|
||||
entities.Add(new Arc(Width - r, r, r,
|
||||
Angle.ToRadians(270), Angle.ToRadians(360)));
|
||||
|
||||
// Right edge
|
||||
entities.Add(new Line(Width, r, Width, Height - r));
|
||||
|
||||
// Top-right corner arc: center at (Width-r, Height-r), from 0deg to 90deg
|
||||
entities.Add(new Arc(Width - r, Height - r, r,
|
||||
Angle.ToRadians(0), Angle.ToRadians(90)));
|
||||
|
||||
// Top edge (right to left)
|
||||
entities.Add(new Line(Width - r, Height, r, Height));
|
||||
|
||||
// Top-left corner arc: center at (r, Height-r), from 90deg to 180deg
|
||||
entities.Add(new Arc(r, Height - r, r,
|
||||
Angle.ToRadians(90), Angle.ToRadians(180)));
|
||||
|
||||
// Left edge
|
||||
entities.Add(new Line(0, Height - r, 0, r));
|
||||
|
||||
// Bottom-left corner arc: center at (r, r), from 180deg to 270deg
|
||||
entities.Add(new Arc(r, r, r,
|
||||
Angle.ToRadians(180), Angle.ToRadians(270)));
|
||||
}
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public abstract class ShapeDefinition
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
protected ShapeDefinition()
|
||||
{
|
||||
var typeName = GetType().Name;
|
||||
Name = typeName.EndsWith("Shape")
|
||||
? typeName.Substring(0, typeName.Length - 5)
|
||||
: typeName;
|
||||
}
|
||||
|
||||
public abstract Drawing GetDrawing();
|
||||
|
||||
protected Drawing CreateDrawing(List<Entity> entities)
|
||||
{
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
|
||||
if (pgm == null)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create program for shape '{Name}'. Check that parameters produce valid geometry.");
|
||||
|
||||
return new Drawing(Name, pgm);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class TShape : ShapeDefinition
|
||||
{
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double StemWidth { get; set; }
|
||||
public double BarHeight { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var sw = StemWidth > 0 ? StemWidth : Width / 3.0;
|
||||
var bh = BarHeight > 0 ? BarHeight : Height / 3.0;
|
||||
var stemLeft = (Width - sw) / 2.0;
|
||||
var stemRight = stemLeft + sw;
|
||||
var stemTop = Height - bh;
|
||||
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(stemLeft, 0, stemRight, 0),
|
||||
new Line(stemRight, 0, 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, 0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Shapes
|
||||
{
|
||||
public class TrapezoidShape : ShapeDefinition
|
||||
{
|
||||
public double TopWidth { get; set; }
|
||||
public double BottomWidth { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var offset = (BottomWidth - TopWidth) / 2.0;
|
||||
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, BottomWidth, 0),
|
||||
new Line(BottomWidth, 0, offset + TopWidth, Height),
|
||||
new Line(offset + TopWidth, Height, offset, Height),
|
||||
new Line(offset, Height, 0, 0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
@@ -6,6 +5,7 @@ using ModelContextProtocol.Server;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using OpenNest.Shapes;
|
||||
using CncProgram = OpenNest.CNC.Program;
|
||||
|
||||
namespace OpenNest.Mcp.Tools
|
||||
@@ -98,110 +98,48 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("Radius for circle shape")] double radius = 5,
|
||||
[Description("G-code string (only used when shape is 'gcode')")] string gcode = null)
|
||||
{
|
||||
CncProgram pgm;
|
||||
ShapeDefinition shapeDef;
|
||||
|
||||
switch (shape.ToLower())
|
||||
{
|
||||
case "rectangle":
|
||||
pgm = CreateRectangle(width, height);
|
||||
shapeDef = new RectangleShape { Name = name, Width = width, Height = height };
|
||||
break;
|
||||
|
||||
case "circle":
|
||||
pgm = CreateCircle(radius);
|
||||
shapeDef = new CircleShape { Name = name, Diameter = radius * 2 };
|
||||
break;
|
||||
|
||||
case "l_shape":
|
||||
pgm = CreateLShape(width, height);
|
||||
shapeDef = new LShape { Name = name, Width = width, Height = height };
|
||||
break;
|
||||
|
||||
case "t_shape":
|
||||
pgm = CreateTShape(width, height);
|
||||
shapeDef = new TShape { Name = name, Width = width, Height = height };
|
||||
break;
|
||||
|
||||
case "gcode":
|
||||
if (string.IsNullOrWhiteSpace(gcode))
|
||||
return "Error: gcode parameter is required when shape is 'gcode'";
|
||||
pgm = ParseGcode(gcode);
|
||||
var pgm = ParseGcode(gcode);
|
||||
if (pgm == null)
|
||||
return "Error: failed to parse G-code";
|
||||
break;
|
||||
var gcodeDrawing = new Drawing(name, pgm);
|
||||
_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 = new Drawing(name, pgm);
|
||||
var drawing = shapeDef.GetDrawing();
|
||||
_session.Drawings.Add(drawing);
|
||||
|
||||
var bbox = pgm.BoundingBox();
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Length: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));
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class CircleShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_ReturnsDrawingWithCorrectBoundingBox()
|
||||
{
|
||||
var shape = new CircleShape { Diameter = 10 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(10, bbox.Width, 0.01);
|
||||
Assert.Equal(10, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_DefaultName_IsCircle()
|
||||
{
|
||||
var shape = new CircleShape { Diameter = 10 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
Assert.Equal("Circle", drawing.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class IsoscelesTriangleShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxMatchesDimensions()
|
||||
{
|
||||
var shape = new IsoscelesTriangleShape { Base = 10, Height = 8 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(10, bbox.Width, 0.01);
|
||||
Assert.Equal(8, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_AreaIsHalfBaseTimesHeight()
|
||||
{
|
||||
var shape = new IsoscelesTriangleShape { Base = 10, Height = 8 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
Assert.Equal(40, drawing.Area, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class LShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxMatchesDimensions()
|
||||
{
|
||||
var shape = new LShape { Width = 10, Height = 20 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(10, bbox.Width, 0.01);
|
||||
Assert.Equal(20, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_DefaultLegDimensions()
|
||||
{
|
||||
var shape = new LShape { Width = 10, Height = 20 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// Default legs: LegWidth = Width/2 = 5, LegHeight = Height/2 = 10
|
||||
// Area = Width*Height - (Width - LegWidth) * (Height - LegHeight)
|
||||
// Area = 10*20 - 5*10 = 150
|
||||
Assert.Equal(150, drawing.Area, 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_CustomLegDimensions()
|
||||
{
|
||||
var shape = new LShape { Width = 10, Height = 20, LegWidth = 3, LegHeight = 5 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// Area = Width*Height - (Width - LegWidth) * (Height - LegHeight)
|
||||
// Area = 10*20 - 7*15 = 200 - 105 = 95
|
||||
Assert.Equal(95, drawing.Area, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class OctagonShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxFitsWithinExpectedSize()
|
||||
{
|
||||
var shape = new OctagonShape { Width = 20 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
// Corner-to-corner is larger than flat-to-flat
|
||||
Assert.True(bbox.Width >= 20 - 0.01);
|
||||
Assert.True(bbox.Length >= 20 - 0.01);
|
||||
// But should not be wildly larger (corner-to-corner ~ width / cos(22.5deg) ~ width * 1.0824)
|
||||
Assert.True(bbox.Width < 22);
|
||||
Assert.True(bbox.Length < 22);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_HasEightEdges()
|
||||
{
|
||||
var shape = new OctagonShape { Width = 20 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// An octagon program should have 8 linear moves (one per edge)
|
||||
var moves = drawing.Program.Codes
|
||||
.OfType<OpenNest.CNC.LinearMove>()
|
||||
.Count();
|
||||
Assert.Equal(8, moves);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class RectangleShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_ReturnsDrawingWithCorrectBoundingBox()
|
||||
{
|
||||
var shape = new RectangleShape { Width = 10, Height = 5 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(10, bbox.Width, 0.01);
|
||||
Assert.Equal(5, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_DefaultName_IsRectangle()
|
||||
{
|
||||
var shape = new RectangleShape { Width = 10, Height = 5 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
Assert.Equal("Rectangle", drawing.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_CustomName_IsUsed()
|
||||
{
|
||||
var shape = new RectangleShape { Name = "Plate1", Width = 10, Height = 5 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
Assert.Equal("Plate1", drawing.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class RightTriangleShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxMatchesDimensions()
|
||||
{
|
||||
var shape = new RightTriangleShape { Width = 12, Height = 8 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(12, bbox.Width, 0.01);
|
||||
Assert.Equal(8, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_AreaIsHalfWidthTimesHeight()
|
||||
{
|
||||
var shape = new RightTriangleShape { Width = 12, Height = 8 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
Assert.Equal(48, drawing.Area, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class RingShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxMatchesOuterDiameter()
|
||||
{
|
||||
var shape = new RingShape { OuterDiameter = 20, InnerDiameter = 10 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(20, bbox.Width, 0.01);
|
||||
Assert.Equal(20, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_AreaExcludesInnerHole()
|
||||
{
|
||||
var shape = new RingShape { OuterDiameter = 20, InnerDiameter = 10 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// Area = pi * (10^2 - 5^2) = pi * 75
|
||||
var expectedArea = System.Math.PI * 75;
|
||||
Assert.Equal(expectedArea, drawing.Area, 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_DefaultName_IsRing()
|
||||
{
|
||||
var shape = new RingShape { OuterDiameter = 20, InnerDiameter = 10 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
Assert.Equal("Ring", drawing.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class RoundedRectangleShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxMatchesDimensions()
|
||||
{
|
||||
var shape = new RoundedRectangleShape { Width = 20, Height = 10, Radius = 2 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(20, bbox.Width, 0.1);
|
||||
Assert.Equal(10, bbox.Length, 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_AreaIsLessThanFullRectangle()
|
||||
{
|
||||
var shape = new RoundedRectangleShape { Width = 20, Height = 10, Radius = 2 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// Area should be less than 20*10=200 because corners are rounded
|
||||
// Area = W*H - (4 - pi) * r^2 = 200 - (4 - pi) * 4 ~ 196.57
|
||||
Assert.True(drawing.Area < 200);
|
||||
Assert.True(drawing.Area > 190);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_ZeroRadius_MatchesRectangleArea()
|
||||
{
|
||||
var shape = new RoundedRectangleShape { Width = 20, Height = 10, Radius = 0 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
Assert.Equal(200, drawing.Area, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class TShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxMatchesDimensions()
|
||||
{
|
||||
var shape = new TShape { Width = 12, Height = 18 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(12, bbox.Width, 0.01);
|
||||
Assert.Equal(18, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_DefaultStemAndBarDimensions()
|
||||
{
|
||||
var shape = new TShape { Width = 12, Height = 18 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// Default: StemWidth = Width/3 = 4, BarHeight = Height/3 = 6
|
||||
// Area = Width * BarHeight + StemWidth * (Height - BarHeight)
|
||||
// Area = 12 * 6 + 4 * 12 = 72 + 48 = 120
|
||||
Assert.Equal(120, drawing.Area, 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_CustomStemAndBarDimensions()
|
||||
{
|
||||
var shape = new TShape { Width = 12, Height = 18, StemWidth = 6, BarHeight = 4 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// Area = Width * BarHeight + StemWidth * (Height - BarHeight)
|
||||
// Area = 12 * 4 + 6 * 14 = 48 + 84 = 132
|
||||
Assert.Equal(132, drawing.Area, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Shapes;
|
||||
|
||||
public class TrapezoidShapeTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetDrawing_BoundingBoxMatchesDimensions()
|
||||
{
|
||||
var shape = new TrapezoidShape { BottomWidth = 20, TopWidth = 10, Height = 8 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
var bbox = drawing.Program.BoundingBox();
|
||||
Assert.Equal(20, bbox.Width, 0.01);
|
||||
Assert.Equal(8, bbox.Length, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDrawing_AreaIsCorrect()
|
||||
{
|
||||
var shape = new TrapezoidShape { BottomWidth = 20, TopWidth = 10, Height = 8 };
|
||||
var drawing = shape.GetDrawing();
|
||||
|
||||
// Area = (top + bottom) / 2 * height = (10 + 20) / 2 * 8 = 120
|
||||
Assert.Equal(120, drawing.Area, 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# OpenNest
|
||||
|
||||
A Windows desktop app for CNC nesting — imports DXF drawings, arranges parts on plates and exports layouts as DXF or G-code for cutting.
|
||||
|
||||

|
||||
|
||||
OpenNest takes your part drawings, lets you define your sheet (plate) sizes, and arranges the parts to make efficient use of material. The result can be exported as DXF files or post-processed into G-code that your CNC cutting machine understands.
|
||||
|
||||
## Features
|
||||
|
||||
- **DXF Import/Export** — Load part drawings from DXF files and export completed nest layouts
|
||||
- **Multiple Fill Strategies** — Grid-based linear fill, NFP (No Fit Polygon) pair fitting, and rectangle bin packing
|
||||
- **Part Rotation** — Automatically tries different rotation angles to find better fits
|
||||
- **Gravity Compaction** — After placing parts, pushes them together to close gaps
|
||||
- **Multi-Plate Support** — Work with multiple plates of different sizes and materials in a single nest
|
||||
- **G-code Output** — Post-process nested layouts to G-code for CNC cutting machines
|
||||
- **Built-in Shapes** — Create basic geometric parts (circles, rectangles, triangles, etc.) without needing a DXF file
|
||||
- **Interactive Editing** — Zoom, pan, select, clone, and manually arrange parts on the plate view
|
||||
- **Lead-in/Lead-out & Tabs** — Configure cutting parameters like approach paths and holding tabs
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Windows 10 or later**
|
||||
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ajisaacs/OpenNest.git
|
||||
cd OpenNest
|
||||
dotnet build OpenNest.sln
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
dotnet run --project OpenNest/OpenNest.csproj
|
||||
```
|
||||
|
||||
Or open `OpenNest.sln` in Visual Studio and run the `OpenNest` project.
|
||||
|
||||
### Quick Walkthrough
|
||||
|
||||
1. **Create a nest** — File > New Nest
|
||||
2. **Add drawings** — Import DXF files or create built-in shapes (rectangles, circles, etc.). DXF drawings should be 1:1 scale CAD files.
|
||||
3. **Set up a plate** — Define the plate size and material
|
||||
4. **Fill the plate** — The nesting engine will automatically arrange parts on the plate
|
||||
5. **Export** — Save as a `.nest` file, export to DXF, or post-process to G-code
|
||||
|
||||
<!-- TODO: Add screenshots for each step -->
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
OpenNest.sln
|
||||
├── OpenNest/ # WinForms desktop application (UI)
|
||||
├── OpenNest.Core/ # Domain model, geometry, and CNC primitives
|
||||
├── OpenNest.Engine/ # Nesting algorithms (fill, pack, compact)
|
||||
├── OpenNest.IO/ # File I/O — DXF import/export, nest file format
|
||||
├── OpenNest.Console/ # Command-line interface for batch nesting
|
||||
├── OpenNest.Gpu/ # GPU-accelerated nesting evaluation
|
||||
├── OpenNest.Training/ # ML training data collection
|
||||
├── OpenNest.Mcp/ # MCP server for AI tool integration
|
||||
└── OpenNest.Tests/ # Unit tests
|
||||
```
|
||||
|
||||
For most users, only the first four matter:
|
||||
|
||||
| Project | What it does |
|
||||
|---------|-------------|
|
||||
| **OpenNest** | The app you run. WinForms UI with plate viewer, drawing list, and dialogs. |
|
||||
| **OpenNest.Core** | The building blocks — parts, plates, drawings, geometry, G-code representation. |
|
||||
| **OpenNest.Engine** | The brains — algorithms that decide where parts go on a plate. |
|
||||
| **OpenNest.IO** | Reads and writes files — DXF (via ACadSharp), G-code, and the `.nest` ZIP format. |
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Ctrl+F` | Fill the area around the cursor with the selected drawing |
|
||||
| `F` | Zoom to fit the plate view |
|
||||
|
||||
## Supported Formats
|
||||
|
||||
| Format | Import | Export |
|
||||
|--------|--------|--------|
|
||||
| DXF (AutoCAD Drawing Exchange) | Yes | Yes |
|
||||
| DWG (AutoCAD Drawing) | Yes | No |
|
||||
| G-code | No | Yes (via post-processors) |
|
||||
| `.nest` (ZIP-based project file) | Yes | Yes |
|
||||
|
||||
## Status
|
||||
|
||||
OpenNest is under active development. The core nesting workflows function, but there's plenty of room for improvement in packing efficiency, UI polish, and format support. Contributions and feedback are welcome.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
# Shape Library Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
A parametric shape library for OpenNest that provides reusable, self-describing shape classes for generating `Drawing` objects. Each shape is its own class with typed parameters, inheriting from an abstract `ShapeDefinition` base class. Inspired by PEP's WINSHAPE library.
|
||||
|
||||
## Location
|
||||
|
||||
- Project: `OpenNest.Core`
|
||||
- Folder: `Shapes/`
|
||||
- Namespace: `OpenNest.Shapes`
|
||||
|
||||
## Architecture
|
||||
|
||||
### Base Class — `ShapeDefinition`
|
||||
|
||||
Abstract base class that all shapes inherit from. `Name` defaults to the shape type name (e.g. `"Rectangle"`) but can be overridden.
|
||||
|
||||
```csharp
|
||||
public abstract class ShapeDefinition
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
protected ShapeDefinition()
|
||||
{
|
||||
// Default name to the concrete class name, stripping "Shape" suffix
|
||||
var typeName = GetType().Name;
|
||||
Name = typeName.EndsWith("Shape")
|
||||
? typeName.Substring(0, typeName.Length - 5)
|
||||
: typeName;
|
||||
}
|
||||
|
||||
public abstract Drawing GetDrawing();
|
||||
|
||||
protected Drawing CreateDrawing(List<Entity> entities)
|
||||
{
|
||||
var pgm = ConvertGeometry.ToProgram(entities);
|
||||
|
||||
if (pgm == null)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create program for shape '{Name}'. Check that parameters produce valid geometry.");
|
||||
|
||||
return new Drawing(Name, pgm);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `Name`: The name assigned to the resulting `Drawing`. Defaults to the shape class name without the "Shape" suffix. Never null.
|
||||
- `GetDrawing()`: Each shape implements this to build its geometry and return a `Drawing`.
|
||||
- `CreateDrawing()`: Shared helper that converts a list of geometry entities into a `Drawing` via `ConvertGeometry.ToProgram()`. Throws `InvalidOperationException` if the geometry is degenerate (prevents null `Program` from reaching `Drawing.UpdateArea()`).
|
||||
|
||||
### Shape Classes
|
||||
|
||||
#### Tier 1 — Basics (extracted from MCP InputTools)
|
||||
|
||||
| Class | Parameters | Description |
|
||||
|-------|-----------|-------------|
|
||||
| `RectangleShape` | `Width`, `Height` | Axis-aligned rectangle from origin |
|
||||
| `CircleShape` | `Diameter` | Circle centered at origin. Implementation divides by 2 for the `Circle` entity's radius parameter. |
|
||||
| `LShape` | `Width`, `Height`, `LegWidth`?, `LegHeight`? | L-shaped profile. `LegWidth` defaults to `Width/2`, `LegHeight` defaults to `Height/2`. |
|
||||
| `TShape` | `Width`, `Height`, `StemWidth`?, `BarHeight`? | T-shaped profile. `StemWidth` defaults to `Width/3`, `BarHeight` defaults to `Height/3`. |
|
||||
|
||||
#### Tier 2 — Common CNC shapes
|
||||
|
||||
| Class | Parameters | Description |
|
||||
|-------|-----------|-------------|
|
||||
| `RingShape` | `OuterDiameter`, `InnerDiameter` | Annular ring (two concentric circles). Both converted to radius internally. |
|
||||
| `RightTriangleShape` | `Width`, `Height` | Right triangle with the right angle at origin |
|
||||
| `IsoscelesTriangleShape` | `Base`, `Height` | Isosceles triangle centered on base |
|
||||
| `TrapezoidShape` | `TopWidth`, `BottomWidth`, `Height` | Trapezoid with bottom edge centered under top |
|
||||
| `OctagonShape` | `Width` | Regular octagon where `Width` is the flat-to-flat distance |
|
||||
| `RoundedRectangleShape` | `Width`, `Height`, `Radius` | Rectangle with 90-degree CCW arc corners |
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
OpenNest.Core/
|
||||
Shapes/
|
||||
ShapeDefinition.cs
|
||||
CircleShape.cs
|
||||
RectangleShape.cs
|
||||
RingShape.cs
|
||||
RightTriangleShape.cs
|
||||
IsoscelesTriangleShape.cs
|
||||
TrapezoidShape.cs
|
||||
OctagonShape.cs
|
||||
RoundedRectangleShape.cs
|
||||
LShape.cs
|
||||
TShape.cs
|
||||
```
|
||||
|
||||
### Geometry Construction
|
||||
|
||||
Each shape builds a `List<Entity>` (using `Line`, `Arc`, `Circle` from `OpenNest.Geometry`) and passes it to the base `CreateDrawing()` helper. Shapes are constructed at the origin (0,0) with positive X/Y extents.
|
||||
|
||||
- **Lines** for straight edges — endpoints must chain end-to-end for `ShapeBuilder` to detect closed shapes.
|
||||
- **Arcs** for rounded corners (`RoundedRectangleShape`). Arcs use CCW direction (not reversed) with angles in radians.
|
||||
- **Circles** for `CircleShape` and `RingShape` outer/inner boundaries.
|
||||
|
||||
### MCP Integration
|
||||
|
||||
`InputTools.CreateDrawing` in `OpenNest.Mcp` will be refactored to instantiate the appropriate `ShapeDefinition` subclass and call `GetDrawing()`, replacing the existing private `CreateRectangle`, `CreateCircle`, `CreateLShape`, `CreateTShape` methods. The MCP tool's existing flat parameter names (`radius`, `width`, `height`) are mapped to the shape class properties at the MCP layer. The `gcode` case remains as-is.
|
||||
|
||||
New Tier 2 shapes can be exposed via the MCP tool by extending the `shape` parameter's accepted values and mapping to the new shape classes, with additional MCP parameters as needed.
|
||||
|
||||
## Future Expansion
|
||||
|
||||
- Additional shapes (Tier 3): Single-D, Parallelogram, House, Stair, Rectangle with chamfer(s), Ring segment, Slot rectangle
|
||||
- UI shape picker with per-shape parameter editors
|
||||
- Shape discovery via reflection or static registry
|
||||
- `LShape`/`TShape` additional sub-dimension parameters for full parametric control
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Reference in New Issue
Block a user