fix: account for contour winding direction in lead-in normal computation

ComputeNormal assumed CW winding for all contours. For CCW-wound cutouts,
line normals pointed to the material side instead of scrap, placing lead-ins
on the wrong side. Now accepts a winding parameter: lines flip the normal
for CCW winding, and arcs flip when arc direction differs from contour
winding (concave feature detection).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 12:06:08 -04:00
parent ec0baad585
commit 64945220b9
2 changed files with 20 additions and 13 deletions

View File

@@ -70,8 +70,8 @@ namespace OpenNest.CNC.CuttingStrategy
private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null) private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null)
{ {
var contourType = forceType ?? DetectContourType(shape); var contourType = forceType ?? DetectContourType(shape);
var normal = ComputeNormal(point, entity, contourType);
var winding = DetermineWinding(shape); var winding = DetermineWinding(shape);
var normal = ComputeNormal(point, entity, contourType, winding);
var leadIn = SelectLeadIn(contourType); var leadIn = SelectLeadIn(contourType);
var leadOut = SelectLeadOut(contourType); var leadOut = SelectLeadOut(contourType);
@@ -143,29 +143,33 @@ namespace OpenNest.CNC.CuttingStrategy
return ContourType.Internal; return ContourType.Internal;
} }
public static double ComputeNormal(Vector point, Entity entity, ContourType contourType) public static double ComputeNormal(Vector point, Entity entity, ContourType contourType,
RotationType winding = RotationType.CW)
{ {
double normal; double normal;
if (entity is Line line) if (entity is Line line)
{ {
// Perpendicular to line direction // Perpendicular to line direction: tangent + π/2 = left side.
// Left side = outward for CW winding; for CCW winding, outward
// is on the right side, so flip.
var tangent = line.EndPoint.AngleFrom(line.StartPoint); var tangent = line.EndPoint.AngleFrom(line.StartPoint);
normal = tangent + Math.Angle.HalfPI; normal = tangent + Math.Angle.HalfPI;
if (winding == RotationType.CCW)
normal += System.Math.PI;
} }
else if (entity is Arc arc) else if (entity is Arc arc)
{ {
// Radial direction from center to point // Radial direction from center to point.
// Flip when the arc direction differs from the contour winding —
// that indicates a concave feature where radial points inward.
normal = point.AngleFrom(arc.Center); normal = point.AngleFrom(arc.Center);
if (arc.Rotation != winding)
// For CCW arcs the radial points the wrong way — flip it.
// CW arcs are convex features (corners) where radial = outward.
// CCW arcs are concave features (slots) where radial = inward.
if (arc.Rotation == RotationType.CCW)
normal += System.Math.PI; normal += System.Math.PI;
} }
else if (entity is Circle circle) else if (entity is Circle circle)
{ {
// Radial outward — always correct regardless of winding
normal = point.AngleFrom(circle.Center); normal = point.AngleFrom(circle.Center);
} }
else else

View File

@@ -102,7 +102,7 @@ namespace OpenNest.Actions
snapPoint = closest; snapPoint = closest;
snapEntity = entity; snapEntity = entity;
snapContourType = info.ContourType; snapContourType = info.ContourType;
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType); snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType, info.Winding);
hasSnap = true; hasSnap = true;
hoveredContour = info; hoveredContour = info;
} }
@@ -282,7 +282,7 @@ namespace OpenNest.Actions
{ {
snapPoint = bestPoint; snapPoint = bestPoint;
snapEntity = bestEntity; snapEntity = bestEntity;
snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType); snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType, hoveredContour.Winding);
activeSnapType = bestType; activeSnapType = bestType;
} }
@@ -356,7 +356,8 @@ namespace OpenNest.Actions
contours.Add(new ShapeInfo contours.Add(new ShapeInfo
{ {
Shape = profile.Perimeter, Shape = profile.Perimeter,
ContourType = ContourType.External ContourType = ContourType.External,
Winding = ContourCuttingStrategy.DetermineWinding(profile.Perimeter)
}); });
} }
@@ -366,7 +367,8 @@ namespace OpenNest.Actions
contours.Add(new ShapeInfo contours.Add(new ShapeInfo
{ {
Shape = cutout, Shape = cutout,
ContourType = ContourCuttingStrategy.DetectContourType(cutout) ContourType = ContourCuttingStrategy.DetectContourType(cutout),
Winding = ContourCuttingStrategy.DetermineWinding(cutout)
}); });
} }
} }
@@ -483,6 +485,7 @@ namespace OpenNest.Actions
{ {
public Shape Shape { get; set; } public Shape Shape { get; set; }
public ContourType ContourType { get; set; } public ContourType ContourType { get; set; }
public RotationType Winding { get; set; }
} }
} }
} }