Files
OpenNest/OpenNest.Desktop/Interaction/SelectTool.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

119 lines
4.0 KiB
C#

namespace OpenNest.Desktop.Interaction;
/// <summary>
/// The default plate tool, with the WinForms Select action's selection rules:
/// <list type="bullet">
/// <item>A left click within <see cref="CutOffPickPixels"/> of a cut-off's stroke selects that cut-off alone.</item>
/// <item>A left click on a part selects it (with its common-line group) in place of the selection;
/// with Ctrl alone held, or on a part already selected, it adds to the selection instead.</item>
/// <item>A left press on an empty area clears the selection and starts a selection window; on
/// release the window adds the parts and cut-offs it selects (<see cref="SelectionWindow"/>).</item>
/// <item>A right click on an unselected part selects it alone, for its menu; on an empty area it
/// clears the selection.</item>
/// </list>
/// Moving the selection by dragging belongs to the move tools built on this one.
/// </summary>
public sealed class SelectTool : PlateTool
{
/// <summary>How near, in pixels, a click must be to a cut-off's stroke to pick it.</summary>
public const double CutOffPickPixels = 5;
public SelectTool(IPlateToolHost host)
: base(host)
{
}
public override string Name => "Select";
/// <summary>The window being dragged, or null.</summary>
public SelectionWindow? Window { get; private set; }
/// <summary>Busy while dragging a window or while anything is selected, so Escape first deselects.</summary>
public override bool IsBusy => Window != null || !Host.Selection.IsEmpty;
public override void Cancel()
{
Window = null;
Host.Selection.Clear();
Host.Invalidate();
}
public override bool PointerPressed(ToolPointer pointer)
{
var plate = Host.Plate;
if (plate == null)
return false;
var selection = Host.Selection;
selection.Prune();
if (pointer.Button == ToolButton.Left)
{
Window = null;
var tolerance = Host.View.LengthToWorld(CutOffPickPixels);
var cutOff = Host.HitTester.CutOffAt(plate, pointer.World, tolerance);
if (cutOff != null)
{
selection.SelectOnly(cutOff);
Host.Invalidate();
return true;
}
selection.ClearCutOffs();
var part = Host.HitTester.PartAt(plate, pointer.World);
if (part == null)
{
selection.Clear();
Window = new SelectionWindow(pointer.World, pointer.World);
}
else
{
if (pointer.Modifiers != ToolModifiers.Control && !selection.Contains(part))
selection.Clear();
selection.Select(part);
}
Host.Invalidate();
return true;
}
if (pointer.Button == ToolButton.Right)
{
var part = Host.HitTester.PartAt(plate, pointer.World);
if (part == null)
Cancel();
else if (!selection.Contains(part))
{
selection.SelectOnly(part);
Host.Invalidate();
}
return true;
}
return false;
}
public override bool PointerMoved(ToolPointer pointer)
{
if (Window is not { } window || pointer.Button != ToolButton.Left)
return false;
Window = window with { Second = pointer.World };
Host.Invalidate();
return true;
}
public override bool PointerReleased(ToolPointer pointer)
{
if (Window is not { } window || pointer.Button != ToolButton.Left)
return false;
window = window with { Second = pointer.World };
Window = null;
var plate = Host.Plate;
if (plate != null)
{
Host.Selection.Select(Host.HitTester.PartsIn(plate, window));
Host.Selection.Select(Host.HitTester.CutOffsIn(plate, window));
}
Host.Invalidate();
return true;
}
}