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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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