test(geometry): characterize converter winding before ArcFit extraction
This commit is contained in:
@@ -375,6 +375,75 @@ public class EllipseConverterTests
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Frozen ordered output from 5bf3c5f, including both angular seams and a
|
||||||
|
// translated, rotated partial ellipse (the public API traverses parameters CCW).
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SignedAngleCharacterizationCases))]
|
||||||
|
public void Convert_SignedAngleCharacterization_PreservesOrderedArcs(
|
||||||
|
string scenario, double[][] expected, bool[] reversed
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var (start, end, rotation) = scenario switch
|
||||||
|
{
|
||||||
|
"atan-seam" => (2.8, 3.8, 0.0),
|
||||||
|
"positive-x-seam" => (5.8, 0.5, 0.0),
|
||||||
|
_ => (0.2, 1.3, 0.6),
|
||||||
|
};
|
||||||
|
var result = EllipseConverter.Convert(new Vector(3, -2), 6, 4, rotation,
|
||||||
|
start, end, tolerance: 0.1);
|
||||||
|
|
||||||
|
Assert.Collection(result, expected.Select((parameters, i) =>
|
||||||
|
(Action<Entity>)(entity => AssertCharacterizedArc(entity, parameters, reversed[i]))
|
||||||
|
).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> SignedAngleCharacterizationCases()
|
||||||
|
{
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"atan-seam",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { -0.16603986922559466, -2.0183680896443055, 2.8340196558132877, 2.6417457217524727, 3.1351113230149354 },
|
||||||
|
new[] { -0.17776605557071123, -1.983538836846724, 2.8222819502985983, 3.1474252588278113, 3.624130294015861 },
|
||||||
|
new[] { 0.7441132153098247, -1.4836007849113604, 3.8709161624146646, 3.6280252302389693, 4.013669489810318 },
|
||||||
|
},
|
||||||
|
new[] { false, false, false },
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"positive-x-seam",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 6.004270273073607, -1.9488971518180598, 2.996165565832901, 5.592171901293686, 6.26612839723762 },
|
||||||
|
new[] { 5.9818198770623745, -2.0564884222942372, 3.0187086968352994, 0.018713869149703036, 0.7128403110339223 },
|
||||||
|
},
|
||||||
|
new[] { false, false },
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"rotated-partial-ellipse",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 5.1346691175944725, -1.0670403345989339, 3.79655973712418, 0.9299300117607583, 1.5841942790174177 },
|
||||||
|
new[] { 5.075469862824099, -4.265516161507367, 6.994699783814414, 1.5696047599218284, 2.002556947193732 },
|
||||||
|
},
|
||||||
|
new[] { false, false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertCharacterizedArc(Entity entity, double[] expected, bool reversed)
|
||||||
|
{
|
||||||
|
var arc = Assert.IsType<Arc>(entity);
|
||||||
|
var actual = new[]
|
||||||
|
{
|
||||||
|
arc.Center.X, arc.Center.Y, arc.Radius, arc.StartAngle, arc.EndAngle,
|
||||||
|
};
|
||||||
|
for (var i = 0; i < expected.Length; i++)
|
||||||
|
Assert.InRange(actual[i], expected[i] - 1e-10, expected[i] + 1e-10);
|
||||||
|
Assert.Equal(reversed, arc.IsReversed);
|
||||||
|
}
|
||||||
|
|
||||||
private static (double minX, double maxX) GetBoundingBox(IEnumerable<Arc> arcs)
|
private static (double minX, double maxX) GetBoundingBox(IEnumerable<Arc> arcs)
|
||||||
{
|
{
|
||||||
var minX = double.MaxValue;
|
var minX = double.MaxValue;
|
||||||
|
|||||||
@@ -241,6 +241,134 @@ public class GeometrySimplifierTests
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Frozen Analyze + Apply output from 5bf3c5f. The circles are order markers;
|
||||||
|
// the rotated ellipse also pins the two trailing lines left unfitted.
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SignedAngleCharacterizationCases))]
|
||||||
|
public void Apply_SignedAngleCharacterization_PreservesOrderedEntities(
|
||||||
|
string scenario, double[][] expected, bool[] reversed, int expectedEndIndex
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var points = SplineConverterTests.SignedAngleCharacterizationPoints(scenario);
|
||||||
|
var shape = new Shape();
|
||||||
|
shape.Entities.Add(new Circle(new Vector(-30, 40), 2.5));
|
||||||
|
for (var i = 0; i < points.Count - 1; i++)
|
||||||
|
shape.Entities.Add(new Line(points[i], points[i + 1]));
|
||||||
|
shape.Entities.Add(new Circle(new Vector(30, -40), 3.5));
|
||||||
|
var simplifier = new GeometrySimplifier { Tolerance = 0.05 };
|
||||||
|
|
||||||
|
var candidate = Assert.Single(simplifier.Analyze(shape));
|
||||||
|
Assert.Equal(1, candidate.StartIndex);
|
||||||
|
Assert.Equal(expectedEndIndex, candidate.EndIndex);
|
||||||
|
var result = simplifier.Apply(shape, new List<ArcCandidate> { candidate });
|
||||||
|
var assertions = new List<Action<Entity>>
|
||||||
|
{
|
||||||
|
entity => AssertCharacterizedCircle(entity, -30, 40, 2.5),
|
||||||
|
entity => AssertCharacterizedArc(entity, expected[0], reversed[0]),
|
||||||
|
};
|
||||||
|
if (scenario == "rotated-partial-ellipse")
|
||||||
|
{
|
||||||
|
assertions.Add(entity => AssertCharacterizedLine(entity,
|
||||||
|
new[] { 2.644054114237731, 2.288101855169011, 2.3955895038427424, 2.1920946874414766 }));
|
||||||
|
assertions.Add(entity => AssertCharacterizedLine(entity,
|
||||||
|
new[] { 2.3955895038427424, 2.1920946874414766, 2.148394352966971, 2.087282751199381 }));
|
||||||
|
}
|
||||||
|
assertions.Add(entity => AssertCharacterizedCircle(entity, 30, -40, 3.5));
|
||||||
|
Assert.Collection(result.Entities, assertions.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> SignedAngleCharacterizationCases()
|
||||||
|
{
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"ccw",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 2.999999999999998, -2.000000000000008, 5.000000000000004, 0.30000000000000143, 2.2999999999999985 },
|
||||||
|
},
|
||||||
|
new[] { false },
|
||||||
|
24,
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"cw",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 3.0000000000000053, -1.9999999999999813, 4.999999999999989, 2.300000000000003, 0.2999999999999968 },
|
||||||
|
},
|
||||||
|
new[] { true },
|
||||||
|
24,
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"reversed",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 2.999999999999998, -2.000000000000008, 5.000000000000004, 2.2999999999999985, 0.30000000000000143 },
|
||||||
|
},
|
||||||
|
new[] { true },
|
||||||
|
24,
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"atan-seam",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 2.9999999999999436, -2.0000000000000098, 4.999999999999949, 2.799999999999994, 3.8000000000000047 },
|
||||||
|
},
|
||||||
|
new[] { false },
|
||||||
|
24,
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"positive-x-seam",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 2.9999999999999565, -2.000000000000001, 5.00000000000004, 5.800000000000003, 0.5168146928204091 },
|
||||||
|
},
|
||||||
|
new[] { false },
|
||||||
|
24,
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"rotated-partial-ellipse",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 4.738877816517773, -2.224586867329932, 4.975203096337214, 1.0053406925186938, 2.0054021506562583 },
|
||||||
|
},
|
||||||
|
new[] { false },
|
||||||
|
22,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertCharacterizedArc(Entity entity, double[] expected, bool reversed)
|
||||||
|
{
|
||||||
|
var arc = Assert.IsType<Arc>(entity);
|
||||||
|
var actual = new[]
|
||||||
|
{
|
||||||
|
arc.Center.X, arc.Center.Y, arc.Radius, arc.StartAngle, arc.EndAngle,
|
||||||
|
};
|
||||||
|
for (var i = 0; i < expected.Length; i++)
|
||||||
|
Assert.InRange(actual[i], expected[i] - 1e-10, expected[i] + 1e-10);
|
||||||
|
Assert.Equal(reversed, arc.IsReversed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertCharacterizedCircle(Entity entity, double x, double y, double radius)
|
||||||
|
{
|
||||||
|
var circle = Assert.IsType<Circle>(entity);
|
||||||
|
Assert.Equal(x, circle.Center.X);
|
||||||
|
Assert.Equal(y, circle.Center.Y);
|
||||||
|
Assert.Equal(radius, circle.Radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertCharacterizedLine(Entity entity, double[] expected)
|
||||||
|
{
|
||||||
|
var line = Assert.IsType<Line>(entity);
|
||||||
|
var actual = new[] { line.StartPoint.X, line.StartPoint.Y, line.EndPoint.X, line.EndPoint.Y };
|
||||||
|
for (var i = 0; i < expected.Length; i++)
|
||||||
|
Assert.InRange(actual[i], expected[i] - 1e-10, expected[i] + 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
private static Vector ArcTangentAt(Arc arc, Vector pt)
|
private static Vector ArcTangentAt(Arc arc, Vector pt)
|
||||||
{
|
{
|
||||||
var ang = System.Math.Atan2(pt.Y - arc.Center.Y, pt.X - arc.Center.X);
|
var ang = System.Math.Atan2(pt.Y - arc.Center.Y, pt.X - arc.Center.X);
|
||||||
|
|||||||
@@ -112,6 +112,120 @@ public class SplineConverterTests
|
|||||||
Assert.Empty(result);
|
Assert.Empty(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recorded on the unmodified 5bf3c5f converter. Preserve even the final CW
|
||||||
|
// arc of the rotated ellipse: this extraction must not change fitting decisions.
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SignedAngleCharacterizationCases))]
|
||||||
|
public void Convert_SignedAngleCharacterization_PreservesOrderedArcs(
|
||||||
|
string scenario, double[][] expected, bool[] reversed
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var points = SignedAngleCharacterizationPoints(scenario);
|
||||||
|
var result = SplineConverter.Convert(points, isClosed: false, tolerance: 0.05);
|
||||||
|
|
||||||
|
Assert.Collection(result, expected.Select((parameters, i) =>
|
||||||
|
(Action<Entity>)(entity => AssertCharacterizedArc(entity, parameters, reversed[i]))
|
||||||
|
).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> SignedAngleCharacterizationCases()
|
||||||
|
{
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"ccw",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 3.0000000000000004, -1.9999999999999991, 4.999999999999999, 0.2999999999999998, 2.3000000000000003 },
|
||||||
|
},
|
||||||
|
new[] { false },
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"cw",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 3, -1.9999999999999991, 4.999999999999999, 2.3, 0.2999999999999999 },
|
||||||
|
},
|
||||||
|
new[] { true },
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"reversed",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 3, -1.9999999999999991, 4.999999999999999, 2.3, 0.2999999999999998 },
|
||||||
|
},
|
||||||
|
new[] { true },
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"atan-seam",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 2.9999999999999982, -2.0000000000000004, 4.999999999999998, 2.7999999999999994, 3.8 },
|
||||||
|
},
|
||||||
|
new[] { false },
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"positive-x-seam",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 2.999999999999999, -2, 5.000000000000002, 5.8, 0.5168146928204133 },
|
||||||
|
},
|
||||||
|
new[] { false },
|
||||||
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
"rotated-partial-ellipse",
|
||||||
|
new double[][]
|
||||||
|
{
|
||||||
|
new[] { 4.7398647354210635, -2.2095236378718557, 4.9619610272064785, 1.0038820776565072, 2.0068607655184447 },
|
||||||
|
new[] { 0.4338148276802185, 7.03129213222217, 5.232877956552578, 5.148453419108238, 5.046209652075572 },
|
||||||
|
},
|
||||||
|
new[] { false, true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static List<Vector> SignedAngleCharacterizationPoints(string scenario)
|
||||||
|
{
|
||||||
|
var (start, end, ellipse) = scenario switch
|
||||||
|
{
|
||||||
|
"cw" => (2.3, 0.3, false),
|
||||||
|
"atan-seam" => (2.8, 3.8, false),
|
||||||
|
"positive-x-seam" => (5.8, 6.8, false),
|
||||||
|
"rotated-partial-ellipse" => (0.2, 1.3, true),
|
||||||
|
_ => (0.3, 2.3, false),
|
||||||
|
};
|
||||||
|
var points = new List<Vector>();
|
||||||
|
for (var i = 0; i <= 24; i++)
|
||||||
|
{
|
||||||
|
var angle = start + (end - start) * i / 24;
|
||||||
|
var x = (ellipse ? 6 : 5) * System.Math.Cos(angle);
|
||||||
|
var y = (ellipse ? 4 : 5) * System.Math.Sin(angle);
|
||||||
|
var rotation = ellipse ? 0.6 : 0;
|
||||||
|
points.Add(new Vector(
|
||||||
|
3 + x * System.Math.Cos(rotation) - y * System.Math.Sin(rotation),
|
||||||
|
-2 + x * System.Math.Sin(rotation) + y * System.Math.Cos(rotation)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (scenario == "reversed")
|
||||||
|
points.Reverse();
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertCharacterizedArc(Entity entity, double[] expected, bool reversed)
|
||||||
|
{
|
||||||
|
var arc = Assert.IsType<Arc>(entity);
|
||||||
|
var actual = new[]
|
||||||
|
{
|
||||||
|
arc.Center.X, arc.Center.Y, arc.Radius, arc.StartAngle, arc.EndAngle,
|
||||||
|
};
|
||||||
|
for (var i = 0; i < expected.Length; i++)
|
||||||
|
Assert.InRange(actual[i], expected[i] - 1e-10, expected[i] + 1e-10);
|
||||||
|
Assert.Equal(reversed, arc.IsReversed);
|
||||||
|
}
|
||||||
|
|
||||||
private static Vector GetStartPoint(Entity e)
|
private static Vector GetStartPoint(Entity e)
|
||||||
{
|
{
|
||||||
return e switch
|
return e switch
|
||||||
|
|||||||
Reference in New Issue
Block a user