From 64945220b9712ceae88c20f7237951cbce57c654 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 2 Apr 2026 12:06:08 -0400 Subject: [PATCH] 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) --- .../CuttingStrategy/ContourCuttingStrategy.cs | 22 +++++++++++-------- OpenNest/Actions/ActionLeadIn.cs | 11 ++++++---- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs index eef0d86..580add9 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs @@ -70,8 +70,8 @@ namespace OpenNest.CNC.CuttingStrategy private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null) { var contourType = forceType ?? DetectContourType(shape); - var normal = ComputeNormal(point, entity, contourType); var winding = DetermineWinding(shape); + var normal = ComputeNormal(point, entity, contourType, winding); var leadIn = SelectLeadIn(contourType); var leadOut = SelectLeadOut(contourType); @@ -143,29 +143,33 @@ namespace OpenNest.CNC.CuttingStrategy 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; 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); normal = tangent + Math.Angle.HalfPI; + if (winding == RotationType.CCW) + normal += System.Math.PI; } 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); - - // 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) + if (arc.Rotation != winding) normal += System.Math.PI; } else if (entity is Circle circle) { + // Radial outward — always correct regardless of winding normal = point.AngleFrom(circle.Center); } else diff --git a/OpenNest/Actions/ActionLeadIn.cs b/OpenNest/Actions/ActionLeadIn.cs index dbb7cbf..1788559 100644 --- a/OpenNest/Actions/ActionLeadIn.cs +++ b/OpenNest/Actions/ActionLeadIn.cs @@ -102,7 +102,7 @@ namespace OpenNest.Actions snapPoint = closest; snapEntity = entity; snapContourType = info.ContourType; - snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType); + snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType, info.Winding); hasSnap = true; hoveredContour = info; } @@ -282,7 +282,7 @@ namespace OpenNest.Actions { snapPoint = bestPoint; snapEntity = bestEntity; - snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType); + snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType, hoveredContour.Winding); activeSnapType = bestType; } @@ -356,7 +356,8 @@ namespace OpenNest.Actions contours.Add(new ShapeInfo { 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 { 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 ContourType ContourType { get; set; } + public RotationType Winding { get; set; } } } }