using OpenNest.Geometry;
using OpenNest.Undo;
namespace OpenNest.Desktop.Interaction;
///
/// The default plate tool, with the WinForms Select action's selection rules:
///
/// - A left click within of a cut-off's stroke selects that cut-off alone.
/// - 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.
/// - Dragging from a part moves every selected part with the pointer, as one "Move" undo
/// step; on release the plate's cut-offs are rebuilt around the parts' new places.
/// - 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 ().
/// - A right click on an unselected part selects it alone, for its menu; on an empty area it
/// clears the selection.
///
/// Cancelling ends a move in progress where it is (still one undo step) and clears the selection.
/// Losing the pointer mid-drag ends a move as a release where it is and keeps the selection; a
/// selection window is dropped without selecting.
///
/// While the host's edits are locked () a press does
/// nothing at all, as in WinForms, whose PlateView.OnMouseDown returns before the Select
/// action sees the press: nothing is selected or deselected and no move starts. A move already
/// open when the lock appears is left to finish at its release: a lock is started by a command,
/// not mid-drag, and ending the move then would leave WinForms' quirk of a drag scope held open
/// until some later release.
///
///
public sealed class SelectTool : PlateTool
{
/// How near, in pixels, a click must be to a cut-off's stroke to pick it.
public const double CutOffPickPixels = 5;
public SelectTool(IPlateToolHost host)
: base(host)
{
}
public override string Name => "Select";
/// The window being dragged, or null.
public SelectionWindow? Window { get; private set; }
/// Whether a drag is moving the selected parts.
public bool IsMoving => move != null;
private ChangeScope move;
private Vector lastMoveWorld;
/// Busy while dragging a window or while anything is selected, so Escape first deselects.
public override bool IsBusy => Window != null || !Host.Selection.IsEmpty;
public override void Cancel()
{
// Parts already dragged stay where they are, so the move is recorded.
EndMove();
Window = null;
Host.Selection.Clear();
Host.Invalidate();
}
public override bool PointerPressed(ToolPointer pointer)
{
var plate = Host.Plate;
if (plate == null || Host.EditsLocked)
return false;
var selection = Host.Selection;
selection.Prune();
if (pointer.Button == ToolButton.Left)
{
EndMove();
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);
move = Host.BeginChange("Move");
lastMoveWorld = pointer.World;
}
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 (pointer.Button != ToolButton.Left)
return false;
if (move != null)
{
var offset = pointer.World - lastMoveWorld;
lastMoveWorld = pointer.World;
move.MarkChanged();
foreach (var part in Host.Selection.Parts)
part.Offset(offset.X, offset.Y);
Host.Invalidate();
return true;
}
if (Window is not { } window)
return false;
Window = window with { Second = pointer.World };
Host.Invalidate();
return true;
}
public override bool PointerReleased(ToolPointer pointer)
{
if (pointer.Button != ToolButton.Left)
return false;
if (move != null)
{
FinishMove();
return true;
}
if (Window is not { } window)
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;
}
public override void PointerCaptureLost()
{
if (move != null)
FinishMove();
Window = null;
Host.Invalidate();
}
///
/// Ends the open move as a release does: the cut-offs are rebuilt around moved parts, then
/// the step is recorded. If rebuilding throws, the scope is disposed incomplete, so the move
/// rolls back, and the exception propagates.
///
private void FinishMove()
{
var scope = move;
move = null;
try
{
// Only after a real move: rebuilding gives the cut-offs new programs, which would
// otherwise count as an unrecorded change after a plain click.
var movedOn = Host.Plate;
if (scope.IsChanged && movedOn != null && Host.Selection.Parts.Count > 0
&& movedOn.CutOffs.Count > 0)
movedOn.RegenerateCutOffs(Host.CutOffSettings);
scope.Complete();
}
finally
{
scope.Dispose();
Host.Invalidate();
}
}
private void EndMove()
{
var scope = move;
move = null;
if (scope == null)
return;
scope.Complete();
scope.Dispose();
}
}