feat: add EllipseConverter evaluation helpers with tests
Add EllipseConverter static class with foundational methods for converting ellipse parameters to circular arcs: EvaluatePoint, EvaluateTangent, EvaluateNormal, and IntersectNormals. All 8 unit tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
67
OpenNest.Core/Geometry/EllipseConverter.cs
Normal file
67
OpenNest.Core/Geometry/EllipseConverter.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
public static class EllipseConverter
|
||||
{
|
||||
private const int MaxSubdivisionDepth = 12;
|
||||
private const int DeviationSamples = 20;
|
||||
|
||||
internal static Vector EvaluatePoint(double semiMajor, double semiMinor, double rotation, Vector center, double t)
|
||||
{
|
||||
var x = semiMajor * System.Math.Cos(t);
|
||||
var y = semiMinor * System.Math.Sin(t);
|
||||
|
||||
var cos = System.Math.Cos(rotation);
|
||||
var sin = System.Math.Sin(rotation);
|
||||
|
||||
return new Vector(
|
||||
center.X + x * cos - y * sin,
|
||||
center.Y + x * sin + y * cos);
|
||||
}
|
||||
|
||||
internal static Vector EvaluateTangent(double semiMajor, double semiMinor, double rotation, double t)
|
||||
{
|
||||
var tx = -semiMajor * System.Math.Sin(t);
|
||||
var ty = semiMinor * System.Math.Cos(t);
|
||||
|
||||
var cos = System.Math.Cos(rotation);
|
||||
var sin = System.Math.Sin(rotation);
|
||||
|
||||
return new Vector(
|
||||
tx * cos - ty * sin,
|
||||
tx * sin + ty * cos);
|
||||
}
|
||||
|
||||
internal static Vector EvaluateNormal(double semiMajor, double semiMinor, double rotation, double t)
|
||||
{
|
||||
// Inward normal: perpendicular to tangent, pointing toward center of curvature.
|
||||
// In local coords: N(t) = (-b*cos(t), -a*sin(t))
|
||||
var nx = -semiMinor * System.Math.Cos(t);
|
||||
var ny = -semiMajor * System.Math.Sin(t);
|
||||
|
||||
var cos = System.Math.Cos(rotation);
|
||||
var sin = System.Math.Sin(rotation);
|
||||
|
||||
return new Vector(
|
||||
nx * cos - ny * sin,
|
||||
nx * sin + ny * cos);
|
||||
}
|
||||
|
||||
internal static Vector IntersectNormals(Vector p1, Vector n1, Vector p2, Vector n2)
|
||||
{
|
||||
// Solve: p1 + s*n1 = p2 + t*n2
|
||||
var det = n1.X * (-n2.Y) - (-n2.X) * n1.Y;
|
||||
if (System.Math.Abs(det) < 1e-10)
|
||||
return Vector.Invalid;
|
||||
|
||||
var dx = p2.X - p1.X;
|
||||
var dy = p2.Y - p1.Y;
|
||||
var s = (dx * (-n2.Y) - dy * (-n2.X)) / det;
|
||||
|
||||
return new Vector(p1.X + s * n1.X, p1.Y + s * n1.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
OpenNest.Tests/EllipseConverterTests.cs
Normal file
81
OpenNest.Tests/EllipseConverterTests.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using Xunit;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class EllipseConverterTests
|
||||
{
|
||||
private const double Tol = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePoint_AtZero_ReturnsMajorAxisEnd()
|
||||
{
|
||||
var p = EllipseConverter.EvaluatePoint(10, 5, 0, new Vector(0, 0), 0);
|
||||
Assert.InRange(p.X, 10 - Tol, 10 + Tol);
|
||||
Assert.InRange(p.Y, -Tol, Tol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePoint_AtHalfPi_ReturnsMinorAxisEnd()
|
||||
{
|
||||
var p = EllipseConverter.EvaluatePoint(10, 5, 0, new Vector(0, 0), System.Math.PI / 2);
|
||||
Assert.InRange(p.X, -Tol, Tol);
|
||||
Assert.InRange(p.Y, 5 - Tol, 5 + Tol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePoint_WithRotation_RotatesCorrectly()
|
||||
{
|
||||
var p = EllipseConverter.EvaluatePoint(10, 5, System.Math.PI / 2, new Vector(0, 0), 0);
|
||||
Assert.InRange(p.X, -Tol, Tol);
|
||||
Assert.InRange(p.Y, 10 - Tol, 10 + Tol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluatePoint_WithCenter_TranslatesCorrectly()
|
||||
{
|
||||
var p = EllipseConverter.EvaluatePoint(10, 5, 0, new Vector(100, 200), 0);
|
||||
Assert.InRange(p.X, 110 - Tol, 110 + Tol);
|
||||
Assert.InRange(p.Y, 200 - Tol, 200 + Tol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateTangent_AtZero_PointsUp()
|
||||
{
|
||||
var t = EllipseConverter.EvaluateTangent(10, 5, 0, 0);
|
||||
Assert.InRange(t.X, -Tol, Tol);
|
||||
Assert.True(t.Y > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvaluateNormal_AtZero_PointsInward()
|
||||
{
|
||||
var n = EllipseConverter.EvaluateNormal(10, 5, 0, 0);
|
||||
Assert.True(n.X < 0);
|
||||
Assert.InRange(n.Y, -Tol, Tol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntersectNormals_PerpendicularNormals_FindsCenter()
|
||||
{
|
||||
var p1 = new Vector(5, 0);
|
||||
var n1 = new Vector(-1, 0);
|
||||
var p2 = new Vector(0, 5);
|
||||
var n2 = new Vector(0, -1);
|
||||
var center = EllipseConverter.IntersectNormals(p1, n1, p2, n2);
|
||||
Assert.InRange(center.X, -Tol, Tol);
|
||||
Assert.InRange(center.Y, -Tol, Tol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntersectNormals_ParallelNormals_ReturnsInvalid()
|
||||
{
|
||||
var p1 = new Vector(0, 0);
|
||||
var n1 = new Vector(1, 0);
|
||||
var p2 = new Vector(0, 5);
|
||||
var n2 = new Vector(1, 0);
|
||||
var center = EllipseConverter.IntersectNormals(p1, n1, p2, n2);
|
||||
Assert.False(center.IsValid());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user