diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
index 1bbd239..dfcd375 100644
--- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
+++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
@@ -636,7 +636,7 @@ namespace OpenNest.CNC.CuttingStrategy
}
}
- private enum CornerKind
+ internal enum CornerKind
{
Convex,
Reflex,
@@ -644,6 +644,38 @@ namespace OpenNest.CNC.CuttingStrategy
Cusp,
}
+ ///
+ /// A vertex classified for automatic start-point planning: what kind of turn the
+ /// contour makes there, and the travel tangents of the two edges it joins. Read-only
+ /// so callers cannot mutate the contour; winding is derived the same way
+ /// derives it, so the kind matches actual emission.
+ ///
+ internal readonly record struct AutomaticCorner(CornerKind Kind, Vector TangentIn, Vector TangentOut);
+
+ ///
+ /// Classification query shared with entry planning: the turn at
+ /// on a closed line/arc contour, using the same corner geometry and winding derivation
+ /// as emission. False when is not a shared vertex of two
+ /// chainable entities (an interior point, an open contour, a degenerate or non-finite
+ /// corner). Lead generation itself is not involved.
+ ///
+ internal static bool TryClassifyAutomaticStartCorner(
+ Shape shape,
+ Vector point,
+ Entity entity,
+ out AutomaticCorner corner)
+ {
+ if (!TryGetCorner(shape, point, entity, out var raw))
+ {
+ corner = default;
+ return false;
+ }
+
+ corner = new AutomaticCorner(
+ ClassifyCorner(raw, DetermineWinding(shape)), raw.TangentIn, raw.TangentOut);
+ return true;
+ }
+
/// A contour vertex: the entity cut into it and the one cut away from it.
private readonly record struct ContourCorner(
Entity Incoming,
diff --git a/OpenNest.Tests/CuttingStrategy/AutomaticCornerClassificationTests.cs b/OpenNest.Tests/CuttingStrategy/AutomaticCornerClassificationTests.cs
new file mode 100644
index 0000000..989a571
--- /dev/null
+++ b/OpenNest.Tests/CuttingStrategy/AutomaticCornerClassificationTests.cs
@@ -0,0 +1,270 @@
+using OpenNest.CNC.CuttingStrategy;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests.CuttingStrategy;
+
+///
+/// The shared classification query for automatic start-point planning reuses the emitter's
+/// own corner geometry and winding derivation: convex, reflex, smooth and cusp vertices
+/// classify identically from either adjacent entity, in both windings, and rotations of the
+/// shape do not change the kind. Midpoints are not corners.
+///
+public class AutomaticCornerClassificationTests
+{
+ private const double ToleranceDegrees = 1e-6;
+
+ // --- fixtures -------------------------------------------------------------
+
+ private static Shape ClosedShape(params Entity[] entities)
+ {
+ var shape = new Shape();
+ shape.Entities.AddRange(entities);
+ Assert.True(shape.IsClosed());
+ return shape;
+ }
+
+ private static Shape Square(Vector[] corners)
+ {
+ var e = new Entity[corners.Length];
+ for (var i = 0; i < corners.Length; i++)
+ e[i] = new Line(corners[i], corners[(i + 1) % corners.Length]);
+ return ClosedShape(e);
+ }
+
+ /// CCW square; the shape's own winding derivation says so.
+ private static Shape CcwSquare(double size = 10) =>
+ Square(new[]
+ {
+ new Vector(0, 0), new Vector(size, 0), new Vector(size, size), new Vector(0, size),
+ });
+
+ /// Same square traversed CW.
+ private static Shape CwSquare(double size = 10)
+ {
+ var s = CcwSquare(size);
+ s.Reverse();
+ return s;
+ }
+
+ /// CCW L-shape: the concave vertex (5,5) turns right — reflex.
+ private static Shape Notched()
+ {
+ return Square(new[]
+ {
+ new Vector(0, 0), new Vector(10, 0), new Vector(10, 10), new Vector(5, 10),
+ new Vector(5, 5), new Vector(0, 5),
+ });
+ }
+
+ /// CCW square whose top side is a half-circle bump; the tangent joints are at (0,10) and (10,10).
+ private static Shape ArcBumpSquare()
+ {
+ return ClosedShape(
+ new Line(new Vector(0, 0), new Vector(10, 0)),
+ new Line(new Vector(10, 0), new Vector(10, 10)),
+ new Arc(new Vector(5, 10), 5, 0, System.Math.PI),
+ new Line(new Vector(0, 10), new Vector(0, 0)));
+ }
+
+ // --- positive cases --------------------------------------------------------
+
+ public static IEnumerable