From 99e8dc3d0eaf2a0ad19f3ca7fbfea3856fe3313d Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 14 Nov 2026 20:56:27 -0500 Subject: [PATCH] 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). --- OpenNest.Avalonia/MainWindow.axaml | 3 + OpenNest.Avalonia/MainWindow.axaml.cs | 43 ++++++++++ OpenNest.Avalonia/SetValueDialog.cs | 116 ++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 OpenNest.Avalonia/SetValueDialog.cs diff --git a/OpenNest.Avalonia/MainWindow.axaml b/OpenNest.Avalonia/MainWindow.axaml index effc42f7..1ae9afaa 100644 --- a/OpenNest.Avalonia/MainWindow.axaml +++ b/OpenNest.Avalonia/MainWindow.axaml @@ -59,6 +59,9 @@ + + + diff --git a/OpenNest.Avalonia/MainWindow.axaml.cs b/OpenNest.Avalonia/MainWindow.axaml.cs index f2eb6c51..4f967e05 100644 --- a/OpenNest.Avalonia/MainWindow.axaml.cs +++ b/OpenNest.Avalonia/MainWindow.axaml.cs @@ -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) => diff --git a/OpenNest.Avalonia/SetValueDialog.cs b/OpenNest.Avalonia/SetValueDialog.cs new file mode 100644 index 00000000..1231fe19 --- /dev/null +++ b/OpenNest.Avalonia/SetValueDialog.cs @@ -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 +{ + /// + /// 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. + /// + internal sealed class SetValueDialog : Window + { + public const double DefaultMaximum = 999999; + + /// What the user set: the value and the option check box. + 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); + + /// + /// The value and option the user set, or null when they closed the dialog without Set. + /// + public static Task ShowAsync( + Window owner, string title, double value, double maximum = DefaultMaximum, + string optionText = null, bool optionChecked = false) => + new SetValueDialog(title, value, maximum, optionText, optionChecked) + .ShowDialog(owner); + } +}