feat: wire up EllipseConverter and SplineConverter in DXF import pipeline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,11 @@ namespace OpenNest.IO
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ACadSharp.Entities.Spline spline:
|
case ACadSharp.Entities.Spline spline:
|
||||||
lines.AddRange(spline.ToOpenNest());
|
foreach (var e in spline.ToOpenNest(SplinePrecision))
|
||||||
|
{
|
||||||
|
if (e is Line l) lines.Add(l);
|
||||||
|
else if (e is Arc a) arcs.Add(a);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ACadSharp.Entities.LwPolyline lwPolyline:
|
case ACadSharp.Entities.LwPolyline lwPolyline:
|
||||||
@@ -57,7 +61,11 @@ namespace OpenNest.IO
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ACadSharp.Entities.Ellipse ellipse:
|
case ACadSharp.Entities.Ellipse ellipse:
|
||||||
lines.AddRange(ellipse.ToOpenNest(SplinePrecision));
|
foreach (var e in ellipse.ToOpenNest())
|
||||||
|
{
|
||||||
|
if (e is Line l) lines.Add(l);
|
||||||
|
else if (e is Arc a) arcs.Add(a);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-76
@@ -56,42 +56,45 @@ namespace OpenNest.IO
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Geometry.Line> ToOpenNest(this Spline spline)
|
public static List<Geometry.Entity> ToOpenNest(this Spline spline, int precision)
|
||||||
{
|
{
|
||||||
var lines = new List<Geometry.Line>();
|
|
||||||
var pts = spline.ControlPoints;
|
|
||||||
|
|
||||||
if (pts.Count == 0)
|
|
||||||
return lines;
|
|
||||||
|
|
||||||
var layer = spline.Layer.ToOpenNest();
|
var layer = spline.Layer.ToOpenNest();
|
||||||
var color = spline.ResolveColor();
|
var color = spline.ResolveColor();
|
||||||
var lineTypeName = spline.ResolveLineTypeName();
|
var lineTypeName = spline.ResolveLineTypeName();
|
||||||
var lastPoint = pts[0].ToOpenNest();
|
|
||||||
|
|
||||||
for (var i = 1; i < pts.Count; i++)
|
// Evaluate actual points on the spline curve (not control points)
|
||||||
|
List<XYZ> curvePoints;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var nextPoint = pts[i].ToOpenNest();
|
curvePoints = spline.PolygonalVertexes(precision > 0 ? precision : 200);
|
||||||
|
}
|
||||||
lines.Add(new Geometry.Line(lastPoint, nextPoint)
|
catch
|
||||||
{
|
{
|
||||||
Layer = layer,
|
curvePoints = null;
|
||||||
Color = color,
|
|
||||||
LineTypeName = lineTypeName
|
|
||||||
});
|
|
||||||
|
|
||||||
lastPoint = nextPoint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spline.IsClosed)
|
if (curvePoints == null || curvePoints.Count < 2)
|
||||||
lines.Add(new Geometry.Line(lastPoint, pts[0].ToOpenNest())
|
{
|
||||||
{
|
// Fallback: use control points if evaluation fails
|
||||||
Layer = layer,
|
curvePoints = new List<XYZ>(spline.ControlPoints);
|
||||||
Color = color,
|
if (curvePoints.Count < 2)
|
||||||
LineTypeName = lineTypeName
|
return new List<Geometry.Entity>();
|
||||||
});
|
}
|
||||||
|
|
||||||
return lines;
|
var points = new List<Vector>(curvePoints.Count);
|
||||||
|
foreach (var pt in curvePoints)
|
||||||
|
points.Add(pt.ToOpenNest());
|
||||||
|
|
||||||
|
var entities = SplineConverter.Convert(points, spline.IsClosed, tolerance: 0.001);
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
entity.Layer = layer;
|
||||||
|
entity.Color = color;
|
||||||
|
entity.LineTypeName = lineTypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Geometry.Line> ToOpenNest(this Polyline polyline)
|
public static List<Geometry.Line> ToOpenNest(this Polyline polyline)
|
||||||
@@ -172,70 +175,32 @@ namespace OpenNest.IO
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Geometry.Line> ToOpenNest(this ACadSharp.Entities.Ellipse ellipse, int precision = 200)
|
public static List<Geometry.Entity> ToOpenNest(this ACadSharp.Entities.Ellipse ellipse, double tolerance = 0.001)
|
||||||
{
|
{
|
||||||
var lines = new List<Geometry.Line>();
|
|
||||||
|
|
||||||
var center = new Vector(ellipse.Center.X, ellipse.Center.Y);
|
var center = new Vector(ellipse.Center.X, ellipse.Center.Y);
|
||||||
var majorAxis = new Vector(ellipse.MajorAxisEndPoint.X, ellipse.MajorAxisEndPoint.Y);
|
var majorAxis = new Vector(ellipse.MajorAxisEndPoint.X, ellipse.MajorAxisEndPoint.Y);
|
||||||
var majorLength = System.Math.Sqrt(majorAxis.X * majorAxis.X + majorAxis.Y * majorAxis.Y);
|
var semiMajor = System.Math.Sqrt(majorAxis.X * majorAxis.X + majorAxis.Y * majorAxis.Y);
|
||||||
var minorLength = majorLength * ellipse.RadiusRatio;
|
var semiMinor = semiMajor * ellipse.RadiusRatio;
|
||||||
var rotation = System.Math.Atan2(majorAxis.Y, majorAxis.X);
|
var rotation = System.Math.Atan2(majorAxis.Y, majorAxis.X);
|
||||||
|
|
||||||
var startParam = ellipse.StartParameter;
|
var startParam = ellipse.StartParameter;
|
||||||
var endParam = ellipse.EndParameter;
|
var endParam = ellipse.EndParameter;
|
||||||
|
|
||||||
if (endParam <= startParam)
|
|
||||||
endParam += System.Math.PI * 2.0;
|
|
||||||
|
|
||||||
var step = (endParam - startParam) / precision;
|
|
||||||
|
|
||||||
var points = new List<Vector>();
|
|
||||||
|
|
||||||
for (var i = 0; i <= precision; i++)
|
|
||||||
{
|
|
||||||
var t = startParam + step * i;
|
|
||||||
var x = majorLength * System.Math.Cos(t);
|
|
||||||
var y = minorLength * System.Math.Sin(t);
|
|
||||||
|
|
||||||
// Rotate by the major axis angle and translate to center
|
|
||||||
var cos = System.Math.Cos(rotation);
|
|
||||||
var sin = System.Math.Sin(rotation);
|
|
||||||
var px = center.X + x * cos - y * sin;
|
|
||||||
var py = center.Y + x * sin + y * cos;
|
|
||||||
|
|
||||||
points.Add(new Vector(px, py));
|
|
||||||
}
|
|
||||||
|
|
||||||
var layer = ellipse.Layer.ToOpenNest();
|
var layer = ellipse.Layer.ToOpenNest();
|
||||||
var color = ellipse.ResolveColor();
|
var color = ellipse.ResolveColor();
|
||||||
var lineTypeName = ellipse.ResolveLineTypeName();
|
var lineTypeName = ellipse.ResolveLineTypeName();
|
||||||
|
|
||||||
for (var i = 0; i < points.Count - 1; i++)
|
var entities = EllipseConverter.Convert(center, semiMajor, semiMinor, rotation,
|
||||||
|
startParam, endParam, tolerance);
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
{
|
{
|
||||||
lines.Add(new Geometry.Line(points[i], points[i + 1])
|
entity.Layer = layer;
|
||||||
{
|
entity.Color = color;
|
||||||
Layer = layer,
|
entity.LineTypeName = lineTypeName;
|
||||||
Color = color,
|
|
||||||
LineTypeName = lineTypeName
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close only if it's a full ellipse (sweep ≈ 2π)
|
return entities;
|
||||||
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();
|
|
||||||
lines.Add(new Geometry.Line(last.EndPoint, first.StartPoint)
|
|
||||||
{
|
|
||||||
Layer = layer,
|
|
||||||
Color = color,
|
|
||||||
LineTypeName = lineTypeName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Geometry.Layer ToOpenNest(this ACadSharp.Tables.Layer layer)
|
public static Geometry.Layer ToOpenNest(this ACadSharp.Tables.Layer layer)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class SolidWorksBendDetectorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Simplifier_EllipseSegments_FewLargeArcs()
|
public void EllipseConverter_ProducesArcsDirectly()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11 Test.dxf");
|
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11 Test.dxf");
|
||||||
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
|
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
|
||||||
@@ -38,36 +38,21 @@ public class SolidWorksBendDetectorTests
|
|||||||
var importer = new OpenNest.IO.DxfImporter { SplinePrecision = 200 };
|
var importer = new OpenNest.IO.DxfImporter { SplinePrecision = 200 };
|
||||||
var result = importer.Import(path);
|
var result = importer.Import(path);
|
||||||
|
|
||||||
|
// EllipseConverter now produces arcs directly during import,
|
||||||
|
// so the imported entities should contain Arc instances from the ellipses
|
||||||
|
var arcCount = result.Entities.Count(e => e is OpenNest.Geometry.Arc);
|
||||||
|
Assert.True(arcCount > 0, "Expected arcs from ellipse conversion");
|
||||||
|
|
||||||
|
// The GeometrySimplifier should find few or no line runs to simplify,
|
||||||
|
// because ellipses are already converted to arcs at import time
|
||||||
var shape = new OpenNest.Geometry.Shape();
|
var shape = new OpenNest.Geometry.Shape();
|
||||||
shape.Entities.AddRange(result.Entities);
|
shape.Entities.AddRange(result.Entities);
|
||||||
|
|
||||||
// Default tolerance is 0.5 — should produce very few large arcs
|
|
||||||
var simplifier = new OpenNest.Geometry.GeometrySimplifier();
|
var simplifier = new OpenNest.Geometry.GeometrySimplifier();
|
||||||
var candidates = simplifier.Analyze(shape);
|
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,
|
Assert.True(candidates.Count <= 10,
|
||||||
$"Expected <=10 arcs but got {candidates.Count}: {info}");
|
$"Expected <=10 simplifier candidates but got {candidates.Count}");
|
||||||
|
|
||||||
// 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]
|
[Fact]
|
||||||
@@ -81,8 +66,16 @@ public class SolidWorksBendDetectorTests
|
|||||||
|
|
||||||
// The DXF has 2 trimmed ellipses forming an oblong slot.
|
// The DXF has 2 trimmed ellipses forming an oblong slot.
|
||||||
// Trimmed ellipses must not generate a closing chord line.
|
// Trimmed ellipses must not generate a closing chord line.
|
||||||
// 83 = 72 lines + 4 arcs + 7 circles + ellipse segments (heavily merged by optimizer)
|
// EllipseConverter now produces arcs instead of line segments,
|
||||||
Assert.Equal(83, result.Entities.Count);
|
// changing the entity count. Verify arcs are present and no
|
||||||
|
// spurious closing chord exists.
|
||||||
|
var arcCount = result.Entities.Count(e => e is OpenNest.Geometry.Arc);
|
||||||
|
var lineCount = result.Entities.Count(e => e is OpenNest.Geometry.Line);
|
||||||
|
var circleCount = result.Entities.Count(e => e is OpenNest.Geometry.Circle);
|
||||||
|
|
||||||
|
Assert.True(arcCount > 0, "Expected arcs from ellipse conversion");
|
||||||
|
Assert.True(circleCount >= 7, $"Expected at least 7 circles, got {circleCount}");
|
||||||
|
Assert.Equal(115, result.Entities.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user