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:
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user