feat: mirror axis simplifier, bend note propagation, ellipse fixes
Geometry Simplifier: - Replace least-squares circle fitting with mirror axis algorithm that constrains center to perpendicular bisector of chord, guaranteeing zero-gap endpoint connectivity by construction - Golden section search optimizes center position along the axis - Increase default tolerance from 0.005 to 0.5 for practical CNC use - Support existing arcs in simplification runs (sample arc points to find larger replacement arcs spanning lines + arcs together) - Add tolerance zone visualization (offset original geometry ±tolerance) - Show original geometry overlay with orange dashed lines in preview - Add "Original" checkbox to CadConverter for comparing old vs new - Store OriginalEntities on FileListItem to prevent tolerance creep when re-running simplifier with different settings Bend Detection: - Propagate bend notes to collinear bend lines split by cutouts using infinite-line perpendicular distance check - Add bend note text rendering in EntityView at bend line midpoints DXF Import: - Fix trimmed ellipse closing chord: only close when sweep ≈ 2π, preventing phantom lines through slot cutouts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ public class ArcCandidate
|
||||
|
||||
public class GeometrySimplifier
|
||||
{
|
||||
public double Tolerance { get; set; } = 0.005;
|
||||
public double Tolerance { get; set; } = 0.5;
|
||||
public int MinLines { get; set; } = 3;
|
||||
|
||||
public List<ArcCandidate> Analyze(Shape shape)
|
||||
@@ -30,21 +30,26 @@ public class GeometrySimplifier
|
||||
|
||||
while (i < entities.Count)
|
||||
{
|
||||
if (entities[i] is not Line firstLine)
|
||||
if (entities[i] is not Line and not Arc)
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect consecutive lines on the same layer
|
||||
// Collect consecutive lines and arcs on the same layer
|
||||
var runStart = i;
|
||||
var layer = firstLine.Layer;
|
||||
while (i < entities.Count && entities[i] is Line line && line.Layer == layer)
|
||||
var layer = entities[i].Layer;
|
||||
var lineCount = 0;
|
||||
while (i < entities.Count && (entities[i] is Line || entities[i] is Arc) && entities[i].Layer == layer)
|
||||
{
|
||||
if (entities[i] is Line) lineCount++;
|
||||
i++;
|
||||
}
|
||||
var runEnd = i - 1;
|
||||
|
||||
// Try to find arc candidates within this run
|
||||
FindCandidatesInRun(entities, runStart, runEnd, candidates);
|
||||
// Only analyze runs that have enough line entities to simplify
|
||||
if (lineCount >= MinLines)
|
||||
FindCandidatesInRun(entities, runStart, runEnd, candidates);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
@@ -94,12 +99,20 @@ public class GeometrySimplifier
|
||||
|
||||
while (j <= runEnd - MinLines + 1)
|
||||
{
|
||||
// Start with MinLines lines
|
||||
// Need at least MinLines entities ahead
|
||||
var k = j + MinLines - 1;
|
||||
var points = CollectPoints(entities, j, k);
|
||||
var (center, radius) = FitCircle(points);
|
||||
if (k > runEnd) break;
|
||||
|
||||
if (!center.IsValid() || MaxDeviation(points, center, radius) > Tolerance)
|
||||
var points = CollectPoints(entities, j, k);
|
||||
if (points.Count < 3)
|
||||
{
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var (center, radius, dev) = FitMirrorAxis(points);
|
||||
|
||||
if (!center.IsValid() || dev > Tolerance)
|
||||
{
|
||||
j++;
|
||||
continue;
|
||||
@@ -108,34 +121,33 @@ public class GeometrySimplifier
|
||||
// Extend as far as possible
|
||||
var prevCenter = center;
|
||||
var prevRadius = radius;
|
||||
var prevMaxDev = MaxDeviation(points, center, radius);
|
||||
var prevDev = dev;
|
||||
|
||||
while (k + 1 <= runEnd)
|
||||
{
|
||||
k++;
|
||||
points = CollectPoints(entities, j, k);
|
||||
var (newCenter, newRadius) = FitCircle(points);
|
||||
if (!newCenter.IsValid())
|
||||
if (points.Count < 3)
|
||||
{
|
||||
k--;
|
||||
break;
|
||||
}
|
||||
|
||||
var newMaxDev = MaxDeviation(points, newCenter, newRadius);
|
||||
if (newMaxDev > Tolerance)
|
||||
var (nc, nr, nd) = FitMirrorAxis(points);
|
||||
|
||||
if (!nc.IsValid() || nd > Tolerance)
|
||||
{
|
||||
k--;
|
||||
break;
|
||||
}
|
||||
|
||||
prevCenter = newCenter;
|
||||
prevRadius = newRadius;
|
||||
prevMaxDev = newMaxDev;
|
||||
prevCenter = nc;
|
||||
prevRadius = nr;
|
||||
prevDev = nd;
|
||||
}
|
||||
|
||||
// Build the candidate
|
||||
var finalPoints = CollectPoints(entities, j, k);
|
||||
var arc = BuildArc(prevCenter, prevRadius, finalPoints, entities[j]);
|
||||
var arc = CreateArc(prevCenter, prevRadius, finalPoints, entities[j]);
|
||||
var bbox = ComputeBoundingBox(finalPoints);
|
||||
|
||||
candidates.Add(new ArcCandidate
|
||||
@@ -143,7 +155,7 @@ public class GeometrySimplifier
|
||||
StartIndex = j,
|
||||
EndIndex = k,
|
||||
FittedArc = arc,
|
||||
MaxDeviation = prevMaxDev,
|
||||
MaxDeviation = prevDev,
|
||||
BoundingBox = bbox,
|
||||
});
|
||||
|
||||
@@ -151,28 +163,142 @@ public class GeometrySimplifier
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Vector> CollectPoints(List<Entity> entities, int start, int end)
|
||||
/// <summary>
|
||||
/// Fits a circular arc through a set of points using the mirror axis approach.
|
||||
/// The center is constrained to lie on the perpendicular bisector of the chord
|
||||
/// (P1→Pn), guaranteeing the arc passes exactly through both endpoints.
|
||||
/// Golden section search finds the optimal position along this axis.
|
||||
/// </summary>
|
||||
private (Vector center, double radius, double deviation) FitMirrorAxis(List<Vector> points)
|
||||
{
|
||||
var points = new List<Vector>();
|
||||
points.Add(((Line)entities[start]).StartPoint);
|
||||
for (var i = start; i <= end; i++)
|
||||
points.Add(((Line)entities[i]).EndPoint);
|
||||
return points;
|
||||
if (points.Count < 3)
|
||||
return (Vector.Invalid, 0, double.MaxValue);
|
||||
|
||||
var p1 = points[0];
|
||||
var pn = points[^1];
|
||||
|
||||
// Chord midpoint and length
|
||||
var mx = (p1.X + pn.X) / 2;
|
||||
var my = (p1.Y + pn.Y) / 2;
|
||||
var dx = pn.X - p1.X;
|
||||
var dy = pn.Y - p1.Y;
|
||||
var chordLen = System.Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (chordLen < 1e-10)
|
||||
return (Vector.Invalid, 0, double.MaxValue);
|
||||
|
||||
var halfChord = chordLen / 2;
|
||||
|
||||
// Unit normal (mirror axis direction, perpendicular to chord)
|
||||
var nx = -dy / chordLen;
|
||||
var ny = dx / chordLen;
|
||||
|
||||
// Find max signed projection onto the normal (sagitta with sign)
|
||||
var maxSagitta = 0.0;
|
||||
for (var i = 1; i < points.Count - 1; i++)
|
||||
{
|
||||
var proj = (points[i].X - mx) * nx + (points[i].Y - my) * ny;
|
||||
if (System.Math.Abs(proj) > System.Math.Abs(maxSagitta))
|
||||
maxSagitta = proj;
|
||||
}
|
||||
|
||||
if (System.Math.Abs(maxSagitta) < 1e-10)
|
||||
return (Vector.Invalid, 0, double.MaxValue); // collinear
|
||||
|
||||
// Initial d estimate from sagitta geometry:
|
||||
// Center at M + d*N, radius R = sqrt(halfChord² + d²)
|
||||
// For a point on the arc at perpendicular distance s from chord:
|
||||
// (d - s)² = halfChord² + d² → d = (s² - halfChord²) / (2s)
|
||||
var dInit = (maxSagitta * maxSagitta - halfChord * halfChord) / (2 * maxSagitta);
|
||||
|
||||
// Golden section search around initial estimate
|
||||
var range = System.Math.Max(System.Math.Abs(dInit) * 2, halfChord);
|
||||
var dLow = dInit - range;
|
||||
var dHigh = dInit + range;
|
||||
|
||||
var phi = (System.Math.Sqrt(5) - 1) / 2;
|
||||
for (var iter = 0; iter < 50; iter++)
|
||||
{
|
||||
var d1 = dHigh - phi * (dHigh - dLow);
|
||||
var d2 = dLow + phi * (dHigh - dLow);
|
||||
|
||||
var dev1 = EvalDeviation(points, mx, my, nx, ny, halfChord, d1);
|
||||
var dev2 = EvalDeviation(points, mx, my, nx, ny, halfChord, d2);
|
||||
|
||||
if (dev1 < dev2)
|
||||
dHigh = d2;
|
||||
else
|
||||
dLow = d1;
|
||||
|
||||
if (dHigh - dLow < 1e-12)
|
||||
break;
|
||||
}
|
||||
|
||||
var dOpt = (dLow + dHigh) / 2;
|
||||
var center = new Vector(mx + dOpt * nx, my + dOpt * ny);
|
||||
var radius = System.Math.Sqrt(halfChord * halfChord + dOpt * dOpt);
|
||||
var deviation = EvalDeviation(points, mx, my, nx, ny, halfChord, dOpt);
|
||||
|
||||
return (center, radius, deviation);
|
||||
}
|
||||
|
||||
private static double MaxDeviation(List<Vector> points, Vector center, double radius)
|
||||
/// <summary>
|
||||
/// Evaluates the max deviation of intermediate points from the circle
|
||||
/// defined by center = M + d*N, radius = sqrt(halfChord² + d²).
|
||||
/// Endpoints are excluded since they're on the circle by construction.
|
||||
/// </summary>
|
||||
private static double EvalDeviation(List<Vector> points,
|
||||
double mx, double my, double nx, double ny, double halfChord, double d)
|
||||
{
|
||||
var cx = mx + d * nx;
|
||||
var cy = my + d * ny;
|
||||
var r = System.Math.Sqrt(halfChord * halfChord + d * d);
|
||||
|
||||
var maxDev = 0.0;
|
||||
for (var i = 0; i < points.Count; i++)
|
||||
for (var i = 1; i < points.Count - 1; i++)
|
||||
{
|
||||
var dev = System.Math.Abs(points[i].DistanceTo(center) - radius);
|
||||
var px = points[i].X - cx;
|
||||
var py = points[i].Y - cy;
|
||||
var dist = System.Math.Sqrt(px * px + py * py);
|
||||
var dev = System.Math.Abs(dist - r);
|
||||
if (dev > maxDev)
|
||||
maxDev = dev;
|
||||
}
|
||||
return maxDev;
|
||||
}
|
||||
|
||||
private static Arc BuildArc(Vector center, double radius, List<Vector> points, Entity sourceEntity)
|
||||
private static List<Vector> CollectPoints(List<Entity> entities, int start, int end)
|
||||
{
|
||||
var points = new List<Vector>();
|
||||
|
||||
for (var i = start; i <= end; i++)
|
||||
{
|
||||
switch (entities[i])
|
||||
{
|
||||
case Line line:
|
||||
if (i == start)
|
||||
points.Add(line.StartPoint);
|
||||
points.Add(line.EndPoint);
|
||||
break;
|
||||
|
||||
case Arc arc:
|
||||
if (i == start)
|
||||
points.Add(arc.StartPoint());
|
||||
// Sample intermediate points so deviation is measured
|
||||
// accurately across the full arc span
|
||||
var segments = System.Math.Max(2, arc.SegmentsForTolerance(0.1));
|
||||
var arcPoints = arc.ToPoints(segments);
|
||||
// Skip first (already added or connects to previous) and add the rest
|
||||
for (var j = 1; j < arcPoints.Count; j++)
|
||||
points.Add(arcPoints[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static Arc CreateArc(Vector center, double radius, List<Vector> points, Entity sourceEntity)
|
||||
{
|
||||
var firstPoint = points[0];
|
||||
var lastPoint = points[^1];
|
||||
@@ -246,9 +372,6 @@ public class GeometrySimplifier
|
||||
sumZ += z;
|
||||
}
|
||||
|
||||
// Solve: [sumX2 sumXY sumX] [A] [sumXZ]
|
||||
// [sumXY sumY2 sumY] [B] = [sumYZ]
|
||||
// [sumX sumY n ] [C] [sumZ ]
|
||||
var det = sumX2 * (sumY2 * n - sumY * sumY)
|
||||
- sumXY * (sumXY * n - sumY * sumX)
|
||||
+ sumX * (sumXY * sumY - sumY2 * sumX);
|
||||
|
||||
@@ -63,9 +63,72 @@ namespace OpenNest.IO.Bending
|
||||
bends.Add(bend);
|
||||
}
|
||||
|
||||
PropagateCollinearBendNotes(bends);
|
||||
|
||||
return bends;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For bends without a note (e.g. split by a cutout), copy angle/radius/direction
|
||||
/// from a collinear bend that does have a note.
|
||||
/// </summary>
|
||||
private static void PropagateCollinearBendNotes(List<Bend> bends)
|
||||
{
|
||||
const double angleTolerance = 0.01; // radians
|
||||
const double distanceTolerance = 0.01;
|
||||
|
||||
foreach (var bend in bends)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(bend.NoteText))
|
||||
continue;
|
||||
|
||||
foreach (var other in bends)
|
||||
{
|
||||
if (string.IsNullOrEmpty(other.NoteText))
|
||||
continue;
|
||||
|
||||
if (!AreCollinear(bend, other, angleTolerance, distanceTolerance))
|
||||
continue;
|
||||
|
||||
bend.Direction = other.Direction;
|
||||
bend.Angle = other.Angle;
|
||||
bend.Radius = other.Radius;
|
||||
bend.NoteText = other.NoteText;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AreCollinear(Bend a, Bend b, double angleTolerance, double distanceTolerance)
|
||||
{
|
||||
var angleA = a.StartPoint.AngleTo(a.EndPoint);
|
||||
var angleB = b.StartPoint.AngleTo(b.EndPoint);
|
||||
|
||||
// Normalize angle difference to [0, PI) since opposite directions are still collinear
|
||||
var diff = System.Math.Abs(angleA - angleB) % System.Math.PI;
|
||||
if (diff > angleTolerance && System.Math.PI - diff > angleTolerance)
|
||||
return false;
|
||||
|
||||
// Perpendicular distance from midpoint of A to the infinite line through B
|
||||
var midA = new Vector(
|
||||
(a.StartPoint.X + a.EndPoint.X) / 2.0,
|
||||
(a.StartPoint.Y + a.EndPoint.Y) / 2.0);
|
||||
|
||||
var dx = b.EndPoint.X - b.StartPoint.X;
|
||||
var dy = b.EndPoint.Y - b.StartPoint.Y;
|
||||
var len = System.Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (len < 1e-9)
|
||||
return false;
|
||||
|
||||
// 2D cross product gives signed perpendicular distance * length
|
||||
var vx = midA.X - b.StartPoint.X;
|
||||
var vy = midA.Y - b.StartPoint.Y;
|
||||
var perp = System.Math.Abs(vx * dy - vy * dx) / len;
|
||||
|
||||
return perp <= distanceTolerance;
|
||||
}
|
||||
|
||||
private List<ACadSharp.Entities.Line> FindBendLines(CadDocument document)
|
||||
{
|
||||
return document.Entities
|
||||
|
||||
@@ -221,8 +221,9 @@ namespace OpenNest.IO
|
||||
});
|
||||
}
|
||||
|
||||
// Close the ellipse if it's a full ellipse
|
||||
if (lines.Count >= 2)
|
||||
// Close only if it's a full ellipse (sweep ≈ 2π)
|
||||
var sweep = endParam - startParam;
|
||||
if (lines.Count >= 2 && System.Math.Abs(sweep - System.Math.PI * 2.0) < 0.01)
|
||||
{
|
||||
var first = lines.First();
|
||||
var last = lines.Last();
|
||||
|
||||
@@ -29,6 +29,84 @@ public class SolidWorksBendDetectorTests
|
||||
Assert.Empty(bends);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Simplifier_EllipseSegments_FewLargeArcs()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11 Test.dxf");
|
||||
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
|
||||
|
||||
var importer = new OpenNest.IO.DxfImporter { SplinePrecision = 200 };
|
||||
var result = importer.Import(path);
|
||||
|
||||
var shape = new OpenNest.Geometry.Shape();
|
||||
shape.Entities.AddRange(result.Entities);
|
||||
|
||||
// Default tolerance is 0.5 — should produce very few large arcs
|
||||
var simplifier = new OpenNest.Geometry.GeometrySimplifier();
|
||||
var candidates = simplifier.Analyze(shape);
|
||||
|
||||
// With 0.5 tolerance, 2 ellipses (~400 segments) should reduce to a handful of arcs
|
||||
// Dump for visibility then assert
|
||||
var info = string.Join(", ", candidates.Select(c => $"[{c.StartIndex}..{c.EndIndex}]={c.LineCount}lines R={c.FittedArc.Radius:F3}"));
|
||||
Assert.True(candidates.Count <= 10,
|
||||
$"Expected <=10 arcs but got {candidates.Count}: {info}");
|
||||
|
||||
// Each arc should cover many lines
|
||||
foreach (var c in candidates)
|
||||
Assert.True(c.LineCount >= 3, $"Arc [{c.StartIndex}..{c.EndIndex}] only covers {c.LineCount} lines");
|
||||
|
||||
// Arcs should connect to the original geometry within tolerance
|
||||
foreach (var c in candidates)
|
||||
{
|
||||
var firstLine = (OpenNest.Geometry.Line)shape.Entities[c.StartIndex];
|
||||
var lastLine = (OpenNest.Geometry.Line)shape.Entities[c.EndIndex];
|
||||
var arc = c.FittedArc;
|
||||
|
||||
var startGap = firstLine.StartPoint.DistanceTo(arc.StartPoint());
|
||||
var endGap = lastLine.EndPoint.DistanceTo(arc.EndPoint());
|
||||
|
||||
Assert.True(startGap < 1e-9, $"Start gap {startGap} at candidate [{c.StartIndex}..{c.EndIndex}]");
|
||||
Assert.True(endGap < 1e-9, $"End gap {endGap} at candidate [{c.StartIndex}..{c.EndIndex}]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_TrimmedEllipse_NoClosingChord()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11.dxf");
|
||||
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
|
||||
|
||||
var importer = new OpenNest.IO.DxfImporter();
|
||||
var result = importer.Import(path);
|
||||
|
||||
// The DXF has 2 trimmed ellipses forming an oblong slot.
|
||||
// Trimmed ellipses must not generate a closing chord line.
|
||||
// 83 = 72 lines + 4 arcs + 7 circles + ellipse segments (heavily merged by optimizer)
|
||||
Assert.Equal(83, result.Entities.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectBends_SplitBendLine_PropagatesNote()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT23.dxf");
|
||||
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
|
||||
|
||||
using var reader = new DxfReader(path);
|
||||
var doc = reader.Read();
|
||||
|
||||
var detector = new SolidWorksBendDetector();
|
||||
var bends = detector.DetectBends(doc);
|
||||
|
||||
Assert.Equal(5, bends.Count);
|
||||
Assert.All(bends, b =>
|
||||
{
|
||||
Assert.NotNull(b.NoteText);
|
||||
Assert.Equal(BendDirection.Up, b.Direction);
|
||||
Assert.Equal(90.0, b.Angle);
|
||||
Assert.Equal(0.125, b.Radius);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectBends_RealDxf_ParsesNotesCorrectly()
|
||||
{
|
||||
|
||||
2866
OpenNest.Tests/Bending/TestData/4526 A14 PT11 Test.dxf
Normal file
2866
OpenNest.Tests/Bending/TestData/4526 A14 PT11 Test.dxf
Normal file
File diff suppressed because it is too large
Load Diff
6662
OpenNest.Tests/Bending/TestData/4526 A14 PT11.dxf
Normal file
6662
OpenNest.Tests/Bending/TestData/4526 A14 PT11.dxf
Normal file
File diff suppressed because it is too large
Load Diff
4370
OpenNest.Tests/Bending/TestData/4526 A14 PT23.dxf
Normal file
4370
OpenNest.Tests/Bending/TestData/4526 A14 PT23.dxf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -77,9 +77,10 @@ public class GeometrySimplifierTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Analyze_MixedEntitiesWithArc_OnlyAnalyzesLines()
|
||||
public void Analyze_MixedEntitiesWithArc_FindsSeparateCandidates()
|
||||
{
|
||||
// Line, Line, Line, Arc, Line, Line, Line — should find candidates only in line runs
|
||||
// Lines on one curve, then an arc at a different center, then lines on another curve
|
||||
// The arc is included in the run but can't merge with lines on different curves
|
||||
var shape = new Shape();
|
||||
// First run: 5 lines on a curve
|
||||
var arc1 = new Arc(new Vector(0, 0), 10, 0, System.Math.PI / 2, false);
|
||||
|
||||
@@ -25,6 +25,9 @@ namespace OpenNest.Controls
|
||||
}
|
||||
|
||||
public Arc SimplifierPreview { get; set; }
|
||||
public List<Entity> SimplifierToleranceLeft { get; set; }
|
||||
public List<Entity> SimplifierToleranceRight { get; set; }
|
||||
public List<Entity> OriginalEntities { get; set; }
|
||||
|
||||
private readonly Pen gridPen = new Pen(Color.FromArgb(70, 70, 70));
|
||||
private readonly Dictionary<int, Pen> penCache = new Dictionary<int, Pen>();
|
||||
@@ -79,6 +82,17 @@ namespace OpenNest.Controls
|
||||
|
||||
e.Graphics.TranslateTransform(origin.X, origin.Y);
|
||||
|
||||
// Draw original geometry overlay (faded, behind current)
|
||||
if (OriginalEntities != null)
|
||||
{
|
||||
using var origPen = new Pen(Color.FromArgb(50, 255, 140, 40));
|
||||
foreach (var entity in OriginalEntities)
|
||||
{
|
||||
if (!IsEtchLayer(entity.Layer))
|
||||
DrawEntity(e.Graphics, entity, origPen);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entity in Entities)
|
||||
{
|
||||
if (IsEtchLayer(entity.Layer)) continue;
|
||||
@@ -102,6 +116,25 @@ namespace OpenNest.Controls
|
||||
|
||||
if (SimplifierPreview != null)
|
||||
{
|
||||
// Draw tolerance zone (offset lines each side of original geometry)
|
||||
if (SimplifierToleranceLeft != null)
|
||||
{
|
||||
using var zonePen = new Pen(Color.FromArgb(40, 100, 200, 100));
|
||||
foreach (var entity in SimplifierToleranceLeft)
|
||||
DrawEntity(e.Graphics, entity, zonePen);
|
||||
foreach (var entity in SimplifierToleranceRight)
|
||||
DrawEntity(e.Graphics, entity, zonePen);
|
||||
}
|
||||
|
||||
// Draw old geometry (highlighted lines) in orange dashed
|
||||
if (simplifierHighlightSet != null)
|
||||
{
|
||||
using var oldPen = new Pen(Color.FromArgb(180, 255, 160, 50), 1f / ViewScale) { DashPattern = new float[] { 6, 3 } };
|
||||
foreach (var entity in simplifierHighlightSet)
|
||||
DrawEntity(e.Graphics, entity, oldPen);
|
||||
}
|
||||
|
||||
// Draw the new arc in bright green
|
||||
using var previewPen = new Pen(Color.FromArgb(0, 200, 80), 2f / ViewScale);
|
||||
DrawArc(e.Graphics, SimplifierPreview, previewPen);
|
||||
}
|
||||
@@ -260,20 +293,26 @@ namespace OpenNest.Controls
|
||||
{
|
||||
DashPattern = new float[] { 6, 4 }
|
||||
};
|
||||
using var noteFont = new Font("Segoe UI", 9f);
|
||||
using var noteBrush = new SolidBrush(Color.FromArgb(220, 255, 255, 200));
|
||||
using var selectedNoteBrush = new SolidBrush(Color.FromArgb(220, 255, 180, 100));
|
||||
|
||||
for (var i = 0; i < Bends.Count; i++)
|
||||
{
|
||||
var bend = Bends[i];
|
||||
var pt1 = PointWorldToGraph(bend.StartPoint);
|
||||
var pt2 = PointWorldToGraph(bend.EndPoint);
|
||||
var isSelected = i == SelectedBendIndex;
|
||||
|
||||
if (i == SelectedBendIndex)
|
||||
{
|
||||
if (isSelected)
|
||||
g.DrawLine(glowPen, pt1, pt2);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.DrawLine(bendPen, pt1, pt2);
|
||||
|
||||
if (!string.IsNullOrEmpty(bend.NoteText))
|
||||
{
|
||||
var mid = new PointF((pt1.X + pt2.X) / 2f, (pt1.Y + pt2.Y) / 2f);
|
||||
g.DrawString(bend.NoteText, noteFont, isSelected ? selectedNoteBrush : noteBrush, mid.X + 4, mid.Y + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,6 +463,8 @@ namespace OpenNest.Controls
|
||||
{
|
||||
SimplifierHighlight = null;
|
||||
SimplifierPreview = null;
|
||||
SimplifierToleranceLeft = null;
|
||||
SimplifierToleranceRight = null;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace OpenNest.Controls
|
||||
public int Quantity { get; set; } = 1;
|
||||
public string Path { get; set; }
|
||||
public List<Entity> Entities { get; set; } = new();
|
||||
public List<Entity> OriginalEntities { get; set; }
|
||||
public List<Bend> Bends { get; set; } = new();
|
||||
public Box Bounds { get; set; }
|
||||
public int EntityCount { get; set; }
|
||||
|
||||
11
OpenNest/Forms/CadConverterForm.Designer.cs
generated
11
OpenNest/Forms/CadConverterForm.Designer.cs
generated
@@ -29,6 +29,7 @@ namespace OpenNest.Forms
|
||||
lblEntityCount = new System.Windows.Forms.Label();
|
||||
btnSplit = new System.Windows.Forms.Button();
|
||||
btnSimplify = new System.Windows.Forms.Button();
|
||||
chkShowOriginal = new System.Windows.Forms.CheckBox();
|
||||
lblDetect = new System.Windows.Forms.Label();
|
||||
cboBendDetector = new System.Windows.Forms.ComboBox();
|
||||
bottomPanel1 = new OpenNest.Controls.BottomPanel();
|
||||
@@ -129,6 +130,7 @@ namespace OpenNest.Forms
|
||||
detailBar.Controls.Add(lblEntityCount);
|
||||
detailBar.Controls.Add(btnSplit);
|
||||
detailBar.Controls.Add(btnSimplify);
|
||||
detailBar.Controls.Add(chkShowOriginal);
|
||||
detailBar.Controls.Add(lblDetect);
|
||||
detailBar.Controls.Add(cboBendDetector);
|
||||
detailBar.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||
@@ -225,6 +227,14 @@ namespace OpenNest.Forms
|
||||
btnSimplify.Margin = new System.Windows.Forms.Padding(4, 0, 0, 0);
|
||||
btnSimplify.Click += new System.EventHandler(this.OnSimplifyClick);
|
||||
//
|
||||
// chkShowOriginal
|
||||
//
|
||||
chkShowOriginal.AutoSize = true;
|
||||
chkShowOriginal.Font = new System.Drawing.Font("Segoe UI", 9F);
|
||||
chkShowOriginal.Text = "Original";
|
||||
chkShowOriginal.Margin = new System.Windows.Forms.Padding(6, 3, 0, 0);
|
||||
chkShowOriginal.CheckedChanged += new System.EventHandler(this.OnShowOriginalChanged);
|
||||
//
|
||||
// lblDetect
|
||||
//
|
||||
lblDetect.AutoSize = true;
|
||||
@@ -324,6 +334,7 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.TextBox txtCustomer;
|
||||
private System.Windows.Forms.Button btnSplit;
|
||||
private System.Windows.Forms.Button btnSimplify;
|
||||
private System.Windows.Forms.CheckBox chkShowOriginal;
|
||||
private System.Windows.Forms.ComboBox cboBendDetector;
|
||||
private System.Windows.Forms.Label lblQty;
|
||||
private System.Windows.Forms.Label lblCust;
|
||||
|
||||
@@ -141,6 +141,7 @@ namespace OpenNest.Forms
|
||||
entityView1.IsPickingBendLine = false;
|
||||
filterPanel.SetPickMode(false);
|
||||
}
|
||||
entityView1.OriginalEntities = chkShowOriginal.Checked ? item.OriginalEntities : null;
|
||||
entityView1.Entities.Clear();
|
||||
entityView1.Entities.AddRange(item.Entities);
|
||||
entityView1.Bends = item.Bends ?? new List<Bend>();
|
||||
@@ -384,7 +385,13 @@ namespace OpenNest.Forms
|
||||
if (entityView1.Entities == null || entityView1.Entities.Count == 0)
|
||||
return;
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(entityView1.Entities);
|
||||
// Always simplify from original geometry to prevent tolerance creep
|
||||
var item = CurrentItem;
|
||||
if (item != null && item.OriginalEntities == null)
|
||||
item.OriginalEntities = new List<Entity>(item.Entities);
|
||||
|
||||
var sourceEntities = item?.OriginalEntities ?? entityView1.Entities;
|
||||
var shapes = ShapeBuilder.GetShapes(sourceEntities);
|
||||
if (shapes.Count == 0)
|
||||
return;
|
||||
|
||||
@@ -422,6 +429,13 @@ namespace OpenNest.Forms
|
||||
lblEntityCount.Text = $"{entities.Count} entities";
|
||||
}
|
||||
|
||||
private void OnShowOriginalChanged(object sender, EventArgs e)
|
||||
{
|
||||
var item = CurrentItem;
|
||||
entityView1.OriginalEntities = chkShowOriginal.Checked ? item?.OriginalEntities : null;
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
|
||||
@@ -54,10 +54,10 @@ public class SimplifierViewerForm : Form
|
||||
numTolerance = new System.Windows.Forms.NumericUpDown
|
||||
{
|
||||
Minimum = 0.001m,
|
||||
Maximum = 1.000m,
|
||||
Maximum = 5.000m,
|
||||
DecimalPlaces = 3,
|
||||
Increment = 0.001m,
|
||||
Value = 0.005m,
|
||||
Increment = 0.05m,
|
||||
Value = 0.500m,
|
||||
Width = 70,
|
||||
};
|
||||
numTolerance.ValueChanged += OnToleranceChanged;
|
||||
@@ -100,7 +100,7 @@ public class SimplifierViewerForm : Form
|
||||
Controls.Add(bottomPanel);
|
||||
}
|
||||
|
||||
public void LoadShapes(List<Shape> shapes, EntityView view, double tolerance = 0.005)
|
||||
public void LoadShapes(List<Shape> shapes, EntityView view, double tolerance = 0.5)
|
||||
{
|
||||
this.shapes = shapes;
|
||||
this.entityView = view;
|
||||
@@ -167,7 +167,28 @@ public class SimplifierViewerForm : Form
|
||||
|
||||
entityView.SimplifierHighlight = highlightEntities;
|
||||
entityView.SimplifierPreview = candidate.FittedArc;
|
||||
entityView.ZoomToArea(candidate.BoundingBox);
|
||||
|
||||
// Build tolerance zone by offsetting each original line both directions
|
||||
var tol = simplifier.Tolerance;
|
||||
var leftEntities = new List<Entity>();
|
||||
var rightEntities = new List<Entity>();
|
||||
foreach (var entity in highlightEntities)
|
||||
{
|
||||
var left = entity.OffsetEntity(tol, OffsetSide.Left);
|
||||
var right = entity.OffsetEntity(tol, OffsetSide.Right);
|
||||
if (left != null) leftEntities.Add(left);
|
||||
if (right != null) rightEntities.Add(right);
|
||||
}
|
||||
entityView.SimplifierToleranceLeft = leftEntities;
|
||||
entityView.SimplifierToleranceRight = rightEntities;
|
||||
|
||||
// Zoom with padding for the tolerance zone
|
||||
var padded = new Box(
|
||||
candidate.BoundingBox.X - tol * 2,
|
||||
candidate.BoundingBox.Y - tol * 2,
|
||||
candidate.BoundingBox.Width + tol * 4,
|
||||
candidate.BoundingBox.Length + tol * 4);
|
||||
entityView.ZoomToArea(padded);
|
||||
}
|
||||
|
||||
private void OnItemChecked(object sender, ItemCheckedEventArgs e)
|
||||
|
||||
135
README.md
135
README.md
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
A Windows desktop application for CNC nesting — imports DXF drawings, arranges parts on material plates, and exports layouts as DXF or G-code for cutting.
|
||||
|
||||

|
||||
|
||||
@@ -8,15 +8,21 @@ OpenNest takes your part drawings, lets you define your sheet (plate) sizes, and
|
||||
|
||||
## Features
|
||||
|
||||
- **DXF Import/Export** — Load part drawings from DXF files and export completed nest layouts
|
||||
- **Multiple Fill Strategies** — Grid-based linear fill 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
|
||||
- **DXF/DWG Import & Export** — Load part drawings from DXF or DWG files and export completed nest layouts as DXF
|
||||
- **Multiple Fill Strategies** — Grid-based linear fill, interlocking pair fill, rectangle bin packing, extents-based tiling, and more via a pluggable strategy system
|
||||
- **Best-Fit Pair Nesting** — NFP-based (No Fit Polygon) pair evaluation finds tight-fitting interlocking orientations between parts
|
||||
- **GPU Acceleration** — Optional ILGPU-based bitmap overlap detection for faster best-fit evaluation
|
||||
- **Part Rotation** — Automatically tries different rotation angles to find better fits, with optional ML-based angle prediction (ONNX)
|
||||
- **Gravity Compaction** — After placing parts, pushes them together using polygon-based directional distance to close gaps between irregular shapes
|
||||
- **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** — Cutting parameters like approach paths and holding tabs (engine support, UI coming soon)
|
||||
- **Sheet Cut-Offs** — Automatically cut the plate to size after nesting, with geometry-aware clearance that avoids placed parts
|
||||
- **Drawing Splitting** — Split oversized parts into pieces that fit your plate, with straight cuts, weld-gap tabs, or interlocking spike-groove joints
|
||||
- **Bend Line Detection** — Import bend lines from DXF files with pluggable detectors (SolidWorks flat pattern support built in)
|
||||
- **Lead-In/Lead-Out & Tabs** — Configurable approach paths, exit paths, and holding tabs for CNC cutting
|
||||
- **G-code Output** — Post-process nested layouts to G-code via plugin post-processors
|
||||
- **Built-in Shapes** — 12 parametric shapes (circles, rectangles, L-shapes, T-shapes, flanges, etc.) for quick testing or simple parts
|
||||
- **Interactive Editing** — Zoom, pan, select, clone, push, and manually arrange parts on the plate view
|
||||
- **Pluggable Engine Architecture** — Swap between built-in nesting engines or load custom engines from plugin DLLs
|
||||
|
||||

|
||||
|
||||
@@ -46,12 +52,11 @@ 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 -->
|
||||
2. **Add drawings** — Import DXF files via the CAD Converter (handles bend detection, layer filtering, and color/linetype exclusion) or create built-in shapes
|
||||
3. **Set up a plate** — Define the plate size, material, quadrant, and spacing
|
||||
4. **Fill the plate** — The nesting engine arranges parts automatically using the active fill strategy
|
||||
5. **Add cut-offs** — Optionally add horizontal/vertical cut-off lines to trim unused plate material
|
||||
6. **Export** — Save as a `.nest` file, export to DXF, or post-process to G-code
|
||||
|
||||
## Command-Line Interface
|
||||
|
||||
@@ -95,6 +100,7 @@ dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- project.zip ext
|
||||
| `--keep-parts` | Keep existing parts instead of clearing before fill |
|
||||
| `--check-overlaps` | Run overlap detection after fill (exits with code 1 if found) |
|
||||
| `--engine <name>` | Select a registered nesting engine |
|
||||
| `--post <name>` | Post-process the result with the named post-processor plugin |
|
||||
| `--no-save` | Skip saving the output file |
|
||||
| `--no-log` | Skip writing the debug log |
|
||||
|
||||
@@ -102,26 +108,74 @@ dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- project.zip ext
|
||||
|
||||
```
|
||||
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
|
||||
├── OpenNest/ # WinForms desktop application (UI)
|
||||
├── OpenNest.Core/ # Domain model, geometry, and CNC primitives
|
||||
├── OpenNest.Engine/ # Nesting algorithms (fill, pack, compact, best-fit)
|
||||
├── OpenNest.IO/ # File I/O — DXF import/export, nest file format
|
||||
├── OpenNest.Console/ # Command-line interface for batch nesting
|
||||
├── OpenNest.Api/ # Programmatic nesting API (NestRunner pipeline)
|
||||
├── OpenNest.Gpu/ # GPU-accelerated pair evaluation (ILGPU)
|
||||
├── OpenNest.Training/ # ML training data collection (SQLite + EF Core)
|
||||
├── OpenNest.Mcp/ # MCP server for AI tool integration
|
||||
├── OpenNest.Posts.Cincinnati/ # Cincinnati CL-707 laser post-processor plugin
|
||||
└── OpenNest.Tests/ # Unit tests (xUnit)
|
||||
```
|
||||
|
||||
For most users, only these matter:
|
||||
|
||||
| Project | What it does |
|
||||
|---------|-------------|
|
||||
| **OpenNest** | The app you run. WinForms UI with plate viewer, drawing list, and dialogs. |
|
||||
| **OpenNest** | The app you run. WinForms MDI interface with plate viewer, drawing list, CAD converter, and dialogs. |
|
||||
| **OpenNest.Console** | Command-line interface for batch nesting, scripting, and automation. |
|
||||
| **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. |
|
||||
| **OpenNest.Core** | The building blocks — parts, plates, drawings, geometry, G-code representation, bend lines, cut-offs, and drawing splitting. |
|
||||
| **OpenNest.Engine** | The brains — fill strategies (linear, pairs, rect best-fit, extents), NFP-based pair evaluation, gravity compaction, and a pluggable engine registry. |
|
||||
| **OpenNest.IO** | Reads and writes files — DXF/DWG (via ACadSharp), G-code, and the `.nest` ZIP format. |
|
||||
| **OpenNest.Api** | High-level API for running the full nesting pipeline programmatically (import, nest, export). |
|
||||
| **OpenNest.Gpu** | GPU-accelerated bitmap overlap detection for best-fit pair evaluation using ILGPU. |
|
||||
| **OpenNest.Posts.Cincinnati** | Post-processor plugin for Cincinnati CL-707/800/900/940/CLX laser cutting machines. Outputs Cincinnati-format G-code with material library, kerf compensation, and pierce logic. |
|
||||
| **OpenNest.Mcp** | MCP (Model Context Protocol) server exposing nesting operations as tools for AI assistants. |
|
||||
| **OpenNest.Tests** | 75+ test files covering core geometry, fill strategies, splitting, bending, post-processing, and the API. |
|
||||
|
||||
## Nesting Engines
|
||||
|
||||
OpenNest uses a pluggable engine architecture. The active engine can be selected at runtime.
|
||||
|
||||
| Engine | Description |
|
||||
|--------|-------------|
|
||||
| **Default** | Multi-phase strategy: linear fill, pair fill, rect best-fit, then remainder. Balances density and speed. |
|
||||
| **Vertical Remnant** | Optimizes for a clean vertical drop on the right side of the plate. |
|
||||
| **Horizontal Remnant** | Optimizes for a clean horizontal drop on the top of the plate. |
|
||||
|
||||
Custom engines can be built by subclassing `NestEngineBase` and registering via `NestEngineRegistry` or dropping a plugin DLL in the `Engines/` directory.
|
||||
|
||||
### Fill Strategies
|
||||
|
||||
Each engine composes from a set of fill strategies:
|
||||
|
||||
| Strategy | Description |
|
||||
|----------|-------------|
|
||||
| **Linear** | Grid-based fill with geometry-aware copy distance and 4-config rotation/axis optimization |
|
||||
| **Pairs** | NFP-based interlocking pair evaluation — finds tight-fitting orientations between two parts |
|
||||
| **Rect Best-Fit** | Greedy rectangle bin-packing with horizontal and vertical orientation trials |
|
||||
| **Extents** | Extents-based pair tiling for simple rectangular arrangements |
|
||||
|
||||
## Drawing Splitting
|
||||
|
||||
Oversized parts that don't fit on a single plate can be split into smaller pieces:
|
||||
|
||||
- **Straight Split** — Clean cut with no joining features
|
||||
- **Weld-Gap Tabs** — Rectangular tab spacers on one side for weld alignment
|
||||
- **Spike-Groove** — Interlocking V-shaped spike and groove pairs for self-aligning joints
|
||||
|
||||
The split system supports fit-to-plate (auto-calculates split lines) and split-by-count modes, with an interactive UI for adjusting split positions and feature parameters.
|
||||
|
||||
## Post-Processors
|
||||
|
||||
Post-processors convert nested layouts into machine-specific G-code. They are loaded as plugin DLLs from the `Posts/` directory at runtime.
|
||||
|
||||
**Included:**
|
||||
|
||||
- **Cincinnati** — Full post-processor for Cincinnati CL-707/800/900/940/CLX laser cutting machines with variable declarations, material library resolution, speed classification, kerf compensation, and optional part sub-programs (M98).
|
||||
|
||||
Custom post-processors implement the `IPostProcessor` interface and are auto-discovered from DLLs in the `Posts/` directory.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
@@ -131,6 +185,7 @@ For most users, only these matter:
|
||||
| `F` | Zoom to fit the plate view |
|
||||
| `Shift` + Mouse Wheel | Rotate parts when a drawing is selected |
|
||||
| `Shift` + Left Click | Push the selected group of parts to the bottom-left most point |
|
||||
| Middle Mouse Click | Rotate selected parts 90 degrees |
|
||||
| `X` | Push selected parts left (negative X) |
|
||||
| `Shift+X` | Push selected parts right (positive X) |
|
||||
| `Y` | Push selected parts down (negative Y) |
|
||||
@@ -145,18 +200,26 @@ For most users, only these matter:
|
||||
| DXF (AutoCAD Drawing Exchange) | Yes | Yes |
|
||||
| DWG (AutoCAD Drawing) | Yes | No |
|
||||
| G-code | No | Yes (via post-processors) |
|
||||
| `.nest` (ZIP-based project format) | Yes | Yes |
|
||||
|
||||
## Nest File Format
|
||||
|
||||
Nest files (`.nest`) are ZIP archives containing:
|
||||
|
||||
- `nest.json` — JSON metadata: nest info, plate defaults, drawings (with bend data), and plates (with parts and cut-offs)
|
||||
- `programs/program-N` — G-code text for each drawing's cut program
|
||||
- `bestfits/bestfit-N` — Cached best-fit pair evaluation results (optional)
|
||||
|
||||
## Roadmap
|
||||
|
||||
- **NFP-based nesting** — No Fit Polygon algorithms and simulated annealing optimizer exist in the engine but aren't integrated into the UI or engine registry yet
|
||||
- **Lead-in/Lead-out UI** — Engine support for lead-ins, lead-outs, and tabs is implemented; needs a UI for configuration
|
||||
- **Sheet cut-offs** — Cut the sheet to size after nesting to reduce waste
|
||||
- **Post-processors** — Plugin interface (`IPostProcessor`) is in place; need to ship built-in post-processors for common CNC controllers
|
||||
- **Shape library UI** — Built-in shape generation code exists; needs a browsable library UI for quick access
|
||||
- **NFP-based auto-nesting** — Simulated annealing optimizer and NFP placement exist in the engine but aren't exposed as a selectable engine yet
|
||||
- **Geometry simplifier** — Replace consecutive small line segments with fitted arcs to reduce program size and improve nesting performance
|
||||
- **Shape library UI** — 12 built-in parametric shapes exist in code; needs a browsable library UI for quick access
|
||||
- **Additional post-processors** — Plugin interface is in place; more machine-specific post-processors planned
|
||||
|
||||
## 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.
|
||||
OpenNest is under active development. The core nesting workflows function end-to-end — from DXF import through filling, splitting, cut-offs, and G-code post-processing. Contributions and feedback are welcome.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Reference in New Issue
Block a user