feat(avalonia): open several nests in tabs along the bottom

Each open nest gets a tab along the bottom of the window, above the
status bar: the shown one is white with a blue line, a dot marks unsaved
Auto Nest results, x closes it and + opens another. Inside a tab, Plates
and Drawings tabs sit at the top of the left panel (Drawings lists each
drawing's nested and required quantity) and the plate header with
first/previous/next/last sits over the plate view. Each nest keeps its
own plate and zoom when you switch.

File > Open takes several files, and a file already open switches to its
tab. File > Close and Close All, View > Next/Previous Nest and
Ctrl+Tab/Ctrl+Shift+Tab switch or close nests. This version cannot save,
so closing a nest with Auto Nest results, or exiting with any, asks first
(Cancel is the default). Other nests can be opened and viewed while Auto
Nest runs; the nest being nested cannot be closed until it finishes.
Several nest paths on the command line open in tabs.
This commit is contained in:
aj committed 2026-11-09 22:30:12 -05:00
1 parent c32f3e5df3
commit b329dd6c6b
6 files changed
+445 -99

No files matched your search

+7 -2
View File
@@ -15,8 +15,13 @@ namespace OpenNest.Avalonia
var window = new MainWindow();
desktop.MainWindow = window;
var args = desktop.Args ?? System.Array.Empty<string>();
if (args.Length > 0)
window.OpenNest(args[0]);
// Each nest path before the first --option opens in its own tab; the last is shown.
foreach (var path in args)
{
if (path.StartsWith("--"))
break;
window.OpenNest(path);
}
// --auto-nest: run Auto Nest on the opened nest (with --snapshot, before rendering).
if (System.Array.IndexOf(args, "--auto-nest") >= 0)
window.AutoNestOnOpen();
+48
View File
@@ -0,0 +1,48 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
namespace OpenNest.Avalonia
{
/// <summary>
/// A two-button question. Cancel is the default and Esc, so a stray Enter never discards work.
/// </summary>
internal sealed class ConfirmDialog : Window
{
private ConfirmDialog(string title, string message, string confirmText)
{
Title = title;
Width = 460;
SizeToContent = SizeToContent.Height;
CanResize = false;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
var confirm = new Button { Content = confirmText };
var cancel = new Button { Content = "Cancel", IsDefault = true, IsCancel = true };
confirm.Click += (_, _) => Close(true);
cancel.Click += (_, _) => Close(false);
var buttons = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8,
};
buttons.Children.Add(confirm);
buttons.Children.Add(cancel);
var panel = new StackPanel { Margin = new Thickness(18), Spacing = 18 };
panel.Children.Add(new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap });
panel.Children.Add(buttons);
Content = panel;
Opened += (_, _) => cancel.Focus();
}
/// <summary>True when the user chose <paramref name="confirmText"/>.</summary>
public static Task<bool> AskAsync(
Window owner, string title, string message, string confirmText) =>
new ConfirmDialog(title, message, confirmText).ShowDialog<bool>(owner);
}
}
+17 -22
View File
@@ -1,7 +1,5 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="using:OpenNest.Avalonia.Controls"
xmlns:local="using:OpenNest.Avalonia"
x:Class="OpenNest.Avalonia.MainWindow"
Title="OpenNest (Avalonia prototype)"
Width="1400" Height="860">
@@ -9,6 +7,8 @@
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem x:Name="OpenMenuItem" Header="_Open..." InputGesture="Ctrl+O" Click="OnOpen" />
<MenuItem x:Name="CloseMenuItem" Header="_Close" Click="OnCloseNest" />
<MenuItem x:Name="CloseAllMenuItem" Header="Close A_ll" Click="OnCloseAll" />
<Separator />
<MenuItem Header="E_xit" Click="OnExit" />
</MenuItem>
@@ -19,6 +19,9 @@
<MenuItem Header="Zoom to _Fit" InputGesture="F" Click="OnFit" />
<MenuItem Header="_Previous Plate" InputGesture="PageUp" Click="OnPreviousPlate" />
<MenuItem Header="_Next Plate" InputGesture="PageDown" Click="OnNextPlate" />
<Separator />
<MenuItem Header="Next N_est" InputGesture="Ctrl+Tab" Click="OnNextNest" />
<MenuItem Header="Previous Ne_st" InputGesture="Ctrl+Shift+Tab" Click="OnPreviousNest" />
</MenuItem>
</Menu>
@@ -36,25 +39,17 @@
<TextBlock x:Name="Status" Text="Open a .nest file to begin." />
</Border>
<Grid ColumnDefinitions="260,4,*,4,320">
<DockPanel Grid.Column="0">
<TextBlock DockPanel.Dock="Top" Margin="6,6,6,2" FontWeight="SemiBold" Text="Plates" />
<ListBox x:Name="PlateList" SelectionChanged="OnPlateSelected" />
</DockPanel>
<GridSplitter Grid.Column="1" ResizeDirection="Columns" />
<c:PlateCanvas Grid.Column="2" x:Name="Canvas" />
<GridSplitter Grid.Column="3" ResizeDirection="Columns" />
<DockPanel Grid.Column="4">
<TextBlock DockPanel.Dock="Top" Margin="6,6,6,2" FontWeight="SemiBold" Text="Drawings on plate" />
<DataGrid x:Name="DrawingGrid" x:DataType="local:DrawingRow" IsReadOnly="True"
AutoGenerateColumns="False" GridLinesVisibility="Horizontal" CanUserSortColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Drawing" Binding="{Binding Name}" Width="*" />
<DataGridTextColumn Header="Qty" Binding="{Binding Count}" />
<DataGridTextColumn Header="Size" Binding="{Binding Size}" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
<!-- One tab per open nest, along the bottom of the window. -->
<Border DockPanel.Dock="Bottom" Background="#E4E7EA">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel x:Name="TabStrip" Orientation="Horizontal" Margin="6,0,6,6" />
</ScrollViewer>
</Border>
<Panel>
<TextBlock x:Name="EmptyHint" Text="Open a .nest file to begin." Foreground="#6B7280"
HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="15" />
<ContentControl x:Name="NestHost" />
</Panel>
</DockPanel>
</Window>
+237 -75
View File
@@ -1,11 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using OpenNest.Desktop;
using OpenNest.Engine;
@@ -15,17 +19,19 @@ using OpenNest.IO;
namespace OpenNest.Avalonia
{
public sealed record DrawingRow(string Name, int Count, string Size);
public partial class MainWindow : Window
{
private Document document;
private string fileName;
private static readonly IBrush Accent = Brush.Parse("#3368A0");
private static readonly IBrush Muted = Brush.Parse("#6B7280");
private static readonly IBrush InactiveTab = Brush.Parse("#DDE1E5");
private readonly NestTabs tabs = new();
private readonly Dictionary<NestTab, NestView> views = new();
private string notice;
private CancellationTokenSource nesting;
private NestTab nestingTab;
private Task autoNest = Task.CompletedTask;
private Nest nest => document?.Nest;
private bool closeConfirmed;
public MainWindow()
{
@@ -35,7 +41,7 @@ namespace OpenNest.Avalonia
var engineWarning = EngineSelection.LoadSavedSelection();
if (!string.IsNullOrEmpty(engineWarning))
notice = engineWarning;
Canvas.ViewChanged += (_, _) => UpdateStatus();
tabs.Changed += (_, _) => ShowActiveTab();
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.O, KeyModifiers.Control),
@@ -43,35 +49,36 @@ 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)) });
// Ctrl+Tab before focus navigation takes the Tab key.
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
Closing += OnWindowClosing;
ShowActiveTab();
}
// Null while a new tab's view is still being created.
private NestView ActiveView =>
tabs.Active != null && views.TryGetValue(tabs.Active, out var view) ? view : null;
/// <summary>Opens the nest in a new tab, or switches to its tab if open.</summary>
public void OpenNest(string path)
{
if (nesting != null)
return;
try
{
document = new Document { Nest = new NestReader(path).Read() };
var tab = tabs.Open(
path, file => new Document { Nest = new NestReader(file).Read() });
if (!views.ContainsKey(tab))
{
var view = new NestView(tab);
view.ViewChanged += (_, _) => { if (view == ActiveView) UpdateStatus(); };
views[tab] = view;
notice = null;
ShowActiveTab();
}
}
catch (Exception ex)
{
Status.Text = $"Could not open {Path.GetFileName(path)}: {ex.Message}";
return;
ShowNotice($"Could not open {Path.GetFileName(path)}: {ex.Message}");
}
fileName = Path.GetFileName(path);
Title = $"{fileName} - OpenNest (Avalonia prototype)";
notice = null;
Canvas.ClearCache();
RefreshPlateList(0);
}
private void RefreshPlateList(int selectIndex)
{
PlateList.ItemsSource = nest.Plates
.Select((plate, i) => $"Plate {i + 1} {plate.Size.Length:0.###} x {plate.Size.Width:0.###} ({plate.Parts.Count} parts)")
.ToList();
PlateList.SelectedIndex = nest.Plates.Count > 0 ? System.Math.Clamp(selectIndex, 0, nest.Plates.Count - 1) : -1;
}
/// <summary>
@@ -90,32 +97,187 @@ namespace OpenNest.Avalonia
{
Opened += async (_, _) =>
{
if (nest?.Plates.Count > 0)
PlateList.SelectedIndex = System.Math.Clamp(plateNumber - 1, 0, nest.Plates.Count - 1);
ActiveView?.ShowPlate(plateNumber - 1);
await autoNest;
await System.Threading.Tasks.Task.Delay(500);
var size = new global::Avalonia.PixelSize((int)Bounds.Width, (int)Bounds.Height);
await Task.Delay(500);
var size = new PixelSize((int)Bounds.Width, (int)Bounds.Height);
using var bitmap = new global::Avalonia.Media.Imaging.RenderTargetBitmap(size);
bitmap.Render(this);
bitmap.Save(path);
// The status line beside the image, so headless checks can read it.
File.WriteAllText(path + ".txt", Status.Text);
closeConfirmed = true;
Close();
};
}
private void ShowActiveTab()
{
var view = ActiveView;
NestHost.Content = view;
EmptyHint.IsVisible = view == null;
CloseMenuItem.IsEnabled = view != null;
CloseAllMenuItem.IsEnabled = view != null;
Title = view == null
? "OpenNest (Avalonia prototype)"
: $"{view.Tab.Name} - OpenNest (Avalonia prototype)";
RebuildTabStrip();
UpdateStatus();
}
private void RebuildTabStrip()
{
TabStrip.Children.Clear();
for (var i = 0; i < tabs.Tabs.Count; i++)
TabStrip.Children.Add(TabButton(tabs.Tabs[i], i == tabs.ActiveIndex));
var add = new Button
{
Content = "+",
FontSize = 15,
Padding = new Thickness(10, 2),
Margin = new Thickness(4, 4, 0, 0),
Background = Brushes.Transparent,
VerticalAlignment = VerticalAlignment.Top,
};
ToolTip.SetTip(add, "Open a nest");
add.Click += OnOpen;
TabStrip.Children.Add(add);
}
private Control TabButton(NestTab tab, bool active)
{
var name = new TextBlock
{
Text = tab.Modified ? tab.Name + " \u2022" : tab.Name,
FontSize = 13,
FontWeight = active ? FontWeight.SemiBold : FontWeight.Normal,
Foreground = active ? Brushes.Black : Muted,
VerticalAlignment = VerticalAlignment.Center,
};
var close = new Button
{
Content = "\u2715",
FontSize = 10,
Padding = new Thickness(5, 1),
Background = Brushes.Transparent,
Foreground = Muted,
VerticalAlignment = VerticalAlignment.Center,
};
ToolTip.SetTip(close, "Close");
close.Click += async (_, e) =>
{
e.Handled = true;
await CloseTabAsync(tab);
};
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
row.Children.Add(name);
row.Children.Add(close);
var border = new Border
{
Child = row,
Padding = new Thickness(14, 5, 6, 5),
Margin = new Thickness(0, 0, 2, 0),
CornerRadius = new CornerRadius(0, 0, 6, 6),
Background = active ? Brushes.White : InactiveTab,
BorderBrush = active ? Accent : Brushes.Transparent,
BorderThickness = new Thickness(0, 0, 0, 2),
Cursor = new Cursor(StandardCursorType.Hand),
};
ToolTip.SetTip(border, tab.Path);
border.PointerPressed += (_, e) =>
{
if (e.GetCurrentPoint(border).Properties.IsLeftButtonPressed)
tabs.Activate(tabs.Tabs.ToList().IndexOf(tab));
};
return border;
}
private async Task<bool> CloseTabAsync(NestTab tab)
{
if (tab == nestingTab)
{
ShowNotice($"Auto Nest is running on {tab.Name}. Stop it before closing the nest.");
return false;
}
if (tab.Modified && !await ConfirmDialog.AskAsync(this, "Close Nest",
$"{tab.Name} has Auto Nest results that are not saved. This version of "
+ "OpenNest cannot save nests yet, so closing it discards them.",
"Discard and Close"))
return false;
tabs.Close(tab);
views.Remove(tab);
return true;
}
private async void OnCloseNest(object sender, RoutedEventArgs e)
{
if (tabs.Active != null)
await CloseTabAsync(tabs.Active);
}
private async void OnCloseAll(object sender, RoutedEventArgs e)
{
foreach (var tab in tabs.Tabs.Reverse().ToList())
{
if (!await CloseTabAsync(tab))
return;
}
}
private async void OnWindowClosing(object sender, WindowClosingEventArgs e)
{
if (closeConfirmed || !tabs.AnyModified)
{
nesting?.Cancel();
return;
}
e.Cancel = true;
var count = tabs.Tabs.Count(t => t.Modified);
var what = count == 1 ? "One open nest has" : $"{count} open nests have";
if (await ConfirmDialog.AskAsync(this, "Exit OpenNest",
$"{what} Auto Nest results that are not saved. This version of OpenNest "
+ "cannot save nests yet, so exiting discards them.",
"Discard and Exit"))
{
closeConfirmed = true;
Close();
}
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab && e.KeyModifiers.HasFlag(KeyModifiers.Control))
{
StepNest(e.KeyModifiers.HasFlag(KeyModifiers.Shift) ? -1 : 1);
e.Handled = true;
}
}
private void OnNextNest(object sender, RoutedEventArgs e) => StepNest(1);
private void OnPreviousNest(object sender, RoutedEventArgs e) => StepNest(-1);
private void StepNest(int step)
{
var count = tabs.Tabs.Count;
if (count > 1)
tabs.Activate(((tabs.ActiveIndex + step) % count + count) % count);
}
private async void OnOpen(object sender, RoutedEventArgs e)
{
if (nesting != null)
return;
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open Nest",
AllowMultiple = false,
AllowMultiple = true,
FileTypeFilter = new[] { new FilePickerFileType("Nest files") { Patterns = new[] { "*.nest" } } },
});
var path = files.FirstOrDefault()?.TryGetLocalPath();
if (path != null)
foreach (var path in files.Select(f => f.TryGetLocalPath()).Where(p => p != null))
OpenNest(path);
}
@@ -133,16 +295,21 @@ namespace OpenNest.Avalonia
}
/// <summary>
/// Nests every drawing's remaining quantity on sheets like the shown plate through the
/// shared pipeline, off the UI thread. Open is refused until it finishes, so the nest
/// and its drawings stay stable from the request to the commit.
/// Nests every drawing's remaining quantity of the active nest on sheets like its shown
/// plate, through the shared pipeline off the UI thread. Other nests can be opened and
/// viewed meanwhile; the nest being nested cannot be closed until it finishes.
/// </summary>
private async Task RunAutoNestAsync()
{
if (nest == null || nesting != null)
var view = ActiveView;
if (view == null || nesting != null)
return;
var tab = view.Tab;
var nest = tab.Document.Nest;
var engineName = EngineSelection.EngineName;
var template = Canvas.Plate ?? nest.Plates.FirstOrDefault() ?? nest.PlateDefaults.CreateNew();
var template = view.PlateView.Plate
?? nest.Plates.FirstOrDefault()
?? nest.PlateDefaults.CreateNew();
var request = AutoNestJob.ForRemaining(nest, template, engineName);
if (request == null)
{
@@ -151,6 +318,7 @@ namespace OpenNest.Avalonia
}
nesting = new CancellationTokenSource();
nestingTab = tab;
var token = nesting.Token;
SetNesting(true);
ShowNotice($"{engineName}: starting...");
@@ -168,8 +336,14 @@ namespace OpenNest.Avalonia
var firstNew = nest.Plates.Count(p => p.Parts.Count > 0);
var kept = AutoNestJob.Keep(nest, result);
if (kept.Nested)
RefreshPlateList(firstNew);
ShowNotice(kept.Nested ? $"{engineName}: {kept.Note}" : AutoNestJob.RefusalReason(result));
{
tab.MarkModified();
view.Refresh(firstNew);
RebuildTabStrip();
}
ShowNotice(kept.Nested
? $"{tab.Name}: {engineName}: {kept.Note}"
: AutoNestJob.RefusalReason(result));
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
@@ -183,6 +357,7 @@ namespace OpenNest.Avalonia
{
nesting.Dispose();
nesting = null;
nestingTab = null;
SetNesting(false);
}
}
@@ -191,58 +366,45 @@ namespace OpenNest.Avalonia
{
AutoNestButton.Content = running ? "Stop" : "Auto Nest";
AutoNestMenuItem.Header = running ? "_Stop Auto Nest" : "_Auto Nest";
OpenButton.IsEnabled = !running;
OpenMenuItem.IsEnabled = !running;
}
private void ShowNotice(string text)
{
notice = text;
UpdateStatus();
if (Canvas.Plate == null)
Status.Text = text;
}
private void OnFit(object sender, RoutedEventArgs e) => Canvas.ZoomToFit();
private void OnFit(object sender, RoutedEventArgs e) => ActiveView?.PlateView.ZoomToFit();
private void OnPreviousPlate(object sender, RoutedEventArgs e) => StepPlate(-1);
private void OnNextPlate(object sender, RoutedEventArgs e) => StepPlate(1);
private void StepPlate(int step)
{
if (nest == null || nest.Plates.Count == 0)
return;
PlateList.SelectedIndex = System.Math.Clamp(PlateList.SelectedIndex + step, 0, nest.Plates.Count - 1);
}
private void OnPlateSelected(object sender, SelectionChangedEventArgs e)
{
var index = PlateList.SelectedIndex;
var plate = nest != null && index >= 0 ? nest.Plates[index] : null;
Canvas.Plate = plate;
DrawingGrid.ItemsSource = plate?.Parts
.Where(part => !part.BaseDrawing.IsCutOff)
.GroupBy(part => part.BaseDrawing)
.Select(group => new DrawingRow(group.Key.Name, group.Count(), SizeOf(group.First())))
.OrderByDescending(row => row.Count)
.ToList();
UpdateStatus();
}
private static string SizeOf(Part part) =>
$"{part.BoundingBox.Length:0.###} x {part.BoundingBox.Width:0.###}";
private void StepPlate(int step) => ActiveView?.StepPlate(step);
private void UpdateStatus()
{
if (Canvas.Plate == null)
var view = ActiveView;
var canvas = view?.PlateView;
if (canvas?.Plate == null)
{
var empty = view == null
? "Open a .nest file to begin."
: $"{view.Tab.Name} No plates";
Status.Text = notice ?? empty;
return;
var text = $"{fileName} Plate {PlateList.SelectedIndex + 1} of {nest.Plates.Count} Zoom {Canvas.Zoom:0.##} px/unit";
if (Canvas.CursorWorld is { } at)
}
var text = $"{view.Tab.Name} Plate {view.PlateIndex + 1} of {view.PlateCount}"
+ $" Zoom {canvas.Zoom:0.##} px/unit";
if (canvas.CursorWorld is { } at)
text += $" X {at.X:0.000} Y {at.Y:0.000}";
var part = Canvas.HoveredPart ?? Canvas.SelectedPart;
var part = canvas.HoveredPart ?? canvas.SelectedPart;
if (part != null)
text += $" {(ReferenceEquals(part, Canvas.SelectedPart) ? "Selected" : "Over")}: {part.BaseDrawing.Name}";
{
var how = ReferenceEquals(part, canvas.SelectedPart) ? "Selected" : "Over";
text += $" {how}: {part.BaseDrawing.Name}";
}
if (notice != null)
text += $" | {notice}";
Status.Text = text;
+41
View File
@@ -0,0 +1,41 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="using:OpenNest.Avalonia.Controls"
xmlns:local="using:OpenNest.Avalonia"
x:Class="OpenNest.Avalonia.NestView">
<Grid ColumnDefinitions="290,4,*">
<Border Grid.Column="0" Background="White">
<TabControl x:Name="SideTabs" Padding="0">
<TabItem Header="Plates" FontSize="14" MinHeight="34">
<ListBox x:Name="PlateList" SelectionChanged="OnPlateSelected" />
</TabItem>
<TabItem Header="Drawings" FontSize="14" MinHeight="34">
<DataGrid x:Name="DrawingGrid" x:DataType="local:DrawingRow" IsReadOnly="True"
FontSize="13" AutoGenerateColumns="False"
GridLinesVisibility="Horizontal" CanUserSortColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Drawing" Binding="{Binding Name}" Width="*" />
<DataGridTextColumn Header="Nested" Binding="{Binding Nested}" />
<DataGridTextColumn Header="Required" Binding="{Binding Required}" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
</TabControl>
</Border>
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Background="#D0D4D8" />
<DockPanel Grid.Column="2">
<Border DockPanel.Dock="Top" Padding="8,4" Background="White"
BorderBrush="#D0D4D8" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Content="&#x23EE;" ToolTip.Tip="First plate" Click="OnFirstPlate" />
<Button Content="&#x25C0;" ToolTip.Tip="Previous plate (Page Up)"
Click="OnPreviousPlate" />
<TextBlock x:Name="PlateLabel" VerticalAlignment="Center" FontSize="13" Margin="6,0" />
<Button Content="&#x25B6;" ToolTip.Tip="Next plate (Page Down)" Click="OnNextPlate" />
<Button Content="&#x23ED;" ToolTip.Tip="Last plate" Click="OnLastPlate" />
</StackPanel>
</Border>
<c:PlateCanvas x:Name="Canvas" />
</DockPanel>
</Grid>
</UserControl>
+95
View File
@@ -0,0 +1,95 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using OpenNest.Avalonia.Controls;
using OpenNest.Desktop;
namespace OpenNest.Avalonia
{
public sealed record DrawingRow(string Name, int Nested, int Required);
/// <summary>
/// One open nest: Plates and Drawings tabs on the left, the plate header and plate view on
/// the right. Each tab keeps its own view, so switching nests keeps each one's plate and zoom.
/// </summary>
public partial class NestView : UserControl
{
public NestView()
{
InitializeComponent();
}
public NestView(NestTab tab)
: this()
{
Tab = tab;
Canvas.ViewChanged += (_, _) => ViewChanged?.Invoke(this, EventArgs.Empty);
Refresh(0);
}
/// <summary>Raised when the shown plate, zoom, hover or selection changes.</summary>
public event EventHandler ViewChanged;
public NestTab Tab { get; }
public PlateCanvas PlateView => Canvas;
public int PlateIndex => PlateList.SelectedIndex;
public int PlateCount => Tab.Document.Nest.Plates.Count;
/// <summary>Rebuilds both lists and shows plate <paramref name="selectIndex"/>.</summary>
public void Refresh(int selectIndex)
{
var nest = Tab.Document.Nest;
Canvas.ClearCache();
PlateList.ItemsSource = nest.Plates
.Select((plate, i) =>
$"Plate {i + 1} {SizeText(plate)} ({plate.Parts.Count} parts)")
.ToList();
PlateList.SelectedIndex = nest.Plates.Count > 0
? System.Math.Clamp(selectIndex, 0, nest.Plates.Count - 1)
: -1;
DrawingGrid.ItemsSource = nest.Drawings
.Where(d => !d.IsCutOff)
.Select(d => new DrawingRow(d.Name, d.Quantity.Nested, d.Quantity.Required))
.ToList();
ShowPlate();
}
public void ShowPlate(int index)
{
if (PlateCount > 0)
PlateList.SelectedIndex = System.Math.Clamp(index, 0, PlateCount - 1);
}
public void StepPlate(int step) => ShowPlate(PlateList.SelectedIndex + step);
private void OnPlateSelected(object sender, SelectionChangedEventArgs e) => ShowPlate();
private void ShowPlate()
{
var nest = Tab.Document.Nest;
var index = PlateList.SelectedIndex;
var plate = index >= 0 && index < nest.Plates.Count ? nest.Plates[index] : null;
Canvas.Plate = plate;
PlateLabel.Text = plate == null
? "No plates"
: $"Plate {index + 1} of {nest.Plates.Count} "
+ $"{SizeText(plate)} qty {plate.Quantity}";
ViewChanged?.Invoke(this, EventArgs.Empty);
}
private static string SizeText(Plate plate) =>
$"{plate.Size.Length:0.###} x {plate.Size.Width:0.###}";
private void OnFirstPlate(object sender, RoutedEventArgs e) => ShowPlate(0);
private void OnPreviousPlate(object sender, RoutedEventArgs e) => StepPlate(-1);
private void OnNextPlate(object sender, RoutedEventArgs e) => StepPlate(1);
private void OnLastPlate(object sender, RoutedEventArgs e) => ShowPlate(PlateCount - 1);
}
}