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:
2026-03-26 20:27:46 -04:00
parent c6652f7707
commit 356b989424
14 changed files with 14400 additions and 85 deletions
+45 -4
View File
@@ -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();
}
+1
View File
@@ -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
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();
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;
+15 -1
View File
@@ -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
+26 -5
View File
@@ -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)