From da8e7e6fd312a5e020b1f18c64066364bffbc02b Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 22 Mar 2026 23:49:27 -0400 Subject: [PATCH] feat: interactive cut-off selection and drag via line hit-testing Select cut-offs by clicking their lines instead of a grip point. Drag is axis-constrained with live regeneration during movement. Selected cut-off highlighted with bright blue 3.5px line. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest/Controls/PlateView.cs | 53 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 3cfc16e..42c80b5 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -33,6 +33,7 @@ namespace OpenNest.Controls private CutOffSettings cutOffSettings = new CutOffSettings(); private CutOff selectedCutOff; private bool draggingCutOff; + private Dictionary dragPerimeterCache; protected List parts; private List stationaryParts = new List(); private List activeParts = new List(); @@ -242,6 +243,7 @@ namespace OpenNest.Controls { SelectedCutOff = hitCutOff; draggingCutOff = true; + dragPerimeterCache = Plate.BuildPerimeterCache(Plate); return; } else @@ -270,6 +272,7 @@ namespace OpenNest.Controls if (draggingCutOff && selectedCutOff != null) { draggingCutOff = false; + dragPerimeterCache = null; Plate.RegenerateCutOffs(cutOffSettings); Invalidate(); return; @@ -333,7 +336,12 @@ namespace OpenNest.Controls if (draggingCutOff && selectedCutOff != null) { - selectedCutOff.Position = CurrentPoint; + if (selectedCutOff.Axis == CutOffAxis.Vertical) + selectedCutOff.Position = new Vector(CurrentPoint.X, selectedCutOff.Position.Y); + else + selectedCutOff.Position = new Vector(selectedCutOff.Position.X, CurrentPoint.Y); + + selectedCutOff.Regenerate(Plate, cutOffSettings, dragPerimeterCache); Invalidate(); return; } @@ -455,7 +463,6 @@ namespace OpenNest.Controls DrawPlate(e.Graphics); DrawParts(e.Graphics); DrawCutOffs(e.Graphics); - DrawCutOffGrip(e.Graphics); DrawActiveWorkArea(e.Graphics); DrawDebugRemnants(e.Graphics); @@ -612,7 +619,8 @@ namespace OpenNest.Controls if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0) return; - using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f / ViewScale); + using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f); + using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f); foreach (var cutoff in Plate.CutOffs) { @@ -620,35 +628,19 @@ namespace OpenNest.Controls if (program == null || program.Codes.Count == 0) continue; + var activePen = cutoff == selectedCutOff ? selectedPen : pen; + for (var i = 0; i < program.Codes.Count - 1; i += 2) { if (program.Codes[i] is RapidMove rapid && program.Codes[i + 1] is LinearMove linear) { - DrawLine(g, rapid.EndPoint, linear.EndPoint, pen); + DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen); } } } } - private void DrawCutOffGrip(Graphics g) - { - if (selectedCutOff == null) - return; - - var radius = 4f / ViewScale; - var pos = selectedCutOff.Position; - var graphPt = PointWorldToGraph(pos); - var scaledRadius = LengthWorldToGui(radius); - - g.FillEllipse(Brushes.DarkGray, - graphPt.X - scaledRadius, graphPt.Y - scaledRadius, - scaledRadius * 2, scaledRadius * 2); - g.DrawEllipse(Pens.Black, - graphPt.X - scaledRadius, graphPt.Y - scaledRadius, - scaledRadius * 2, scaledRadius * 2); - } - public CutOff GetCutOffAtPoint(Vector point, double tolerance) { if (Plate?.CutOffs == null) @@ -656,9 +648,20 @@ namespace OpenNest.Controls foreach (var cutoff in Plate.CutOffs) { - var dist = cutoff.Position.DistanceTo(point); - if (dist <= tolerance) - return cutoff; + var program = cutoff.Drawing?.Program; + if (program == null) + continue; + + for (var i = 0; i < program.Codes.Count - 1; i += 2) + { + if (program.Codes[i] is RapidMove rapid && + program.Codes[i + 1] is LinearMove linear) + { + var line = new Geometry.Line(rapid.EndPoint, linear.EndPoint); + if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance) + return cutoff; + } + } } return null;