feat(cutoff): tell users Space turns the manual cut-off line

Issue #13 asked whether a Sheet Cut-Off can be rotated; Space already
toggles vertical/horizontal, but nothing said so. The tool now shows a
"Space: turn horizontal" tag by the pointer, spells out its keys in the
status bar, and F1 opens a new sheet-cutoff help topic while it is
active (actions can now supply a status hint and a help topic).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-29 08:47:16 -04:00
1 parent 41460481d6
commit a1d20bdeb5
7 files changed
+93 -5

No files matched your search

@@ -0,0 +1,33 @@
using System.Reflection;
using System.Windows.Forms;
using OpenNest.Actions;
using OpenNest.Controls;
using OpenNest.Help;
namespace OpenNest.WinForms.Tests.Actions;
/// <summary>The manual cut-off tool says how to turn its line and where its help is.</summary>
public class ActionCutOffHintTests
{
private static void PressKey(PlateView view, Keys key) =>
typeof(Control).GetMethod("OnKeyDown", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(view, new object[] { new KeyEventArgs(key) });
[Fact]
public void StatusNamesTheAxisAndSpaceTurnsIt() => StaTestThread.Run(() =>
{
using var view = new PlateView { Plate = new Plate(48, 120) };
view.SetAction(typeof(ActionCutOff));
Assert.Equal(
"Sheet Cut-Off: click to add a vertical line, Space turns it horizontal, Esc finishes, F1 for help",
view.Status);
Assert.Equal(HelpTopics.SheetCutOff, view.ActionHelpTopic);
PressKey(view, Keys.Space);
Assert.StartsWith("Sheet Cut-Off: click to add a horizontal line, Space turns it vertical", view.Status);
view.SetAction(typeof(ActionSelect));
Assert.Null(view.ActionHelpTopic);
}, TimeSpan.FromMinutes(1), "The STA test did not complete.");
}
+6
View File
@@ -33,6 +33,12 @@ namespace OpenNest.Actions
/// </summary>
public virtual bool ResumeOnEscape => true;
/// <summary>Status bar guidance shown after the action's name, or null for the name alone.</summary>
public virtual string Hint => null;
/// <summary>The help topic F1 opens while this action is active, or null for the general help.</summary>
public virtual string HelpTopic => null;
public virtual void OnPlateChanged() { }
public virtual void ConnectEvents() { }
+30 -2
View File
@@ -6,6 +6,7 @@ using System.Windows.Forms;
using OpenNest.CNC;
using OpenNest.Controls;
using OpenNest.Geometry;
using OpenNest.Help;
namespace OpenNest.Actions
{
@@ -50,6 +51,17 @@ namespace OpenNest.Actions
public override bool IsBusy() => false;
public override string Hint =>
$"click to add a {AxisName(lockedAxis)} line, Space turns it {AxisName(OtherAxis(lockedAxis))}, Esc finishes, F1 for help";
public override string HelpTopic => HelpTopics.SheetCutOff;
private static CutOffAxis OtherAxis(CutOffAxis axis) =>
axis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
private static string AxisName(CutOffAxis axis) =>
axis == CutOffAxis.Vertical ? "vertical" : "horizontal";
private void OnMouseMove(object sender, MouseEventArgs e)
{
var pt = plateView.CurrentPoint;
@@ -78,8 +90,8 @@ namespace OpenNest.Actions
{
if (e.KeyCode == Keys.Space)
{
lockedAxis =
lockedAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
lockedAxis = OtherAxis(lockedAxis);
plateView.RefreshActionStatus();
if (previewCutOff != null)
{
@@ -126,6 +138,22 @@ namespace OpenNest.Actions
e.Graphics.DrawLine(pen, pt1, pt2);
}
}
DrawRotateTag(e.Graphics);
}
// The status bar is easy to miss, so the cursor says how to turn the line.
private void DrawRotateTag(Graphics g)
{
var text = $"Space: turn {AxisName(OtherAxis(lockedAxis))}";
var cursor = plateView.PointWorldToGraph(plateView.CurrentPoint);
var size = g.MeasureString(text, plateView.Font);
var rect = new RectangleF(cursor.X + 16f, cursor.Y + 16f, size.Width + 6, size.Height + 4);
using (var background = new SolidBrush(Color.FromArgb(230, Color.White)))
g.FillRectangle(background, rect);
g.DrawRectangle(Pens.DimGray, rect.X, rect.Y, rect.Width, rect.Height);
g.DrawString(text, plateView.Font, Brushes.Black, rect.X + 3, rect.Y + 2);
}
}
}
+11 -3
View File
@@ -40,7 +40,7 @@ namespace OpenNest.Controls
}
currentAction = action;
view.Status = GetDisplayName(type);
RefreshStatus();
}
public void SetAction(Type type, params object[] args)
@@ -63,7 +63,7 @@ namespace OpenNest.Controls
return;
currentAction = action;
view.Status = GetDisplayName(type);
RefreshStatus();
}
public void ProcessEscapeKey()
@@ -98,7 +98,7 @@ namespace OpenNest.Controls
action.ConnectEvents();
currentAction = action;
view.Status = GetDisplayName(action.GetType());
RefreshStatus();
}
public void OnPlateChanged()
@@ -119,6 +119,14 @@ namespace OpenNest.Controls
}
}
/// <summary>Shows the current action's name and hint in the status bar.</summary>
public void RefreshStatus()
{
var name = GetDisplayName(currentAction.GetType());
var hint = currentAction.Hint;
view.Status = string.IsNullOrEmpty(hint) ? name : $"{name}: {hint}";
}
private string GetDisplayName(Type type)
{
var attributes = type.GetCustomAttributes(true);
+6
View File
@@ -866,6 +866,12 @@ namespace OpenNest.Controls
public void SetAction(Type type, params object[] args) =>
actionManager.SetAction(type, args);
/// <summary>Shows the current action's name and hint again after the hint changed.</summary>
public void RefreshActionStatus() => actionManager.RefreshStatus();
/// <summary>The help topic for the current action, or null when it has none.</summary>
public string ActionHelpTopic => actionManager.CurrentAction?.HelpTopic;
public void AlignSelected(AlignType alignType) => selection.AlignSelected(alignType);
public void AlignSelected(AlignType alignType, LayoutPart fixedPart) =>
+6
View File
@@ -99,6 +99,12 @@ namespace OpenNest.Forms
}
if (ProcessUndoKeys(keyData))
return true;
// F1 opens the active tool's topic before the Help menu's general help.
if (keyData == Keys.F1 && activeForm?.PlateView?.ActionHelpTopic is { } topic)
{
HelpTopics.Show(this, topic);
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
+1
View File
@@ -19,6 +19,7 @@ namespace OpenNest.Help
public const string AutoNest = "auto-nest";
public const string Post = "post";
public const string SupportGrid = "support-grid";
public const string SheetCutOff = "sheet-cutoff";
/// <summary>Overrides the help folder, e.g. a local opennest-help <c>site/</c> build.</summary>
public const string DirectoryVariable = "OPENNEST_HELP_DIR";