fix: geometry simplifier arc connectivity and ellipse support

Three bugs prevented the simplifier from working on ellipse geometry:

1. Sweep angle check blocked initial fit — the 5-degree minimum sweep
   was inside TryFit(), killing candidates before the extension loop
   could accumulate enough segments. Moved to TryFitArcAt() after
   extension.

2. Layer reference equality split runs — entities from separate DXF
   ellipses had different Layer object instances for the same layer "0",
   splitting them into independent runs. Changed to compare Layer.Name.

3. Symmetrize replaced arcs with mirrored copies whose endpoints didn't
   match the target's original geometry, creating ~0.014 gaps. Now only
   applies mirrored arcs when endpoints are within tolerance of the
   target's boundary points.

Also: default tolerance 0.02 -> 0.004, Export DXF button in
CadConverterForm for debugging simplified geometry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 13:49:27 -04:00
parent 356b989424
commit e27def388f
4 changed files with 583 additions and 295 deletions

View File

@@ -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();
btnExportDxf = new System.Windows.Forms.Button();
chkShowOriginal = new System.Windows.Forms.CheckBox();
lblDetect = new System.Windows.Forms.Label();
cboBendDetector = new System.Windows.Forms.ComboBox();
@@ -130,6 +131,7 @@ namespace OpenNest.Forms
detailBar.Controls.Add(lblEntityCount);
detailBar.Controls.Add(btnSplit);
detailBar.Controls.Add(btnSimplify);
detailBar.Controls.Add(btnExportDxf);
detailBar.Controls.Add(chkShowOriginal);
detailBar.Controls.Add(lblDetect);
detailBar.Controls.Add(cboBendDetector);
@@ -227,6 +229,15 @@ namespace OpenNest.Forms
btnSimplify.Margin = new System.Windows.Forms.Padding(4, 0, 0, 0);
btnSimplify.Click += new System.EventHandler(this.OnSimplifyClick);
//
// btnExportDxf
//
btnExportDxf.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
btnExportDxf.Font = new System.Drawing.Font("Segoe UI", 9F);
btnExportDxf.Text = "Export DXF";
btnExportDxf.AutoSize = true;
btnExportDxf.Margin = new System.Windows.Forms.Padding(4, 0, 0, 0);
btnExportDxf.Click += new System.EventHandler(this.OnExportDxfClick);
//
// chkShowOriginal
//
chkShowOriginal.AutoSize = true;
@@ -334,6 +345,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.Button btnExportDxf;
private System.Windows.Forms.CheckBox chkShowOriginal;
private System.Windows.Forms.ComboBox cboBendDetector;
private System.Windows.Forms.Label lblQty;

View File

@@ -436,6 +436,60 @@ namespace OpenNest.Forms
entityView1.Invalidate();
}
private void OnExportDxfClick(object sender, EventArgs e)
{
var item = CurrentItem;
if (item == null) return;
using var dlg = new SaveFileDialog
{
Filter = "DXF Files|*.dxf",
FileName = Path.ChangeExtension(item.Name, ".dxf"),
};
if (dlg.ShowDialog() != DialogResult.OK) return;
var doc = new ACadSharp.CadDocument();
foreach (var entity in item.Entities)
{
switch (entity)
{
case Geometry.Line line:
doc.Entities.Add(new ACadSharp.Entities.Line
{
StartPoint = new CSMath.XYZ(line.StartPoint.X, line.StartPoint.Y, 0),
EndPoint = new CSMath.XYZ(line.EndPoint.X, line.EndPoint.Y, 0),
});
break;
case Geometry.Arc arc:
var startAngle = arc.StartAngle;
var endAngle = arc.EndAngle;
if (arc.IsReversed)
OpenNest.Math.Generic.Swap(ref startAngle, ref endAngle);
doc.Entities.Add(new ACadSharp.Entities.Arc
{
Center = new CSMath.XYZ(arc.Center.X, arc.Center.Y, 0),
Radius = arc.Radius,
StartAngle = startAngle,
EndAngle = endAngle,
});
break;
case Geometry.Circle circle:
doc.Entities.Add(new ACadSharp.Entities.Circle
{
Center = new CSMath.XYZ(circle.Center.X, circle.Center.Y, 0),
Radius = circle.Radius,
});
break;
}
}
using var writer = new ACadSharp.IO.DxfWriter(dlg.FileName, doc, false);
writer.Write();
}
#endregion
#region Output

View File

@@ -7,12 +7,8 @@ using OpenNest.Geometry;
namespace OpenNest.Forms;
public class SimplifierViewerForm : Form
public partial class SimplifierViewerForm : Form
{
private ListView listView;
private System.Windows.Forms.NumericUpDown numTolerance;
private Label lblCount;
private Button btnApply;
private EntityView entityView;
private GeometrySimplifier simplifier;
private List<Shape> shapes;
@@ -22,85 +18,10 @@ public class SimplifierViewerForm : Form
public SimplifierViewerForm()
{
Text = "Geometry Simplifier";
FormBorderStyle = FormBorderStyle.SizableToolWindow;
ShowInTaskbar = false;
TopMost = true;
StartPosition = FormStartPosition.Manual;
Size = new System.Drawing.Size(420, 450);
Font = new Font("Segoe UI", 9f);
InitializeControls();
InitializeComponent();
}
private void InitializeControls()
{
// Bottom panel
var bottomPanel = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
Height = 36,
Padding = new Padding(4, 6, 4, 4),
WrapContents = false,
};
var lblTolerance = new Label
{
Text = "Tolerance:",
AutoSize = true,
Margin = new Padding(0, 3, 2, 0),
};
numTolerance = new System.Windows.Forms.NumericUpDown
{
Minimum = 0.001m,
Maximum = 5.000m,
DecimalPlaces = 3,
Increment = 0.05m,
Value = 0.500m,
Width = 70,
};
numTolerance.ValueChanged += OnToleranceChanged;
lblCount = new Label
{
Text = "0 of 0 selected",
AutoSize = true,
Margin = new Padding(8, 3, 4, 0),
};
btnApply = new Button
{
Text = "Apply",
FlatStyle = FlatStyle.Flat,
Width = 60,
Margin = new Padding(4, 0, 0, 0),
};
btnApply.Click += OnApplyClick;
bottomPanel.Controls.AddRange(new Control[] { lblTolerance, numTolerance, lblCount, btnApply });
// ListView
listView = new ListView
{
Dock = DockStyle.Fill,
View = View.Details,
FullRowSelect = true,
CheckBoxes = true,
GridLines = true,
};
listView.Columns.Add("Lines", 50);
listView.Columns.Add("Radius", 70);
listView.Columns.Add("Deviation", 75);
listView.Columns.Add("Location", 100);
listView.ItemSelectionChanged += OnItemSelected;
listView.ItemChecked += OnItemChecked;
Controls.Add(listView);
Controls.Add(bottomPanel);
}
public void LoadShapes(List<Shape> shapes, EntityView view, double tolerance = 0.5)
public void LoadShapes(List<Shape> shapes, EntityView view, double tolerance = 0.004)
{
this.shapes = shapes;
this.entityView = view;
@@ -119,6 +40,11 @@ public class SimplifierViewerForm : Form
var shapeCandidates = simplifier.Analyze(shapes[i]);
foreach (var c in shapeCandidates)
c.ShapeIndex = i;
var axis = GeometrySimplifier.DetectMirrorAxis(shapes[i]);
if (axis.IsValid)
simplifier.Symmetrize(shapeCandidates, axis);
candidates.AddRange(shapeCandidates);
}
RefreshList();