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:
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user