diff --git a/OpenNest.Core/Geometry/Arc.cs b/OpenNest.Core/Geometry/Arc.cs
index 002cd09..1982bce 100644
--- a/OpenNest.Core/Geometry/Arc.cs
+++ b/OpenNest.Core/Geometry/Arc.cs
@@ -155,6 +155,17 @@ namespace OpenNest.Geometry
Center.Y + Radius * System.Math.Sin(EndAngle));
}
+ ///
+ /// Mid point of the arc (point at the angle midway between start and end).
+ ///
+ public Vector MidPoint()
+ {
+ var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2);
+ return new Vector(
+ Center.X + Radius * System.Math.Cos(midAngle),
+ Center.Y + Radius * System.Math.Sin(midAngle));
+ }
+
///
/// Splits the arc at the given point, returning two sub-arcs.
/// Either half may be null if the split point coincides with an endpoint.
diff --git a/OpenNest/Actions/ActionLeadIn.cs b/OpenNest/Actions/ActionLeadIn.cs
index 6d5addc..b23864b 100644
--- a/OpenNest/Actions/ActionLeadIn.cs
+++ b/OpenNest/Actions/ActionLeadIn.cs
@@ -14,6 +14,10 @@ namespace OpenNest.Actions
[DisplayName("Place Lead-in")]
public class ActionLeadIn : Action
{
+ private enum SnapType { None, Endpoint, Midpoint }
+
+ private const double SnapCapturePixels = 10.0;
+
private LayoutPart selectedLayoutPart;
private Part selectedPart;
private ShapeProfile profile;
@@ -23,6 +27,7 @@ namespace OpenNest.Actions
private ContourType snapContourType;
private double snapNormal;
private bool hasSnap;
+ private SnapType activeSnapType;
private ShapeInfo hoveredContour;
private ContextMenuStrip contextMenu;
private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180));
@@ -57,6 +62,7 @@ namespace OpenNest.Actions
profile = null;
contours = null;
hasSnap = false;
+ activeSnapType = SnapType.None;
hoveredContour = null;
plateView.Invalidate();
}
@@ -81,6 +87,7 @@ namespace OpenNest.Actions
// Find closest contour and point
var bestDist = double.MaxValue;
hasSnap = false;
+ activeSnapType = SnapType.None;
hoveredContour = null;
foreach (var info in contours)
@@ -100,6 +107,10 @@ namespace OpenNest.Actions
}
}
+ // Check endpoint/midpoint snaps on the hovered contour
+ if (hoveredContour != null)
+ TrySnapToEntityPoints(localPt);
+
plateView.Invalidate();
}
@@ -142,33 +153,39 @@ namespace OpenNest.Actions
{
var g = e.Graphics;
- // Gray overlay on all parts except the selected one
+ DrawOverlay(g);
+ DrawHoveredContour(g);
+ DrawLeadInPreview(g);
+ }
+
+ private void DrawOverlay(Graphics g)
+ {
foreach (var lp in plateView.LayoutParts)
{
- if (lp == selectedLayoutPart)
- continue;
-
- if (lp.Path != null)
+ if (lp != selectedLayoutPart && lp.Path != null)
g.FillPath(grayOverlay, lp.Path);
}
+ }
- // Highlight the hovered contour
- if (hoveredContour != null && selectedPart != null)
- {
- using var contourPath = hoveredContour.Shape.GetGraphicsPath();
+ private void DrawHoveredContour(Graphics g)
+ {
+ if (hoveredContour == null || selectedPart == null)
+ return;
- // Translate from local part space to world space, then apply view transform
- using var contourMatrix = new Matrix();
- contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
- contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
- contourPath.Transform(contourMatrix);
+ using var contourPath = hoveredContour.Shape.GetGraphicsPath();
+ using var contourMatrix = new Matrix();
+ contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
+ contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
+ contourPath.Transform(contourMatrix);
- var prevSmooth = g.SmoothingMode;
- g.SmoothingMode = SmoothingMode.AntiAlias;
- g.DrawPath(highlightPen, contourPath);
- g.SmoothingMode = prevSmooth;
- }
+ var prevSmooth = g.SmoothingMode;
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.DrawPath(highlightPen, contourPath);
+ g.SmoothingMode = prevSmooth;
+ }
+ private void DrawLeadInPreview(Graphics g)
+ {
if (!hasSnap || selectedPart == null)
return;
@@ -176,67 +193,113 @@ namespace OpenNest.Actions
if (parameters == null)
return;
- // Transform snap point from local part space to world space
- var worldSnap = TransformToWorld(snapPoint);
-
- // Get the appropriate lead-in for this contour type
var leadIn = SelectLeadIn(parameters, snapContourType);
if (leadIn == null)
return;
- // Clamp lead-in for circle contours so it stays inside the hole
- if (snapContourType == ContourType.ArcCircle && snapEntity is Circle snapCircle
- && parameters.PierceClearance > 0)
- {
- var pierceCheck = leadIn.GetPiercePoint(snapPoint, snapNormal);
- var distFromCenter = pierceCheck.DistanceTo(snapCircle.Center);
- var maxRadius = snapCircle.Radius - parameters.PierceClearance;
- if (maxRadius > 0 && distFromCenter > maxRadius)
- {
- var currentDist = snapPoint.DistanceTo(pierceCheck);
- if (currentDist > Tolerance.Epsilon)
- {
- var dx = (pierceCheck.X - snapPoint.X) / currentDist;
- var dy = (pierceCheck.Y - snapPoint.Y) / currentDist;
- var vx = snapPoint.X - snapCircle.Center.X;
- var vy = snapPoint.Y - snapCircle.Center.Y;
- var b = 2.0 * (vx * dx + vy * dy);
- var c = vx * vx + vy * vy - maxRadius * maxRadius;
- var disc = b * b - 4.0 * c;
- if (disc >= 0)
- {
- var t = (-b + System.Math.Sqrt(disc)) / 2.0;
- if (t > 0 && t < currentDist)
- leadIn = leadIn.Scale(t / currentDist);
- }
- }
- }
- }
+ leadIn = ClampLeadInForCircle(leadIn, parameters);
- // Get the pierce point (in local space)
var piercePoint = leadIn.GetPiercePoint(snapPoint, snapNormal);
- var worldPierce = TransformToWorld(piercePoint);
+ var pt1 = plateView.PointWorldToGraph(TransformToWorld(piercePoint));
+ var pt2 = plateView.PointWorldToGraph(TransformToWorld(snapPoint));
var oldSmooth = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
- // Draw the lead-in preview as a line from pierce point to contour point
- var pt1 = plateView.PointWorldToGraph(worldPierce);
- var pt2 = plateView.PointWorldToGraph(worldSnap);
-
using var previewPen = new Pen(Color.Magenta, 2.0f / plateView.ViewScale);
g.DrawLine(previewPen, pt1, pt2);
- // Draw a small circle at the pierce point
var radius = 3.0f / plateView.ViewScale;
g.FillEllipse(Brushes.Magenta, pt1.X - radius, pt1.Y - radius, radius * 2, radius * 2);
- // Draw a small circle at the contour start point
- g.FillEllipse(Brushes.Lime, pt2.X - radius, pt2.Y - radius, radius * 2, radius * 2);
+ if (activeSnapType != SnapType.None)
+ DrawSnapMarker(g, pt2, activeSnapType);
+ else
+ g.FillEllipse(Brushes.Lime, pt2.X - radius, pt2.Y - radius, radius * 2, radius * 2);
g.SmoothingMode = oldSmooth;
}
+ private LeadIn ClampLeadInForCircle(LeadIn leadIn, CuttingParameters parameters)
+ {
+ if (snapContourType != ContourType.ArcCircle
+ || !(snapEntity is Circle snapCircle)
+ || parameters.PierceClearance <= 0)
+ return leadIn;
+
+ var pierceCheck = leadIn.GetPiercePoint(snapPoint, snapNormal);
+ var maxRadius = snapCircle.Radius - parameters.PierceClearance;
+
+ if (maxRadius <= 0 || pierceCheck.DistanceTo(snapCircle.Center) <= maxRadius)
+ return leadIn;
+
+ var currentDist = snapPoint.DistanceTo(pierceCheck);
+ if (currentDist <= Tolerance.Epsilon)
+ return leadIn;
+
+ var dx = (pierceCheck.X - snapPoint.X) / currentDist;
+ var dy = (pierceCheck.Y - snapPoint.Y) / currentDist;
+ var vx = snapPoint.X - snapCircle.Center.X;
+ var vy = snapPoint.Y - snapCircle.Center.Y;
+ var b = 2.0 * (vx * dx + vy * dy);
+ var c = vx * vx + vy * vy - maxRadius * maxRadius;
+ var disc = b * b - 4.0 * c;
+
+ if (disc < 0)
+ return leadIn;
+
+ var t = (-b + System.Math.Sqrt(disc)) / 2.0;
+ return (t > 0 && t < currentDist) ? leadIn.Scale(t / currentDist) : leadIn;
+ }
+
+ private void TrySnapToEntityPoints(Vector localPt)
+ {
+ var captureRadius = SnapCapturePixels / plateView.ViewScale;
+ var bestDist = captureRadius;
+ var bestPoint = default(Vector);
+ var bestEntity = default(Entity);
+ var bestType = SnapType.None;
+
+ foreach (var entity in hoveredContour.Shape.Entities)
+ {
+ switch (entity)
+ {
+ case Line line:
+ TryCandidate(line.StartPoint, line, SnapType.Endpoint);
+ TryCandidate(line.EndPoint, line, SnapType.Endpoint);
+ TryCandidate(line.MidPoint, line, SnapType.Midpoint);
+ break;
+ case Arc arc:
+ TryCandidate(arc.StartPoint(), arc, SnapType.Endpoint);
+ TryCandidate(arc.EndPoint(), arc, SnapType.Endpoint);
+ TryCandidate(arc.MidPoint(), arc, SnapType.Midpoint);
+ break;
+ }
+ }
+
+ if (bestType != SnapType.None)
+ {
+ snapPoint = bestPoint;
+ snapEntity = bestEntity;
+ snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType);
+ activeSnapType = bestType;
+ }
+
+ return;
+
+ void TryCandidate(Vector pt, Entity ent, SnapType type)
+ {
+ var dist = pt.DistanceTo(localPt);
+ if (dist < bestDist)
+ {
+ bestDist = dist;
+ bestPoint = pt;
+ bestEntity = ent;
+ bestType = type;
+ }
+ }
+ }
+
private void SelectPartAtCursor()
{
var layoutPart = plateView.GetPartAtPoint(plateView.CurrentPoint);
@@ -342,6 +405,7 @@ namespace OpenNest.Actions
profile = null;
contours = null;
hasSnap = false;
+ activeSnapType = SnapType.None;
hoveredContour = null;
plateView.Invalidate();
}
@@ -365,6 +429,35 @@ namespace OpenNest.Actions
contextMenu.Show(plateView, location);
}
+ private void DrawSnapMarker(Graphics g, PointF pt, SnapType type)
+ {
+ var size = 5f;
+
+ if (type == SnapType.Endpoint)
+ {
+ // Diamond
+ var points = new[]
+ {
+ new PointF(pt.X, pt.Y - size),
+ new PointF(pt.X + size, pt.Y),
+ new PointF(pt.X, pt.Y + size),
+ new PointF(pt.X - size, pt.Y)
+ };
+ g.FillPolygon(Brushes.Red, points);
+ }
+ else if (type == SnapType.Midpoint)
+ {
+ // Triangle
+ var points = new[]
+ {
+ new PointF(pt.X, pt.Y - size),
+ new PointF(pt.X + size, pt.Y + size),
+ new PointF(pt.X - size, pt.Y + size)
+ };
+ g.FillPolygon(Brushes.Red, points);
+ }
+ }
+
private Vector TransformToWorld(Vector localPt)
{
// The contours are already in rotated local space (we rotated the program