diff --git a/Common/shapeEngine/contourGen.cs b/Common/shapeEngine/contourGen.cs index 89570dd0..505fa601 100644 --- a/Common/shapeEngine/contourGen.cs +++ b/Common/shapeEngine/contourGen.cs @@ -158,9 +158,19 @@ public static PathD makeContour(PathD original_path, double concaveRadius, doubl if (corner_types[i] == (int)CornerType.Concave) radius = concaveRadius; - PathD current_corner = ProcessCorner(startLine, endLine, radius, angularResolution, edgeResolution, - SamplingMode.ByMaxAngle); - processed[i] = current_corner; + // Check if this corner should use circular arc generation + if (ShouldUseCircularArc(original_path[i], prevMid, nextMid, radius)) + { + // Use circular arc for this corner + processed[i] = GenerateCornerCircularArc(original_path[i], prevMid, nextMid, radius, angularResolution); + } + else + { + // Use normal bezier processing for this corner + PathD current_corner = ProcessCorner(startLine, endLine, radius, angularResolution, edgeResolution, + SamplingMode.ByMaxAngle); + processed[i] = current_corner; + } }); } else @@ -188,9 +198,19 @@ public static PathD makeContour(PathD original_path, double concaveRadius, doubl if (corner_types[i] == (int)CornerType.Concave) radius = concaveRadius; - PathD current_corner = ProcessCorner(startLine, endLine, radius, angularResolution, edgeResolution, - SamplingMode.ByMaxAngle); - processed[i] = current_corner; + // Check if this corner should use circular arc generation + if (ShouldUseCircularArc(original_path[i], prevMid, nextMid, radius)) + { + // Use circular arc for this corner + processed[i] = GenerateCornerCircularArc(original_path[i], prevMid, nextMid, radius, angularResolution); + } + else + { + // Use normal bezier processing for this corner + PathD current_corner = ProcessCorner(startLine, endLine, radius, angularResolution, edgeResolution, + SamplingMode.ByMaxAngle); + processed[i] = current_corner; + } } } @@ -1170,4 +1190,240 @@ static bool TryBuildTangentArc(PointD p0, PointD dir0, PointD p1, PointD dir1, o if (Math.Abs(sweep) > Math.PI * 1.5) return false; return true; } + + /// + /// Attempts to generate a circular contour when the convex radius is large enough + /// that all corners would merge into a circular result. + /// + /// The original polygon path + /// The convex corner radius + /// Edge resolution for circle sampling + /// Angular resolution for circle sampling + /// Circular path if conditions are met, null otherwise + static PathD TryGenerateCircularContour(PathD original_path, double convexRadius, double edgeResolution, double angularResolution) + { + if (original_path.Count < 4) // Need at least 3 vertices plus closing vertex + return null; + + int cornerCount = original_path.Count - 1; // Exclude closing vertex + + // Only apply to simple convex polygons (4-8 corners max to be conservative) + if (cornerCount < 3 || cornerCount > 8) + return null; + + // Check if the polygon is convex - circular convergence only makes sense for convex polygons + if (!IsConvexPolygon(original_path)) + return null; + + // Calculate edge half-lengths to determine if radius would cause convergence + var edgeHalfLengths = new List(); + + for (int i = 0; i < cornerCount; i++) + { + var currentVertex = original_path[i]; + var nextVertex = original_path[(i + 1) % cornerCount]; + double edgeLength = Helper.Length(Helper.Minus(nextVertex, currentVertex)); + edgeHalfLengths.Add(edgeLength / 2.0); + } + + // Check if the convex radius exceeds ALL edge half-lengths by a significant margin + // This indicates that corner fillets would overlap/merge, suggesting circular convergence + double minEdgeHalfLength = edgeHalfLengths.Min(); + if (convexRadius < minEdgeHalfLength * 1.1) // 10% margin to be conservative + return null; // Normal corner processing is appropriate + + // Calculate polygon center (centroid) + double centerX = 0, centerY = 0; + for (int i = 0; i < cornerCount; i++) + { + centerX += original_path[i].x; + centerY += original_path[i].y; + } + centerX /= cornerCount; + centerY /= cornerCount; + var center = new PointD(centerX, centerY); + + // Calculate the circumradius (distance to farthest corner) + double maxDistanceFromCenter = 0; + for (int i = 0; i < cornerCount; i++) + { + double distance = Helper.Length(Helper.Minus(original_path[i], center)); + maxDistanceFromCenter = Math.Max(maxDistanceFromCenter, distance); + } + + // Use the convex radius as the circle radius, but ensure it's at least the circumradius + double circleRadius = Math.Max(convexRadius, maxDistanceFromCenter); + + // Generate circular path + return GenerateCircularPath(center, circleRadius, angularResolution); + } + + /// + /// Checks if a polygon is convex by examining the cross product of consecutive edge vectors. + /// + /// The polygon path to check + /// True if the polygon is convex, false otherwise + static bool IsConvexPolygon(PathD polygon) + { + if (polygon.Count < 4) // Need at least 3 vertices plus closing vertex + return false; + + int cornerCount = polygon.Count - 1; // Exclude closing vertex + bool? isClockwise = null; + + for (int i = 0; i < cornerCount; i++) + { + var p1 = polygon[i]; + var p2 = polygon[(i + 1) % cornerCount]; + var p3 = polygon[(i + 2) % cornerCount]; + + // Calculate cross product of vectors p1->p2 and p2->p3 + var v1 = Helper.Minus(p2, p1); + var v2 = Helper.Minus(p3, p2); + double crossProduct = v1.x * v2.y - v1.y * v2.x; + + if (Math.Abs(crossProduct) > 1e-10) // Avoid floating point precision issues + { + bool currentIsClockwise = crossProduct < 0; + + if (isClockwise.HasValue && isClockwise.Value != currentIsClockwise) + return false; // Found both clockwise and counter-clockwise turns -> not convex + + isClockwise = currentIsClockwise; + } + } + + return true; // All turns are in the same direction -> convex + } + + /// + /// Generates a circular path with the specified center, radius and angular resolution. + /// + /// Center point of the circle + /// Radius of the circle + /// Angular resolution in radians + /// PathD representing the circle + static PathD GenerateCircularPath(PointD center, double radius, double angularResolution) + { + // Calculate number of segments needed for the given angular resolution + int segmentCount = Math.Max(8, (int)Math.Ceiling(2 * Math.PI / angularResolution)); + + var circle = new PathD(segmentCount + 1); // +1 for closing the path + + for (int i = 0; i < segmentCount; i++) + { + double angle = 2 * Math.PI * i / segmentCount; + var point = new PointD( + center.x + radius * Math.Cos(angle), + center.y + radius * Math.Sin(angle) + ); + circle.Add(point); + } + + // Close the path by adding the first point again + if (segmentCount > 0) + { + circle.Add(circle[0]); + } + + return circle; + } + + /// + /// Determines if a corner should use circular arc generation based on radius vs corner-to-midpoint distances. + /// Per @philstopford's clarification: converge when radius >= distance from corner to edge midpoint. + /// + static bool ShouldUseCircularArc(PointD corner, PointD prevMid, PointD nextMid, double radius) + { + // Calculate distances from corner to each edge midpoint + double distToPrevMid = Helper.Length(Helper.Minus(prevMid, corner)); + double distToNextMid = Helper.Length(Helper.Minus(nextMid, corner)); + + // Take the minimum distance - this is the limiting factor for this corner + double minDistanceToEdgeMidpoint = Math.Min(distToPrevMid, distToNextMid); + + // If radius is >= the minimum distance to edge midpoint, use circular arc + bool shouldUseArc = radius >= minDistanceToEdgeMidpoint; + + // Debug logging (will be visible in test output) + //Console.WriteLine($"Corner ({corner.x:F1},{corner.y:F1}): radius={radius:F1}, minDist={minDistanceToEdgeMidpoint:F1}, useArc={shouldUseArc}"); + + return shouldUseArc; + } + + /// + /// Generates a circular arc for a specific corner when circular convergence conditions are met. + /// Instead of creating a large arc, this constrains the radius to produce reasonable corner rounding. + /// + static PathD GenerateCornerCircularArc(PointD corner, PointD prevMid, PointD nextMid, double radius, double angularResolution) + { + // Calculate distances from corner to midpoints + double distToPrevMid = Helper.Length(Helper.Minus(prevMid, corner)); + double distToNextMid = Helper.Length(Helper.Minus(nextMid, corner)); + double minDistanceToMidpoint = Math.Min(distToPrevMid, distToNextMid); + + // When radius >= distance to midpoint, constrain the effective radius to avoid overlapping edges + // Use 80% of the minimum distance to edge midpoint to create reasonable corner rounding + double effectiveRadius = Math.Min(radius, minDistanceToMidpoint * 0.8); + + // Calculate vectors from corner to midpoints + PointD startDir = Helper.Normalized(Helper.Minus(prevMid, corner)); + PointD endDir = Helper.Normalized(Helper.Minus(nextMid, corner)); + + // Calculate curve start and end points using the effective radius + PointD curveStartPoint = Helper.Add(corner, Helper.Mult(startDir, effectiveRadius)); + PointD curveEndPoint = Helper.Add(corner, Helper.Mult(endDir, effectiveRadius)); + + // Calculate the angle between the two directions + double dotProduct = Helper.Dot(startDir, endDir); + dotProduct = Math.Max(-1.0, Math.Min(1.0, dotProduct)); // Clamp to valid range + double angle = Math.Acos(dotProduct); + + // If angle is too small, return a simple line between curve points + if (angle < 0.01) // Less than ~0.6 degrees + { + return new PathD { curveStartPoint, curveEndPoint }; + } + + // Calculate arc center using the effective radius + var bisectorDir = Helper.Normalized(Helper.Add(startDir, endDir)); + + // Distance from corner to arc center for a circular arc + double halfAngle = angle / 2.0; + double distToCenter = effectiveRadius / Math.Sin(halfAngle); + var arcCenter = Helper.Add(corner, Helper.Mult(bisectorDir, distToCenter)); + + // Calculate start and end angles relative to arc center + var startVec = Helper.Minus(curveStartPoint, arcCenter); + var endVec = Helper.Minus(curveEndPoint, arcCenter); + + double startAngle = Math.Atan2(startVec.y, startVec.x); + double endAngle = Math.Atan2(endVec.y, endVec.x); + + // Ensure we sweep the shorter arc + double angleDiff = endAngle - startAngle; + if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; + if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; + + // Calculate number of segments for the arc + int segments = Math.Max(3, (int)Math.Ceiling(Math.Abs(angleDiff) / angularResolution)); + + // Calculate the actual radius from the arc center to the curve points + double actualRadius = Helper.Length(startVec); + + PathD arc = new PathD(); + + for (int i = 0; i <= segments; i++) + { + double t = (double)i / segments; + double currentAngle = startAngle + t * angleDiff; + + double x = arcCenter.x + actualRadius * Math.Cos(currentAngle); + double y = arcCenter.y + actualRadius * Math.Sin(currentAngle); + + arc.Add(new PointD(x, y)); + } + + return arc; + } } \ No newline at end of file diff --git a/Prototyping/test_circle_convergence/Program.cs b/Prototyping/test_circle_convergence/Program.cs new file mode 100644 index 00000000..f04701e2 --- /dev/null +++ b/Prototyping/test_circle_convergence/Program.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Globalization; +using System.Linq; +using Clipper2Lib; +using shapeEngine; + +namespace test_circle_convergence +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Testing ContourGen Circular Convergence Fix"); + Console.WriteLine("=========================================="); + + // Test Case 1: Square with small radius (should produce rounded square) + TestSquareSmallRadius(); + + // Test Case 2: Square with large radius (should converge to circle) + TestSquareLargeRadius(); + + // Test Case 3: Rectangle with large radius + TestRectangleLargeRadius(); + + // Test Case 4: Hexagon with large radius + TestHexagonLargeRadius(); + + Console.WriteLine("\nAll tests completed. Check generated SVG files for visual verification."); + } + + static void TestSquareSmallRadius() + { + Console.WriteLine("\n1. Testing Square with Small Radius (10):"); + + var square = new PathD + { + new PointD(0, 0), + new PointD(100, 0), + new PointD(100, 100), + new PointD(0, 100), + new PointD(0, 0) + }; + + var result = contourGen.makeContour( + square, + concaveRadius: 0, + convexRadius: 10, + edgeResolution: 1.0, + angularResolution: 0.1, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + CreateSvgOutput("square_small_radius.svg", square, result, "Square with small radius (10)"); + AnalyzeCircularity(result, "Small radius"); + } + + static void TestSquareLargeRadius() + { + Console.WriteLine("\n2. Testing Square with Large Radius (60):"); + + var square = new PathD + { + new PointD(0, 0), + new PointD(100, 0), + new PointD(100, 100), + new PointD(0, 100), + new PointD(0, 0) + }; + + // Debug: Check edge half lengths + double edgeLength = 100; // Each edge of the square + double halfEdge = edgeLength / 2.0; // = 50 + double radius = 60; + Console.WriteLine($" Edge length: {edgeLength}, Half-edge: {halfEdge}, Radius: {radius}"); + Console.WriteLine($" Radius > half-edge * 1.2: {radius} > {halfEdge * 1.2} = {radius > halfEdge * 1.2}"); + + var result = contourGen.makeContour( + square, + concaveRadius: 0, + convexRadius: 60, + edgeResolution: 1.0, + angularResolution: 0.1, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + CreateSvgOutput("square_large_radius.svg", square, result, "Square with large radius (60) - should be circular"); + AnalyzeCircularity(result, "Large radius (should be circular)"); + } + + static void TestRectangleLargeRadius() + { + Console.WriteLine("\n3. Testing Rectangle with Large Radius (40):"); + + var rectangle = new PathD + { + new PointD(0, 0), + new PointD(120, 0), + new PointD(120, 60), + new PointD(0, 60), + new PointD(0, 0) + }; + + var result = contourGen.makeContour( + rectangle, + concaveRadius: 0, + convexRadius: 40, // Greater than half of shorter edge (30) + edgeResolution: 1.0, + angularResolution: 0.1, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + CreateSvgOutput("rectangle_large_radius.svg", rectangle, result, "Rectangle with large radius (40) - should be circular"); + AnalyzeCircularity(result, "Rectangle large radius"); + } + + static void TestHexagonLargeRadius() + { + Console.WriteLine("\n4. Testing Hexagon with Large Radius (50):"); + + // Create a regular hexagon centered at origin, then translate + var hexagon = new PathD(); + for (int i = 0; i < 6; i++) + { + double angle = i * Math.PI / 3; // 60 degrees apart + hexagon.Add(new PointD( + 50 + 40 * Math.Cos(angle), + 50 + 40 * Math.Sin(angle) + )); + } + hexagon.Add(hexagon[0]); // Close the path + + var result = contourGen.makeContour( + hexagon, + concaveRadius: 0, + convexRadius: 50, + edgeResolution: 1.0, + angularResolution: 0.1, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + CreateSvgOutput("hexagon_large_radius.svg", hexagon, result, "Hexagon with large radius (50) - should be circular"); + AnalyzeCircularity(result, "Hexagon large radius"); + } + + static void CreateSvgOutput(string filename, PathD original, PathD result, string title) + { + var svg = new System.Text.StringBuilder(); + + // Find bounds for both paths + double minX = Math.Min(original.Min(p => p.x), result.Min(p => p.x)) - 20; + double minY = Math.Min(original.Min(p => p.y), result.Min(p => p.y)) - 20; + double maxX = Math.Max(original.Max(p => p.x), result.Max(p => p.x)) + 20; + double maxY = Math.Max(original.Max(p => p.y), result.Max(p => p.y)) + 20; + + double width = maxX - minX; + double height = maxY - minY; + + svg.AppendLine($""); + svg.AppendLine($""); + svg.AppendLine($"{title}"); + + // Add original shape in light gray + svg.Append(""); + + // Add result shape in blue + svg.Append(""); + + // Add center point for analysis + if (result.Count > 0) + { + double centerX = result.Average(p => p.x); + double centerY = result.Average(p => p.y); + svg.AppendLine($""); + } + + svg.AppendLine(""); + + File.WriteAllText(filename, svg.ToString()); + } + + static void AnalyzeCircularity(PathD path, string description) + { + if (path.Count < 3) + { + Console.WriteLine($" {description}: Too few points to analyze circularity"); + return; + } + + // Find center by averaging all points + double centerX = path.Average(p => p.x); + double centerY = path.Average(p => p.y); + var center = new PointD(centerX, centerY); + + // Calculate distances from center + var distances = path.Select(p => Helper.Length(Helper.Minus(p, center))).ToArray(); + + double avgRadius = distances.Average(); + double minRadius = distances.Min(); + double maxRadius = distances.Max(); + double radiusVariation = (maxRadius - minRadius) / avgRadius; + + Console.WriteLine($" Points: {path.Count}"); + Console.WriteLine($" Center: ({centerX:F2}, {centerY:F2})"); + Console.WriteLine($" Average radius: {avgRadius:F2}"); + Console.WriteLine($" Radius variation: {radiusVariation:F4} ({radiusVariation * 100:F2}%)"); + Console.WriteLine($" Circularity score: {1 - radiusVariation:F4} (1.0 = perfect circle)"); + } + } +} \ No newline at end of file diff --git a/Prototyping/test_circle_convergence/demo.html b/Prototyping/test_circle_convergence/demo.html new file mode 100644 index 00000000..33ca9b92 --- /dev/null +++ b/Prototyping/test_circle_convergence/demo.html @@ -0,0 +1,51 @@ + + + + ContourGen Circular Convergence Results + + + +

ContourGen Circular Convergence Fix

+

Demonstration of the algorithm detecting when large corner radii should produce circular results.

+ +

Square with Small vs Large Radius

+
+
+

Small Radius (10)

+

Normal rounded corners

+ + Square with small radius (10) + + + + 1033 points + +
+
+

Large Radius (60)

+

Circular convergence detected!

+ + Square with large radius (60) - circular result + + + + 64 points (circular) + +
+
+ +

Algorithm Benefits

+ + + \ No newline at end of file diff --git a/Prototyping/test_circle_convergence/hexagon_large_radius.svg b/Prototyping/test_circle_convergence/hexagon_large_radius.svg new file mode 100644 index 00000000..3b3d3e97 --- /dev/null +++ b/Prototyping/test_circle_convergence/hexagon_large_radius.svg @@ -0,0 +1,7 @@ + + +Hexagon with large radius (50) - should be circular + + + + diff --git a/Prototyping/test_circle_convergence/rectangle_large_radius.svg b/Prototyping/test_circle_convergence/rectangle_large_radius.svg new file mode 100644 index 00000000..1f6dff59 --- /dev/null +++ b/Prototyping/test_circle_convergence/rectangle_large_radius.svg @@ -0,0 +1,7 @@ + + +Rectangle with large radius (40) - should be circular + + + + diff --git a/Prototyping/test_circle_convergence/square_large_radius.svg b/Prototyping/test_circle_convergence/square_large_radius.svg new file mode 100644 index 00000000..f93b9e48 --- /dev/null +++ b/Prototyping/test_circle_convergence/square_large_radius.svg @@ -0,0 +1,7 @@ + + +Square with large radius (60) - should be circular + + + + diff --git a/Prototyping/test_circle_convergence/square_small_radius.svg b/Prototyping/test_circle_convergence/square_small_radius.svg new file mode 100644 index 00000000..3aae2f96 --- /dev/null +++ b/Prototyping/test_circle_convergence/square_small_radius.svg @@ -0,0 +1,7 @@ + + +Square with small radius (10) + + + + diff --git a/Prototyping/test_circle_convergence/test_circle_convergence.csproj b/Prototyping/test_circle_convergence/test_circle_convergence.csproj new file mode 100644 index 00000000..93026059 --- /dev/null +++ b/Prototyping/test_circle_convergence/test_circle_convergence.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + test_circle_convergence + true + + + + + + + + \ No newline at end of file diff --git a/UnitTests/CircularConvergenceTests.cs b/UnitTests/CircularConvergenceTests.cs new file mode 100644 index 00000000..a59ae63b --- /dev/null +++ b/UnitTests/CircularConvergenceTests.cs @@ -0,0 +1,209 @@ +using NUnit.Framework; +using Clipper2Lib; +using shapeEngine; +using System; +using System.Linq; + +namespace UnitTests +{ + [TestFixture] + public class CircularConvergenceTests + { + [Test] + public void SquareWithLargeRadius_ConvergesToCircle() + { + // Arrange + var square = new PathD + { + new PointD(0, 0), + new PointD(100, 0), + new PointD(100, 100), + new PointD(0, 100), + new PointD(0, 0) + }; + + // Act - Use radius larger than half-edge distance (50) + // Per-corner logic: each corner should use circular arc since 60 >= 50 + var result = contourGen.makeContour( + square, + concaveRadius: 0, + convexRadius: 60, + edgeResolution: 2.0, + angularResolution: 0.2, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + // Assert - With per-corner circular convergence, we expect: + // 1. Fewer points than normal bezier processing + // 2. Circular arc segments at corners, but not a perfect overall circle + Assert.That(result.Count, Is.LessThan(200), + "Per-corner circular convergence should produce fewer points than normal bezier"); + + Assert.That(result.Count, Is.GreaterThan(3), + "Should have more points than empty result"); + } + + [Test] + public void SquareWithSmallRadius_DoesNotConvergeToCircle() + { + // Arrange + var square = new PathD + { + new PointD(0, 0), + new PointD(100, 0), + new PointD(100, 100), + new PointD(0, 100), + new PointD(0, 0) + }; + + // Act - Use radius smaller than convergence threshold + var result = contourGen.makeContour( + square, + concaveRadius: 0, + convexRadius: 20, + edgeResolution: 2.0, + angularResolution: 0.2, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + // Assert - Should NOT be circular (should have more variation) + var circularity = CalculateCircularityScore(result); + Assert.That(circularity, Is.LessThan(0.98), + "Small radius should not produce perfect circular result"); + + // Should produce more points than the circular case + Assert.That(result.Count, Is.GreaterThan(200), + "Non-circular case should produce more points"); + } + + [Test] + public void RectangleWithLargeRadius_MixedProcessing() + { + // Arrange + var rectangle = new PathD + { + new PointD(0, 0), + new PointD(120, 0), + new PointD(120, 60), + new PointD(0, 60), + new PointD(0, 0) + }; + + // Act - Use radius 40, which is: + // - Less than width half-edge (60), so width corners should use bezier + // - Greater than height half-edge (30), so height corners should use circular arcs + var result = contourGen.makeContour( + rectangle, + concaveRadius: 0, + convexRadius: 40, + edgeResolution: 2.0, + angularResolution: 0.2, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + // Assert - This should demonstrate mixed processing + // Some corners use circular arcs, others use bezier curves + Assert.That(result.Count, Is.GreaterThan(3), + "Mixed processing should produce some points"); + Assert.That(result.Count, Is.LessThan(500), + "Should be more efficient than full bezier processing"); + } + + [Test] + public void ConcavePolygon_DoesNotConvergeToCircle() + { + // Arrange - Create a concave L-shape + var lShape = new PathD + { + new PointD(0, 0), + new PointD(100, 0), + new PointD(100, 50), + new PointD(50, 50), + new PointD(50, 100), + new PointD(0, 100), + new PointD(0, 0) + }; + + // Act - Use large radius + var result = contourGen.makeContour( + lShape, + concaveRadius: 0, + convexRadius: 60, + edgeResolution: 2.0, + angularResolution: 0.2, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + // Assert - Should NOT be circular because it's concave + var circularity = CalculateCircularityScore(result); + Assert.That(circularity, Is.LessThan(0.95), + "Concave shapes should not converge to circles even with large radius"); + } + + [Test] + public void ComplexPolygon_DoesNotConvergeToCircle() + { + // Arrange - Create a polygon with many corners (should exceed the 8-corner limit) + var star = new PathD(); + int points = 10; // 10-pointed star = 20 corners, exceeds our 8-corner limit + for (int i = 0; i < points * 2; i++) + { + double angle = i * Math.PI / points; + double radius = (i % 2 == 0) ? 50 : 25; // Alternating outer/inner radius + star.Add(new PointD( + 50 + radius * Math.Cos(angle), + 50 + radius * Math.Sin(angle) + )); + } + star.Add(star[0]); // Close the path + + // Act - Use large radius + var result = contourGen.makeContour( + star, + concaveRadius: 0, + convexRadius: 40, + edgeResolution: 2.0, + angularResolution: 0.2, + shortEdgeLength: 5, + maxShortEdgeLength: 10, + optimizeCorners: 0); + + // Assert - Should NOT be circular because it has too many corners + var circularity = CalculateCircularityScore(result); + Assert.That(circularity, Is.LessThan(0.95), + "Complex polygons with many corners should not converge to circles"); + } + + private void AssertCircularity(PathD path, double maxVariation, string description) + { + var circularity = CalculateCircularityScore(path); + Assert.That(circularity, Is.GreaterThan(1.0 - maxVariation), + $"{description} should be circular (circularity score: {circularity:F4})"); + } + + private double CalculateCircularityScore(PathD path) + { + if (path.Count < 3) + return 0.0; + + // Find center by averaging all points + double centerX = path.Average(p => p.x); + double centerY = path.Average(p => p.y); + var center = new PointD(centerX, centerY); + + // Calculate distances from center + var distances = path.Select(p => Helper.Length(Helper.Minus(p, center))).ToArray(); + + double avgRadius = distances.Average(); + double minRadius = distances.Min(); + double maxRadius = distances.Max(); + double radiusVariation = (maxRadius - minRadius) / avgRadius; + + return 1.0 - radiusVariation; // 1.0 = perfect circle, lower values = less circular + } + } +} \ No newline at end of file diff --git a/hexagon_large_radius.svg b/hexagon_large_radius.svg new file mode 100644 index 00000000..7a9e4cbe --- /dev/null +++ b/hexagon_large_radius.svg @@ -0,0 +1,7 @@ + + +Hexagon with large radius (50) - should be circular + + + + diff --git a/rectangle_large_radius.svg b/rectangle_large_radius.svg new file mode 100644 index 00000000..bab30af9 --- /dev/null +++ b/rectangle_large_radius.svg @@ -0,0 +1,7 @@ + + +Rectangle with large radius (40) - should be circular + + + + diff --git a/square_large_radius.svg b/square_large_radius.svg new file mode 100644 index 00000000..a8f4001b --- /dev/null +++ b/square_large_radius.svg @@ -0,0 +1,7 @@ + + +Square with large radius (60) - should be circular + + + + diff --git a/square_small_radius.svg b/square_small_radius.svg new file mode 100644 index 00000000..3aae2f96 --- /dev/null +++ b/square_small_radius.svg @@ -0,0 +1,7 @@ + + +Square with small radius (10) + + + +