Files
OpenNest/OpenNest.Avalonia/MainWindow.axaml.cs
T
ajandClaude Opus 5.5 3e767ca3b6 feat(avalonia): prototype a cross-platform plate viewer
Adds OpenNest.Avalonia, a throwaway Avalonia 11 app kept out of
OpenNest.sln, to gauge a Linux port. It reads nests through
OpenNest.IO and draws plates with Skia: a port of the desktop's
display paths (cuts, etches, manual lead-ins, filled material),
pan, cursor zoom, fit, hover and selection, beside a plate list and
a drawings DataGrid. --snapshot renders the window to a PNG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:12:18 -04:00

140 lines
5.2 KiB
C#

using System;
using System.IO;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using OpenNest.IO;
namespace OpenNest.Avalonia
{
public sealed record DrawingRow(string Name, int Count, string Size);
public partial class MainWindow : Window
{
private Nest nest;
private string fileName;
public MainWindow()
{
InitializeComponent();
Canvas.ViewChanged += (_, _) => UpdateStatus();
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.O, KeyModifiers.Control),
Command = new Command(() => OnOpen(null, null)),
});
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)) });
}
public void OpenNest(string path)
{
try
{
nest = new NestReader(path).Read();
}
catch (Exception ex)
{
Status.Text = $"Could not open {Path.GetFileName(path)}: {ex.Message}";
return;
}
fileName = Path.GetFileName(path);
Title = $"{fileName} - OpenNest (Avalonia prototype)";
Canvas.ClearCache();
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 ? 0 : -1;
}
public void SaveSnapshotAndClose(string path, int plateNumber)
{
Opened += async (_, _) =>
{
if (nest?.Plates.Count > 0)
PlateList.SelectedIndex = System.Math.Clamp(plateNumber - 1, 0, nest.Plates.Count - 1);
await System.Threading.Tasks.Task.Delay(500);
var size = new global::Avalonia.PixelSize((int)Bounds.Width, (int)Bounds.Height);
using var bitmap = new global::Avalonia.Media.Imaging.RenderTargetBitmap(size);
bitmap.Render(this);
bitmap.Save(path);
Close();
};
}
private async void OnOpen(object sender, RoutedEventArgs e)
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open Nest",
AllowMultiple = false,
FileTypeFilter = new[] { new FilePickerFileType("Nest files") { Patterns = new[] { "*.nest" } } },
});
var path = files.FirstOrDefault()?.TryGetLocalPath();
if (path != null)
OpenNest(path);
}
private void OnExit(object sender, RoutedEventArgs e) => Close();
private void OnFit(object sender, RoutedEventArgs e) => Canvas.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 UpdateStatus()
{
if (Canvas.Plate == null)
return;
var text = $"{fileName} Plate {PlateList.SelectedIndex + 1} of {nest.Plates.Count} 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;
if (part != null)
text += $" {(ReferenceEquals(part, Canvas.SelectedPart) ? "Selected" : "Over")}: {part.BaseDrawing.Name}";
Status.Text = text;
}
private sealed class Command : System.Windows.Input.ICommand
{
private readonly Action action;
public Command(Action action) => this.action = action;
public event EventHandler CanExecuteChanged { add { } remove { } }
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => action();
}
}
}