The docked side panel could only be closed with Escape, which was unreliable: MainForm consumes Escape before ActionLeadIn's KeyDown handler, so with a part selected ActionManager called the action's empty CancelAction and nothing happened; with nothing selected the panel closed but was stored as the previous action, so the next Escape reopened it. - EditNestForm side panel gets a header with the panel title and an always-visible close button that ends the action. - ActionLeadIn.CancelAction now steps back (unlock contour, then deselect part), replacing the dead KeyDown handler. - Actions can opt out of Escape-resume (ResumeOnEscape); the lead-in action does, so a closed panel stays closed.
665 lines
22 KiB
C#
665 lines
22 KiB
C#
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Linq;
|
|
using System.Windows.Forms;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Controls;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Forms;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
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;
|
|
private List<ShapeInfo> contours;
|
|
private Vector snapPoint;
|
|
private Entity snapEntity;
|
|
private ContourType snapContourType;
|
|
private double snapNormal;
|
|
private bool hasSnap;
|
|
private SnapType activeSnapType;
|
|
private ShapeInfo hoveredContour;
|
|
private ShapeInfo lockedContour;
|
|
private ContextMenuStrip contextMenu;
|
|
private CuttingPanel cuttingPanel;
|
|
private static readonly Brush grayOverlay = new SolidBrush(
|
|
Color.FromArgb(160, 180, 180, 180)
|
|
);
|
|
private static readonly Pen highlightPen = new Pen(Color.Cyan, 2.5f);
|
|
private static readonly Pen lockedPen = new Pen(Color.Yellow, 3.0f);
|
|
|
|
public ActionLeadIn(PlateView plateView)
|
|
: base(plateView)
|
|
{
|
|
ConnectEvents();
|
|
}
|
|
|
|
public override void ConnectEvents()
|
|
{
|
|
plateView.MouseMove += OnMouseMove;
|
|
plateView.MouseDown += OnMouseDown;
|
|
plateView.Paint += OnPaint;
|
|
ShowSidePanel();
|
|
}
|
|
|
|
public override void DisconnectEvents()
|
|
{
|
|
plateView.MouseMove -= OnMouseMove;
|
|
plateView.MouseDown -= OnMouseDown;
|
|
plateView.Paint -= OnPaint;
|
|
|
|
HideSidePanel();
|
|
|
|
contextMenu?.Dispose();
|
|
contextMenu = null;
|
|
|
|
DeselectPart();
|
|
}
|
|
|
|
// Escape reaches here through ActionManager.ProcessEscapeKey while IsBusy:
|
|
// step back one level (unlock the contour, then release the part). Once
|
|
// nothing is selected, Escape ends the action and closes the side panel.
|
|
public override void CancelAction()
|
|
{
|
|
if (lockedContour != null)
|
|
UnlockContour();
|
|
else
|
|
DeselectPart();
|
|
}
|
|
|
|
public override bool IsBusy() => selectedPart != null;
|
|
|
|
// Escape closes the side panel; a second Escape must not reopen it.
|
|
public override bool ResumeOnEscape => false;
|
|
|
|
private void ShowSidePanel()
|
|
{
|
|
var form = plateView.FindForm() as EditNestForm;
|
|
if (form == null)
|
|
return;
|
|
|
|
cuttingPanel = new CuttingPanel { ShowAutoAssign = true };
|
|
cuttingPanel.AutoAssignClicked += OnAutoAssignClicked;
|
|
cuttingPanel.ParametersChanged += OnToolParametersChanged;
|
|
|
|
// Load current parameters or defaults
|
|
var plate = plateView.Plate;
|
|
if (plate?.CuttingParameters != null)
|
|
cuttingPanel.LoadFromParameters(plate.CuttingParameters);
|
|
else
|
|
{
|
|
var json = Properties.Settings.Default.CuttingParametersJson;
|
|
if (!string.IsNullOrEmpty(json))
|
|
{
|
|
try
|
|
{
|
|
var saved = CuttingParametersSerializer.Deserialize(json);
|
|
cuttingPanel.LoadFromParameters(saved);
|
|
}
|
|
catch
|
|
{ /* use defaults */
|
|
}
|
|
}
|
|
}
|
|
|
|
form.ShowSidePanel(cuttingPanel, "Place Lead-in");
|
|
}
|
|
|
|
private void HideSidePanel()
|
|
{
|
|
if (cuttingPanel == null)
|
|
return;
|
|
|
|
SaveParameters();
|
|
|
|
cuttingPanel.ParametersChanged -= OnToolParametersChanged;
|
|
cuttingPanel.AutoAssignClicked -= OnAutoAssignClicked;
|
|
|
|
var form = plateView.FindForm() as EditNestForm;
|
|
form?.HideSidePanel();
|
|
|
|
cuttingPanel = null;
|
|
}
|
|
|
|
private CuttingParameters GetCurrentParameters()
|
|
{
|
|
return cuttingPanel?.BuildParameters()
|
|
?? plateView.Plate?.CuttingParameters
|
|
?? new CuttingParameters();
|
|
}
|
|
|
|
private void SaveParameters()
|
|
{
|
|
if (cuttingPanel == null)
|
|
return;
|
|
|
|
var parameters = cuttingPanel.BuildParameters();
|
|
var json = CuttingParametersSerializer.Serialize(parameters);
|
|
Properties.Settings.Default.CuttingParametersJson = json;
|
|
Properties.Settings.Default.Save();
|
|
|
|
if (plateView.Plate != null)
|
|
plateView.Plate.CuttingParameters = parameters;
|
|
}
|
|
|
|
private void OnToolParametersChanged(object sender, System.EventArgs e)
|
|
{
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void OnMouseMove(object sender, MouseEventArgs e)
|
|
{
|
|
if (selectedPart == null || contours == null)
|
|
return;
|
|
|
|
var worldPt = plateView.CurrentPoint;
|
|
|
|
// Transform world point into program-local space by subtracting the
|
|
// part's location. The contour shapes are already in the program's
|
|
// rotated coordinate system, so no additional un-rotation is needed.
|
|
var localPt = new Vector(
|
|
worldPt.X - selectedPart.Location.X,
|
|
worldPt.Y - selectedPart.Location.Y
|
|
);
|
|
|
|
// Find closest contour and point
|
|
var bestDist = double.MaxValue;
|
|
hasSnap = false;
|
|
activeSnapType = SnapType.None;
|
|
hoveredContour = null;
|
|
|
|
// When a contour is locked, only snap within that contour
|
|
var searchContours =
|
|
lockedContour != null ? new List<ShapeInfo> { lockedContour } : contours;
|
|
|
|
foreach (var info in searchContours)
|
|
{
|
|
var closest = info.Shape.ClosestPointTo(localPt, out var entity);
|
|
var dist = closest.DistanceTo(localPt);
|
|
|
|
if (dist < bestDist)
|
|
{
|
|
bestDist = dist;
|
|
snapPoint = closest;
|
|
snapEntity = entity;
|
|
snapContourType = info.ContourType;
|
|
hasSnap = true;
|
|
hoveredContour = info;
|
|
}
|
|
}
|
|
|
|
// Check endpoint/midpoint snaps on the hovered contour
|
|
if (hoveredContour != null)
|
|
{
|
|
TrySnapToEntityPoints(localPt);
|
|
|
|
// Auto-switch tool window tab only when no contour is locked
|
|
if (cuttingPanel != null && lockedContour == null)
|
|
cuttingPanel.ActiveContourType = snapContourType;
|
|
}
|
|
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void OnMouseDown(object sender, MouseEventArgs e)
|
|
{
|
|
if (e.Button == MouseButtons.Left)
|
|
{
|
|
if (selectedPart == null)
|
|
{
|
|
// First click: select a part
|
|
SelectPartAtCursor();
|
|
}
|
|
else if (lockedContour == null && hasSnap)
|
|
{
|
|
// Second click: lock the hovered contour
|
|
LockContour(hoveredContour);
|
|
}
|
|
else if (lockedContour != null && hasSnap)
|
|
{
|
|
// Third click: commit lead-in at snap point on locked contour
|
|
CommitLeadIn();
|
|
}
|
|
}
|
|
else if (e.Button == MouseButtons.Right)
|
|
{
|
|
if (lockedContour != null)
|
|
UnlockContour();
|
|
else if (selectedPart != null && selectedPart.HasManualLeadIns)
|
|
ShowContextMenu(e.Location);
|
|
else
|
|
DeselectPart();
|
|
}
|
|
}
|
|
|
|
private void OnPaint(object sender, PaintEventArgs e)
|
|
{
|
|
var g = e.Graphics;
|
|
|
|
DrawOverlay(g);
|
|
DrawHoveredContour(g);
|
|
DrawLeadInPreview(g);
|
|
}
|
|
|
|
private void LockContour(ShapeInfo contour)
|
|
{
|
|
lockedContour = contour;
|
|
|
|
// Lock the tab to this contour type
|
|
if (cuttingPanel != null)
|
|
cuttingPanel.ActiveContourType = contour.ContourType;
|
|
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void UnlockContour()
|
|
{
|
|
lockedContour = null;
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void DrawOverlay(Graphics g)
|
|
{
|
|
foreach (var lp in plateView.LayoutParts)
|
|
{
|
|
if (lp != selectedLayoutPart && lp.Path != null)
|
|
g.FillPath(grayOverlay, lp.Path);
|
|
}
|
|
}
|
|
|
|
private void DrawHoveredContour(Graphics g)
|
|
{
|
|
if (selectedPart == null)
|
|
return;
|
|
|
|
// Draw locked contour with distinct pen
|
|
if (lockedContour != null)
|
|
{
|
|
DrawContourHighlight(g, lockedContour.Shape, lockedPen);
|
|
return;
|
|
}
|
|
|
|
// Draw hovered contour
|
|
if (hoveredContour != null)
|
|
DrawContourHighlight(g, hoveredContour.Shape, highlightPen);
|
|
}
|
|
|
|
private void DrawContourHighlight(Graphics g, Shape shape, Pen pen)
|
|
{
|
|
using var contourPath = 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(pen, contourPath);
|
|
g.SmoothingMode = prevSmooth;
|
|
}
|
|
|
|
private void DrawLeadInPreview(Graphics g)
|
|
{
|
|
if (!hasSnap || selectedPart == null)
|
|
return;
|
|
|
|
var parameters = GetCurrentParameters();
|
|
|
|
var leadIn = SelectLeadIn(parameters, snapContourType);
|
|
if (leadIn == null)
|
|
return;
|
|
|
|
// Same resolution as program generation, so a corner previews identically
|
|
// whichever of its two edges the cursor picked.
|
|
leadIn = ContourCuttingStrategy.ResolveLeadIn(
|
|
hoveredContour.Shape,
|
|
snapPoint,
|
|
snapEntity,
|
|
snapContourType,
|
|
leadIn,
|
|
hoveredContour.Winding,
|
|
parameters.PierceClearance,
|
|
out snapNormal
|
|
);
|
|
leadIn = ClampLeadInForCircle(leadIn, parameters);
|
|
|
|
var piercePoint = leadIn.GetPiercePoint(snapPoint, snapNormal);
|
|
var pt1 = plateView.PointWorldToGraph(TransformToWorld(piercePoint));
|
|
var pt2 = plateView.PointWorldToGraph(TransformToWorld(snapPoint));
|
|
|
|
var oldSmooth = g.SmoothingMode;
|
|
g.SmoothingMode = SmoothingMode.AntiAlias;
|
|
|
|
using var previewPen = new Pen(Color.Magenta, 2.0f / plateView.ViewScale);
|
|
g.DrawLine(previewPen, pt1, pt2);
|
|
|
|
var radius = 3.0f / plateView.ViewScale;
|
|
g.FillEllipse(Brushes.Magenta, pt1.X - radius, pt1.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;
|
|
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);
|
|
if (layoutPart == null)
|
|
return;
|
|
|
|
var part = layoutPart.BasePart;
|
|
|
|
// Don't allow lead-in placement on cut-off parts
|
|
if (part.BaseDrawing.IsCutOff)
|
|
return;
|
|
|
|
selectedLayoutPart = layoutPart;
|
|
selectedPart = part;
|
|
|
|
// Build contour info from the part's program geometry
|
|
BuildContourInfo();
|
|
|
|
// Highlight the selected part
|
|
layoutPart.IsSelected = true;
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void BuildContourInfo()
|
|
{
|
|
// Get a clean program (no lead-ins) in the part's current rotated space.
|
|
// If the part has manual lead-ins, rebuild from base drawing + rotation.
|
|
// Otherwise the current Program is already clean and rotated.
|
|
CNC.Program cleanProgram;
|
|
|
|
if (selectedPart.HasManualLeadIns)
|
|
{
|
|
cleanProgram = selectedPart.BaseDrawing.Program.Clone() as CNC.Program;
|
|
if (!OpenNest.Math.Tolerance.IsEqualTo(selectedPart.Rotation, 0))
|
|
cleanProgram.Rotate(selectedPart.Rotation);
|
|
}
|
|
else
|
|
{
|
|
cleanProgram = selectedPart.Program;
|
|
}
|
|
|
|
var entities = ConvertProgram
|
|
.ToGeometry(cleanProgram)
|
|
.Where(e => e.Layer == SpecialLayers.Cut)
|
|
.ToList();
|
|
|
|
profile = new ShapeProfile(entities);
|
|
|
|
contours = new List<ShapeInfo>();
|
|
|
|
// Perimeter is always External
|
|
if (profile.Perimeter != null)
|
|
{
|
|
contours.Add(
|
|
new ShapeInfo
|
|
{
|
|
Shape = profile.Perimeter,
|
|
ContourType = ContourType.External,
|
|
Winding = ContourCuttingStrategy.DetermineWinding(profile.Perimeter),
|
|
}
|
|
);
|
|
}
|
|
|
|
// Cutouts
|
|
foreach (var cutout in profile.Cutouts)
|
|
{
|
|
contours.Add(
|
|
new ShapeInfo
|
|
{
|
|
Shape = cutout,
|
|
ContourType = ContourCuttingStrategy.DetectContourType(cutout),
|
|
Winding = ContourCuttingStrategy.DetermineWinding(cutout),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
private void CommitLeadIn()
|
|
{
|
|
var parameters = GetCurrentParameters();
|
|
|
|
if (selectedPart.HasManualLeadIns)
|
|
selectedPart.RemoveLeadIns();
|
|
|
|
ApplyAutoTab(parameters);
|
|
|
|
selectedPart.ApplySingleLeadIn(parameters, snapPoint, snapEntity, snapContourType);
|
|
selectedPart.LeadInsLocked = true;
|
|
|
|
selectedLayoutPart.IsDirty = true;
|
|
UnlockContour();
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void OnAutoAssignClicked(object sender, System.EventArgs e)
|
|
{
|
|
if (selectedPart == null)
|
|
return;
|
|
|
|
var parameters = GetCurrentParameters();
|
|
|
|
if (selectedPart.HasManualLeadIns)
|
|
selectedPart.RemoveLeadIns();
|
|
|
|
ApplyAutoTab(parameters);
|
|
|
|
selectedPart.ApplyLeadIns(parameters, Vector.Zero);
|
|
selectedPart.LeadInsLocked = true;
|
|
|
|
selectedLayoutPart.IsDirty = true;
|
|
UnlockContour();
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void ApplyAutoTab(CuttingParameters parameters)
|
|
{
|
|
if (parameters.AutoTabMinSize <= 0 && parameters.AutoTabMaxSize <= 0)
|
|
return;
|
|
|
|
var bbox = selectedPart.Program.BoundingBox();
|
|
var minDim = System.Math.Min(bbox.Width, bbox.Length);
|
|
|
|
if (minDim >= parameters.AutoTabMinSize && minDim <= parameters.AutoTabMaxSize)
|
|
parameters.TabsEnabled = true;
|
|
}
|
|
|
|
private void DeselectPart()
|
|
{
|
|
if (selectedLayoutPart != null)
|
|
{
|
|
selectedLayoutPart.IsSelected = false;
|
|
selectedLayoutPart = null;
|
|
}
|
|
|
|
selectedPart = null;
|
|
profile = null;
|
|
contours = null;
|
|
lockedContour = null;
|
|
hasSnap = false;
|
|
activeSnapType = SnapType.None;
|
|
hoveredContour = null;
|
|
plateView.Invalidate();
|
|
}
|
|
|
|
private void ShowContextMenu(Point location)
|
|
{
|
|
contextMenu?.Dispose();
|
|
contextMenu = new ContextMenuStrip();
|
|
|
|
var removeItem = new ToolStripMenuItem("Remove All Lead-ins");
|
|
removeItem.Click += (s, e) =>
|
|
{
|
|
selectedPart.RemoveLeadIns();
|
|
selectedLayoutPart.IsDirty = true;
|
|
selectedLayoutPart.Update();
|
|
DeselectPart();
|
|
plateView.Invalidate();
|
|
};
|
|
|
|
contextMenu.Items.Add(removeItem);
|
|
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
|
|
// before building the profile), so just add the part location offset
|
|
return new Vector(
|
|
localPt.X + selectedPart.Location.X,
|
|
localPt.Y + selectedPart.Location.Y
|
|
);
|
|
}
|
|
|
|
private static LeadIn SelectLeadIn(CuttingParameters parameters, ContourType contourType)
|
|
{
|
|
return contourType switch
|
|
{
|
|
ContourType.ArcCircle => parameters.ArcCircleLeadIn ?? parameters.InternalLeadIn,
|
|
ContourType.Internal => parameters.InternalLeadIn,
|
|
_ => parameters.ExternalLeadIn,
|
|
};
|
|
}
|
|
|
|
private class ShapeInfo
|
|
{
|
|
public Shape Shape { get; set; }
|
|
public ContourType ContourType { get; set; }
|
|
public RotationType Winding { get; set; }
|
|
}
|
|
}
|
|
}
|