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

View File

@@ -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

View File

@@ -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();