Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 262 additions & 6 deletions Common/shapeEngine/contourGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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;
}

/// <summary>
/// Attempts to generate a circular contour when the convex radius is large enough
/// that all corners would merge into a circular result.
/// </summary>
/// <param name="original_path">The original polygon path</param>
/// <param name="convexRadius">The convex corner radius</param>
/// <param name="edgeResolution">Edge resolution for circle sampling</param>
/// <param name="angularResolution">Angular resolution for circle sampling</param>
/// <returns>Circular path if conditions are met, null otherwise</returns>
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<double>();

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

/// <summary>
/// Checks if a polygon is convex by examining the cross product of consecutive edge vectors.
/// </summary>
/// <param name="polygon">The polygon path to check</param>
/// <returns>True if the polygon is convex, false otherwise</returns>
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
}

/// <summary>
/// Generates a circular path with the specified center, radius and angular resolution.
/// </summary>
/// <param name="center">Center point of the circle</param>
/// <param name="radius">Radius of the circle</param>
/// <param name="angularResolution">Angular resolution in radians</param>
/// <returns>PathD representing the circle</returns>
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;
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
/// 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.
/// </summary>
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;
}
}
Loading