Files
OpenNest/OpenNest.Desktop/Interaction/PlateToolManager.cs
T
ajandClaude Opus 5.5 313b5f57d6 feat(desktop): add a toolkit-neutral plate tool and selection model
PlateToolManager keeps one active tool with the WinForms action rules
(Escape cancels, then returns to Select, then resumes the ended tool) and
status text. SelectTool selects by click, Ctrl+click and window
(contains/intersect), takes common-line groups together and picks
cut-offs by their strokes. PlateHitTester hit-tests the shared display
geometry; PlateSelection reads and sets by part index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 19:30:47 -05:00

111 lines
3.4 KiB
C#

using System;
namespace OpenNest.Desktop.Interaction;
/// <summary>
/// Keeps one plate tool active, as the WinForms view's action manager does: Select by default;
/// Escape first cancels what the active tool is doing, then ends the tool and returns to Select,
/// and an Escape from an idle Select brings back the tool Escape ended (when that tool allows it).
/// <see cref="Status"/> is the active tool's name and hint for the status bar.
/// </summary>
public sealed class PlateToolManager
{
private readonly IPlateToolHost host;
private PlateTool previous;
public PlateToolManager(IPlateToolHost host)
{
this.host = host ?? throw new ArgumentNullException(nameof(host));
Current = new SelectTool(host);
Current.HintChanged += OnHintChanged;
Current.Activate();
Status = StatusOf(Current);
}
/// <summary>The active tool.</summary>
public PlateTool Current { get; private set; }
/// <summary>The active tool's name, and its hint when it has one.</summary>
public string Status { get; private set; }
/// <summary>Raised when the active tool or its status changes.</summary>
public event EventHandler StatusChanged;
/// <summary>
/// Makes <paramref name="tool"/> active, ending the current one. Ending another tool for
/// Select keeps it for Escape to resume; any other switch forgets the resumable tool.
/// </summary>
public void SetTool(PlateTool tool)
{
ArgumentNullException.ThrowIfNull(tool);
previous = tool is SelectTool && Current is not SelectTool && Current.ResumeOnEscape ? Current : null;
Switch(tool);
}
/// <summary>Returns to Select (as a fresh tool, so nothing selected is kept).</summary>
public void Select() => SetTool(new SelectTool(host));
/// <summary>
/// Ends the active tool at the user's explicit request (a panel's Close button, say). Unlike
/// Escape, the ended tool is not kept for Escape to resume.
/// </summary>
public void EndTool()
{
Select();
previous = null;
}
/// <summary>Handles Escape; see the class summary.</summary>
public void Escape()
{
if (Current.IsBusy)
Current.Cancel();
else if (Current is SelectTool && previous != null)
{
var resumed = previous;
previous = null;
Switch(resumed);
}
else
Select();
host.Invalidate();
}
/// <summary>Called when the host shows another plate: tools that do not survive it end.</summary>
public void OnPlateChanged()
{
if (!Current.SurvivesPlateChange)
Select();
else
Current.OnPlateChanged();
}
private void Switch(PlateTool tool)
{
var old = Current;
old.Cancel();
old.Deactivate();
old.HintChanged -= OnHintChanged;
Current = tool;
tool.HintChanged += OnHintChanged;
tool.Activate();
RefreshStatus();
host.Invalidate();
}
private void OnHintChanged(object sender, EventArgs e)
{
if (ReferenceEquals(sender, Current))
RefreshStatus();
}
private void RefreshStatus()
{
Status = StatusOf(Current);
StatusChanged?.Invoke(this, EventArgs.Empty);
}
private static string StatusOf(PlateTool tool) =>
string.IsNullOrEmpty(tool.Hint) ? tool.Name : $"{tool.Name}: {tool.Hint}";
}