feat(avalonia): set the offset and rotation increments

Tools > Set Offset Increment (F5) and Set Rotation Increment (F6) open
the WinForms SetValueForm's dialog: an "Increment :" box shown with
four decimals and a Set button that Enter also presses; Esc or closing
the window changes nothing.

- The offset dialog has the "Calculated distance (selection size +
  part spacing)" check box, which turns the value box off; with it on,
  arrow keys step the selection by its size plus the part spacing.
- The rotation increment goes up to 360 degrees; Shift+wheel turns by
  it and Ctrl+Shift+wheel by a tenth of it.
- A typed value out of range is taken to the nearest limit and an
  emptied box keeps the last value it held, as in WinForms.

Each nest keeps its own increments for the session, starting at 10, as
each WinForms nest window does; they are not saved. With no nest open
both commands are off (WinForms hides them).
This commit is contained in:
aj committed 2026-11-14 21:05:25 -05:00
1 parent f061fcacea
commit 99e8dc3d0e
3 files changed
+162

No files matched your search

+3
View File
@@ -59,6 +59,9 @@
</MenuItem>
<MenuItem Header="_Tools">
<MenuItem x:Name="OptionsMenuItem" Header="_Options..." Click="OnOptions" />
<Separator />
<MenuItem x:Name="SetOffsetIncrementMenuItem" Header="Set Offset Increment" InputGesture="F5" Click="OnSetOffsetIncrement" />
<MenuItem x:Name="SetRotationIncrementMenuItem" Header="Set Rotation Increment" InputGesture="F6" Click="OnSetRotationIncrement" />
</MenuItem>
</Menu>
+43
View File
@@ -95,6 +95,16 @@ namespace OpenNest.Avalonia
});
KeyBindings.Add(new KeyBinding { Gesture = new KeyGesture(Key.PageUp), Command = new Command(() => StepPlate(-1)) });
KeyBindings.Add(new KeyBinding { Gesture = new KeyGesture(Key.PageDown), Command = new Command(() => StepPlate(1)) });
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.F5),
Command = new Command(() => OnSetOffsetIncrement(null, null)),
});
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.F6),
Command = new Command(() => OnSetRotationIncrement(null, null)),
});
// Ctrl+Tab before focus navigation takes the Tab key.
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
// .nest files dropped anywhere on the window, the nest view included, open in tabs.
@@ -295,6 +305,8 @@ namespace OpenNest.Avalonia
SaveButton.IsEnabled = view != null;
SaveAsButton.IsEnabled = view != null;
CloseAllMenuItem.IsEnabled = view != null;
SetOffsetIncrementMenuItem.IsEnabled = view != null;
SetRotationIncrementMenuItem.IsEnabled = view != null;
UpdateExportCommands(view != null);
Title = view == null
? "OpenNest (Avalonia prototype)"
@@ -849,6 +861,37 @@ namespace OpenNest.Avalonia
private void StepPlate(int step) => ActiveView?.StepPlate(step);
// Tools > Set Offset Increment (F5), as WinForms: how far an arrow key nudges the active
// nest's selection, or the calculated step (selection size plus part spacing). Each nest
// keeps its own increments for the session, as each WinForms nest window does.
private async void OnSetOffsetIncrement(object sender, RoutedEventArgs e)
{
if (ActiveView is not { } view)
return;
var edits = view.PlateView.Edits;
var result = await SetValueDialog.ShowAsync(this, "Set Offset Increment",
edits.OffsetIncrement,
optionText: "Calculated distance (selection size + part spacing)",
optionChecked: edits.CalculatedOffset);
if (result == null)
return;
edits.OffsetIncrement = result.Value;
edits.CalculatedOffset = result.OptionChecked;
}
// Tools > Set Rotation Increment (F6): the Shift+wheel turn, at most 360 degrees.
private async void OnSetRotationIncrement(object sender, RoutedEventArgs e)
{
if (ActiveView is not { } view)
return;
var edits = view.PlateView.Edits;
var result = await SetValueDialog.ShowAsync(this, "Set Rotation Increment",
edits.RotateIncrementDegrees, maximum: 360);
if (result == null)
return;
edits.RotateIncrementDegrees = result.Value;
}
// Tools > Options. Settings for features the app does not have yet (Database mode, the
// update feed) are still saved, for the WinForms app and later milestones.
private async void OnOptions(object sender, RoutedEventArgs e) =>
+116
View File
@@ -0,0 +1,116 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace OpenNest.Avalonia
{
/// <summary>
/// One number to set, as the WinForms SetValueForm: an "Increment :" box from 0 to a maximum
/// shown with four decimals, an optional check box that disables the box, and a Set button
/// that is also Enter. Esc or closing the window changes nothing.
/// </summary>
internal sealed class SetValueDialog : Window
{
public const double DefaultMaximum = 999999;
/// <summary>What the user set: the value and the option check box.</summary>
public sealed record Result(double Value, bool OptionChecked);
private readonly NumericUpDown value;
private readonly CheckBox option;
// The last value the box held, which an emptied box keeps, as the WinForms box does.
private double lastValid;
private readonly double maximum;
private SetValueDialog(
string title, double initialValue, double maximum, string optionText,
bool optionChecked)
{
Title = title;
// Wide enough for the option's text on one line, as the WinForms form widens.
MinWidth = 300;
SizeToContent = SizeToContent.WidthAndHeight;
CanResize = false;
ShowInTaskbar = false;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
this.maximum = maximum;
lastValid = System.Math.Clamp(initialValue, 0, maximum);
value = new NumericUpDown
{
Name = "Value",
Minimum = 0,
Maximum = (decimal)maximum,
Increment = 1,
// A typed value out of range goes to the nearest limit, as in WinForms, rather
// than being refused.
ClipValueToMinMax = true,
FormatString = "0.0000",
Value = (decimal)lastValid,
HorizontalAlignment = HorizontalAlignment.Stretch,
};
value.ValueChanged += (_, e) =>
{
if (e.NewValue is { } v)
lastValid = (double)v;
};
var set = new Button { Name = "Set", Content = "Set", IsDefault = true };
set.Click += (_, _) => Close(new Result(Current, option.IsChecked == true));
var row = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
ColumnSpacing = 8,
};
var label = new TextBlock
{
Text = "Increment :",
VerticalAlignment = VerticalAlignment.Center,
};
row.Children.Add(label);
Grid.SetColumn(value, 1);
row.Children.Add(value);
Grid.SetColumn(set, 2);
row.Children.Add(set);
option = new CheckBox
{
Name = "Option",
Content = optionText,
IsChecked = optionChecked,
IsVisible = !string.IsNullOrEmpty(optionText),
};
value.IsEnabled = !optionChecked;
option.IsCheckedChanged += (_, _) => value.IsEnabled = option.IsChecked != true;
var panel = new StackPanel { Margin = new Thickness(12), Spacing = 10 };
panel.Children.Add(row);
panel.Children.Add(option);
Content = panel;
AddHandler(KeyDownEvent, (_, e) =>
{
if (e.Key != Key.Escape)
return;
e.Handled = true;
Close(null);
}, RoutingStrategies.Tunnel);
Opened += (_, _) => value.Focus();
}
// A value out of range is taken to the nearest limit, as the WinForms box does.
private double Current => System.Math.Clamp(lastValid, 0, maximum);
/// <summary>
/// The value and option the user set, or null when they closed the dialog without Set.
/// </summary>
public static Task<Result> ShowAsync(
Window owner, string title, double value, double maximum = DefaultMaximum,
string optionText = null, bool optionChecked = false) =>
new SetValueDialog(title, value, maximum, optionText, optionChecked)
.ShowDialog<Result>(owner);
}
}