From d95058238039d9315b94d297268cd3d3e2beb352 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Fri, 5 Dec 2025 23:55:48 +0100 Subject: [PATCH 1/4] split PalletBuilderViewModel into 3 viewmodels for each tab --- App.xaml.cs | 3 + Infrastructure/EventAggregator.cs | 48 ++ ViewModels/Pages/LayerAnalyzerViewModel.cs | 399 ++++++++++++ ViewModels/Pages/PalletAnalyzerViewModel.cs | 32 + .../Pages/PalletBuilderSettingsViewModel.cs | 213 +++++++ ViewModels/Pages/PalletBuilderViewModel.cs | 588 +----------------- Views/Pages/PalletBuilderPage.xaml | 61 +- Views/Pages/PalletBuilderPage.xaml.cs | 20 +- 8 files changed, 739 insertions(+), 625 deletions(-) create mode 100644 Infrastructure/EventAggregator.cs create mode 100644 ViewModels/Pages/LayerAnalyzerViewModel.cs create mode 100644 ViewModels/Pages/PalletAnalyzerViewModel.cs create mode 100644 ViewModels/Pages/PalletBuilderSettingsViewModel.cs diff --git a/App.xaml.cs b/App.xaml.cs index 1082fd9..2e89a34 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting; using Stack_Solver.Data; using Stack_Solver.Data.Repositories; +using Stack_Solver.Infrastructure; using Stack_Solver.Services; using Stack_Solver.ViewModels.Pages; using Stack_Solver.ViewModels.Windows; @@ -61,6 +62,8 @@ public partial class App services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddDbContextFactory(options => { options.UseSqlite($"Data Source={AppPaths.DatabaseFile}"); diff --git a/Infrastructure/EventAggregator.cs b/Infrastructure/EventAggregator.cs new file mode 100644 index 0000000..99d61cb --- /dev/null +++ b/Infrastructure/EventAggregator.cs @@ -0,0 +1,48 @@ +namespace Stack_Solver.Infrastructure +{ + public interface IEventAggregator + { + void Publish(TMessage message); + void Subscribe(Action handler); + void Unsubscribe(Action handler); + } + + public class EventAggregator : IEventAggregator + { + private readonly Dictionary> _handlers = []; + + public void Publish(TMessage message) + { + var t = typeof(TMessage); + if (_handlers.TryGetValue(t, out var list)) + { + foreach (var h in list.ToArray()) + { + try { ((Action)h).Invoke(message); } + catch { } + } + } + } + + public void Subscribe(Action handler) + { + var t = typeof(TMessage); + if (!_handlers.TryGetValue(t, out var list)) + { + list = []; + _handlers[t] = list; + } + if (!list.Contains(handler)) list.Add(handler); + } + + public void Unsubscribe(Action handler) + { + var t = typeof(TMessage); + if (_handlers.TryGetValue(t, out var list)) + { + list.Remove(handler); + if (list.Count == 0) _handlers.Remove(t); + } + } + } +} diff --git a/ViewModels/Pages/LayerAnalyzerViewModel.cs b/ViewModels/Pages/LayerAnalyzerViewModel.cs new file mode 100644 index 0000000..a4cc19c --- /dev/null +++ b/ViewModels/Pages/LayerAnalyzerViewModel.cs @@ -0,0 +1,399 @@ +using Stack_Solver.Helpers.Rendering; +using Stack_Solver.Infrastructure; +using Stack_Solver.Models; +using Stack_Solver.Models.Layering; +using Stack_Solver.Models.Supports; +using Stack_Solver.Services; +using Stack_Solver.Services.Layering; +using System.Collections.ObjectModel; +using System.Text; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Media3D; + +namespace Stack_Solver.ViewModels.Pages +{ + public partial class LayerAnalyzerViewModel : ObservableObject + { + private readonly IEventAggregator _events; + private readonly LayerSceneBuilder _sceneBuilder = new(); + private CancellationTokenSource? _sceneBuildCts; + private CancellationTokenSource? _generationCts; + + [ObservableProperty] + private string _layerGenStats = "Click on 'Generate' to start layer generation."; + + [ObservableProperty] + private bool _isGenerating; + + [ObservableProperty] + private ObservableCollection _layers = []; + + [ObservableProperty] + private Layer? _selectedLayer; + + [ObservableProperty] + private bool _hasLayers; + + [ObservableProperty] + private string _outputText = string.Empty; + + [ObservableProperty] + private ObservableCollection _layerRectangles = []; + + [ObservableProperty] + private bool _showGeometryOptimized; + + public bool CanOptimizeGeometry + { + get + { + var l = SelectedLayer; + if (l == null) return false; + var name = l.Name?.ToLowerInvariant() ?? string.Empty; + bool isBlf = name.Contains("blf"); + bool isStrip = name.Contains("strip"); + return isBlf || isStrip; + } + } + + public Model3DGroup Scene { get; } = new(); + private ViewportController? _viewportController; + public ViewportController? ViewportController => _viewportController; + + private Layer? _optimizedViewLayer; + private static List? _allLayers = []; + + public LayerAnalyzerViewModel(IEventAggregator events) + { + _events = events; + _events.Subscribe(OnSettingsChanged); + ZoomCommand = new RelayCommand(Zoom); + BeginPanCommand = new RelayCommand(BeginPan); + PanCommand = new RelayCommand(Pan); + } + + private int _palletLength; + private int _palletWidth; + private double _palletHeight; + private bool _useCpsat; + private int _maxCpsatCandidates; + private int _solverTimeLimit; + private List _selectedSkus = new(); + + private void OnSettingsChanged(SettingsChangedMessage msg) + { + _palletLength = msg.PalletLength; + _palletWidth = msg.PalletWidth; + _palletHeight = msg.PalletHeight; + _useCpsat = msg.UseCpsat; + _maxCpsatCandidates = msg.MaxCpsatCandidates; + _solverTimeLimit = msg.SolverTimeLimit; + _selectedSkus = [.. msg.Skus.Where(s => s.Quantity > 0)]; + RecenterCameraTarget(); + if (SelectedLayer != null) + { + _ = UpdateSceneForLayerAsync(SelectedLayer); + Update2DPreview(); + } + } + + public ICommand ZoomCommand { get; } + public ICommand BeginPanCommand { get; } + public ICommand PanCommand { get; } + + private void Zoom(double delta) => ViewportController?.Zoom(delta); + private void BeginPan(Point p) => ViewportController?.BeginPan(p); + private void Pan(Point p) => ViewportController?.Pan(p); + + public void AttachCamera(PerspectiveCamera camera) + { + if (camera == null) return; + if (_viewportController == null) + { + _viewportController = new ViewportController(camera, CurrentPalletCenter); + OnPropertyChanged(nameof(ViewportController)); + } + else + { + _viewportController.Target = CurrentPalletCenter; + } + } + + private Point3D CurrentPalletCenter => new(_palletLength / 2.0, 0, _palletWidth / 2.0); + + private void RecenterCameraTarget() + { + if (_viewportController != null) + { + _viewportController.Target = CurrentPalletCenter; + } + } + + partial void OnSelectedLayerChanged(Layer? value) + { + _optimizedViewLayer = null; + OnPropertyChanged(nameof(CanOptimizeGeometry)); + if (value != null) + { + if (!CanOptimizeGeometry && ShowGeometryOptimized) + ShowGeometryOptimized = false; + + if (SelectedLayer != null) + { + OutputText = BuildLayerText(SelectedLayer); + _ = UpdateSceneForLayerAsync(SelectedLayer); + Update2DPreview(); + } + } + } + + partial void OnShowGeometryOptimizedChanged(bool value) + { + if (SelectedLayer != null) + { + _optimizedViewLayer = null; + _ = UpdateSceneForLayerAsync(SelectedLayer); + OutputText = BuildLayerText(SelectedLayer); + Update2DPreview(); + } + } + + private async Task UpdateSceneForLayerAsync(Layer layer) + { + _sceneBuildCts?.Cancel(); + _sceneBuildCts?.Dispose(); + _sceneBuildCts = new CancellationTokenSource(); + var ct = _sceneBuildCts.Token; + try + { + await _sceneBuilder.BuildAsync(Scene, layer, _palletLength, _palletWidth, _palletHeight, ct); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + OutputText = $"Scene build error: {ex.Message}"; + } + } + + [RelayCommand] + private async Task Generate() + { + if (IsGenerating) + return; + + var localCts = new CancellationTokenSource(); + _generationCts = localCts; + IsGenerating = true; + HasLayers = false; + Layers.Clear(); + SelectedLayer = null; + OutputText = "Generating..."; + try + { + if (_selectedSkus.Count == 0) + { + OutputText = "No SKUs with quantity > 0."; + return; + } + + var pallet = new Pallet("Pallet", _palletLength, _palletWidth, (int)Math.Round(_palletHeight)); + var options = new GenerationOptions(_solverTimeLimit, _maxCpsatCandidates); + var ct = localCts.Token; + + var strategiesList = new List + { + new BLFGenerationStrategy(), + new HomogeneousGenerationStrategy(), + new StripFillGenerationStrategy(), + new RadialPlacementGenerationStrategy() + }; + + if (_useCpsat) + { + strategiesList.Add(new CPSATGenerationStrategy()); + } + var strategies = strategiesList.ToArray(); + + _allLayers = await Task.Run(() => + { + var aggregate = new List(); + foreach (var strat in strategies) + { + if (ct.IsCancellationRequested) break; + try + { + var produced = strat.Generate(_selectedSkus, pallet, options); + if (produced != null && produced.Count > 0) + aggregate.AddRange(produced); + } + catch (OperationCanceledException) { throw; } + catch { } + } + return aggregate; + }, ct); + + if (ct.IsCancellationRequested) return; + + if (_allLayers == null || _allLayers.Count == 0) + { + OutputText = "No layers generated."; + return; + } + + _allLayers = [.. _allLayers + .Where(l => + l?.Metadata != null && + !double.IsNaN(l.Metadata.Utilization) && + !double.IsInfinity(l.Metadata.Utilization) && + l.Metadata.Utilization > 0.0 && + l.Metadata.Utilization <= 1.0)]; + + foreach (var layer in _allLayers) + LayerGeometryOptimizer.CenterLayer(layer); + + var topLayers = _allLayers + .OrderByDescending(l => l.Metadata.Utilization) + .ThenBy(l => l.Name) + .Take(10) + .ToList(); + + foreach (var layer in topLayers) + Layers.Add(layer); + + HasLayers = Layers.Count > 0; + + SelectedLayer = Layers.OrderByDescending(l => l.Metadata.Utilization).FirstOrDefault(); + + if (SelectedLayer == null) + { + OutputText = "No layers after filtering."; + } + + LayerGenStats = $"Generated {_allLayers.Count} candidate layers using"; + foreach (var strat in strategies) + { + LayerGenStats += $" {strat.Name},"; + } + LayerGenStats = LayerGenStats.TrimEnd(',') + "."; + + // notify pallet analyzer + _events.Publish(new LayersGeneratedMessage(_allLayers)); + } + catch (OperationCanceledException) + { + OutputText = "Generation canceled."; + } + catch (Exception ex) + { + OutputText = $"Error: {ex.Message}"; + } + finally + { + IsGenerating = false; + _generationCts?.Dispose(); + _generationCts = null; + } + } + + [RelayCommand] + private void Cancel() + { + if (_generationCts != null && !_generationCts.IsCancellationRequested) + { + _generationCts.Cancel(); + OutputText = "Canceling..."; + } + } + + private static string BuildLayerText(Layer layer) + { + var sb = new StringBuilder(); + sb.AppendLine($"{layer.Name}\n"); + sb.AppendLine($"Utilization: {layer.Metadata.Utilization:F3}"); + sb.AppendLine($"Height: {layer.Metadata.Height}"); + double totalWeight = 0; + foreach (var g in layer.Items) + { + totalWeight += g.SkuType.Weight; + } + sb.AppendLine($"Total weight: {totalWeight} kg"); + sb.AppendLine($"Total placed items: {layer.Items.Count}"); + foreach (var g in layer.Items.GroupBy(i => i.SkuType.SkuId)) + { + var sku = g.First().SkuType; + sb.AppendLine($" {sku.Name} x {g.Count()} [{sku.Length}x{sku.Width}x{sku.Height}]"); + } + sb.AppendLine("=================="); + sb.AppendLine("Full details are included in the PDF report."); + + return sb.ToString(); + } + + private Model3DGroup? _selectionHighlight; + + public void UpdateSelectedItem(PositionedItem? item) + { + if (item?.SkuType != null) + { + var sku = item.SkuType; + SelectedItemInfo = $" > {sku.Name} ({sku.Length}x{sku.Width}x{sku.Height}) positioned at {item.X}, {item.Y}"; + HighlightItem(item); + } + else + { + SelectedItemInfo = string.Empty; + HighlightItem(null); + } + } + + [ObservableProperty] + private string _selectedItemInfo = string.Empty; + + private void HighlightItem(PositionedItem? item) + { + if (_selectionHighlight != null) + { + Scene.Children.Remove(_selectionHighlight); + _selectionHighlight = null; + } + if (item == null) return; + var sku = item.SkuType; + double boxLength = item.Rotated ? sku.Width : sku.Length; + double boxWidth = item.Rotated ? sku.Length : sku.Width; + double boxHeight = sku.Height; + double inflate = 0.6; + var origin = new Point3D(item.X - inflate / 2.0, _palletHeight + 0.01, item.Y - inflate / 2.0); + var fillBrush = new SolidColorBrush(Color.FromArgb(40, 255, 255, 0)); + var edgeColor = Colors.Yellow; + _selectionHighlight = GeometryCreator.CreateBoxWithEdges(origin, boxLength + inflate, boxHeight + inflate, boxWidth + inflate, fillBrush, edgeColor, 0.6); + Scene.Children.Add(_selectionHighlight); + } + + private void Update2DPreview() + { + if (SelectedLayer == null) + { + LayerRectangles.Clear(); + return; + } + + var layer = SelectedLayer; + var pallet = new Pallet("Pallet", _palletLength, _palletWidth, (int)Math.Round(_palletHeight)); + LayerGeometryBuilder.Build(layer, pallet, 1); + + LayerRectangles.Clear(); + if (layer.Geometry?.ItemRectangles != null) + { + double canvasHeight = _palletWidth; + foreach (var r in layer.Geometry.ItemRectangles) + { + var display = new Rect(r.X, canvasHeight - (r.Y + r.Height), r.Width, r.Height); + LayerRectangles.Add(display); + } + } + } + } + + public record LayersGeneratedMessage(List Layers); +} diff --git a/ViewModels/Pages/PalletAnalyzerViewModel.cs b/ViewModels/Pages/PalletAnalyzerViewModel.cs new file mode 100644 index 0000000..67c58ed --- /dev/null +++ b/ViewModels/Pages/PalletAnalyzerViewModel.cs @@ -0,0 +1,32 @@ +using Stack_Solver.Infrastructure; +using Stack_Solver.Models.Layering; +using System.Collections.ObjectModel; + +namespace Stack_Solver.ViewModels.Pages +{ + public partial class PalletAnalyzerViewModel : ObservableObject + { + private readonly IEventAggregator _events; + + [ObservableProperty] + private string _outputText = string.Empty; + + [ObservableProperty] + private ObservableCollection _candidateLayers = []; + + public PalletAnalyzerViewModel(IEventAggregator events) + { + _events = events; + _events.Subscribe(OnLayersGenerated); + } + + private void OnLayersGenerated(LayersGeneratedMessage msg) + { + CandidateLayers.Clear(); + foreach (var l in msg.Layers) + CandidateLayers.Add(l); + + OutputText = $"{CandidateLayers.Count} candidate layers received."; + } + } +} diff --git a/ViewModels/Pages/PalletBuilderSettingsViewModel.cs b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs new file mode 100644 index 0000000..5d5849c --- /dev/null +++ b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs @@ -0,0 +1,213 @@ +using Stack_Solver.Data.Repositories; +using Stack_Solver.Infrastructure; +using Stack_Solver.Models; +using Stack_Solver.Models.Supports; +using System.Collections.ObjectModel; + +namespace Stack_Solver.ViewModels.Pages +{ + public partial class PalletBuilderSettingsViewModel : ObservableObject + { + private readonly ISkuRepository _skuRepository; + private readonly IEventAggregator _events; + private bool _isInitialized; + + [ObservableProperty] + private ObservableCollection _skus = []; + + [ObservableProperty] + private int _palletLength = 120; + + [ObservableProperty] + private int _palletWidth = 80; + + [ObservableProperty] + private double _palletHeight = 14.4; + + [ObservableProperty] + private bool _useCpsat; + + [ObservableProperty] + private int _maxCpsatCandidates = 2000; + + [ObservableProperty] + private int _solverTimeLimit = 60; + + public ObservableCollection CommonPalletsInternational { get; } = []; + public ObservableCollection CommonPalletsAmerica { get; } = []; + + private Pallet? _selectedInternationalPallet; + public Pallet? SelectedInternationalPallet + { + get => _selectedInternationalPallet; + set + { + if (SetProperty(ref _selectedInternationalPallet, value) && value is not null) + { + SelectPallet(value); + } + } + } + + private Pallet? _selectedAmericanPallet; + public Pallet? SelectedAmericanPallet + { + get => _selectedAmericanPallet; + set + { + if (SetProperty(ref _selectedAmericanPallet, value) && value is not null) + { + SelectPallet(value); + } + } + } + + public PalletBuilderSettingsViewModel(ISkuRepository skuRepository, IEventAggregator events) + { + _skuRepository = skuRepository; + _events = events; + _skuRepository.SkuAdded += OnSkuAdded; + _skuRepository.SkuUpdated += OnSkuUpdated; + _skuRepository.SkuDeleted += OnSkuDeleted; + } + + public async Task InitializeAsync() + { + if (_isInitialized) return; + var list = await _skuRepository.GetAllAsync(); + Skus = new ObservableCollection(list); + + if (CommonPalletsInternational.Count == 0) + { + foreach (var p in PalletCatalog.International) + CommonPalletsInternational.Add(p); + } + + if (CommonPalletsAmerica.Count == 0) + { + foreach (var p in PalletCatalog.America) + CommonPalletsAmerica.Add(p); + } + + _isInitialized = true; + PublishSettingsChanged(); + } + + [RelayCommand] + private void SelectPallet(Pallet? pallet) + { + if (pallet is null) return; + PalletLength = pallet.Length; + PalletWidth = pallet.Width; + PublishSettingsChanged(); + } + + public async Task UpdateSkuAsync(SKU sku, CancellationToken ct = default) + { + if (sku == null) return; + await _skuRepository.UpdateAsync(sku, ct); + PublishSettingsChanged(); + } + + partial void OnPalletLengthChanged(int value) => PublishSettingsChanged(); + partial void OnPalletWidthChanged(int value) => PublishSettingsChanged(); + partial void OnPalletHeightChanged(double value) => PublishSettingsChanged(); + partial void OnUseCpsatChanged(bool value) => PublishSettingsChanged(); + partial void OnMaxCpsatCandidatesChanged(int value) => PublishSettingsChanged(); + partial void OnSolverTimeLimitChanged(int value) => PublishSettingsChanged(); + + private void PublishSettingsChanged() + { + _events.Publish(new SettingsChangedMessage( + PalletLength, PalletWidth, PalletHeight, + UseCpsat, MaxCpsatCandidates, SolverTimeLimit, [.. Skus])); + } + + private void OnSkuAdded(object? sender, SKU sku) + { + if (!Skus.Any(s => s.SkuId == sku.SkuId)) + { + App.Current?.Dispatcher.BeginInvoke(() => Skus.Add(sku)); + } + } + + private void OnSkuUpdated(object? sender, SKU sku) + { + var existing = Skus.FirstOrDefault(s => s.SkuId == sku.SkuId); + if (existing != null) + { + App.Current?.Dispatcher.BeginInvoke(() => + { + existing.Name = sku.Name; + existing.Length = sku.Length; + existing.Width = sku.Width; + existing.Height = sku.Height; + existing.Weight = sku.Weight; + existing.Notes = sku.Notes; + existing.Rotatable = sku.Rotatable; + }); + } + else + { + OnSkuAdded(sender, sku); + } + } + + private async void OnSkuDeleted(object? sender, string skuId) + { + try + { + await App.Current!.Dispatcher.InvokeAsync(async () => + { + var existing = Skus.FirstOrDefault(s => string.Equals(s.SkuId, skuId, StringComparison.OrdinalIgnoreCase)); + if (existing != null) + { + Skus.Remove(existing); + } + else + { + var latest = await _skuRepository.GetAllAsync(); + SyncSkuCollection(latest); + } + }); + } + catch { } + } + + private void SyncSkuCollection(IList latest) + { + for (int i = Skus.Count - 1; i >= 0; i--) + { + if (!latest.Any(s => s.SkuId == Skus[i].SkuId)) + Skus.RemoveAt(i); + } + foreach (var sku in latest) + { + var existing = Skus.FirstOrDefault(s => s.SkuId == sku.SkuId); + if (existing == null) + { + Skus.Add(sku); + } + else + { + existing.Name = sku.Name; + existing.Length = sku.Length; + existing.Width = sku.Width; + existing.Height = sku.Height; + existing.Weight = sku.Weight; + existing.Notes = sku.Notes; + existing.Rotatable = sku.Rotatable; + } + } + } + } + + public record SettingsChangedMessage( + int PalletLength, + int PalletWidth, + double PalletHeight, + bool UseCpsat, + int MaxCpsatCandidates, + int SolverTimeLimit, + List Skus); +} diff --git a/ViewModels/Pages/PalletBuilderViewModel.cs b/ViewModels/Pages/PalletBuilderViewModel.cs index 868a2a1..11b1b71 100644 --- a/ViewModels/Pages/PalletBuilderViewModel.cs +++ b/ViewModels/Pages/PalletBuilderViewModel.cs @@ -1,595 +1,19 @@ using Stack_Solver.Data.Repositories; -using Stack_Solver.Helpers.Rendering; -using Stack_Solver.Models; -using Stack_Solver.Models.Layering; -using Stack_Solver.Models.Supports; -using Stack_Solver.Services; -using Stack_Solver.Services.Layering; -using System.Collections.ObjectModel; -using System.Text; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Media3D; +using Stack_Solver.Infrastructure; namespace Stack_Solver.ViewModels.Pages { - public partial class PalletBuilderViewModel : ObservableObject + public partial class PalletBuilderViewModel(ISkuRepository skuRepository, IEventAggregator events) : ObservableObject { - private readonly ISkuRepository _skuRepository; - private bool _isInitialized = false; - private CancellationTokenSource? _generationCts; - - private readonly LayerSceneBuilder _sceneBuilder = new(); - private CancellationTokenSource? _sceneBuildCts; - - [ObservableProperty] - private ObservableCollection _skus = []; - - [ObservableProperty] - private int _palletLength = 120; - - [ObservableProperty] - private int _palletWidth = 80; - - [ObservableProperty] - private double _palletHeight = 14.4; - - [ObservableProperty] - private string _outputText = string.Empty; - - [ObservableProperty] - private bool _isGenerating; - - [ObservableProperty] - private ObservableCollection _layers = []; - - [ObservableProperty] - private Layer? _selectedLayer; - - [ObservableProperty] - private bool _hasLayers; - - [ObservableProperty] - private bool _useCpsat; - - [ObservableProperty] - private int _maxCpsatCandidates = 2000; - - [ObservableProperty] - private int _solverTimeLimit = 60; - - [ObservableProperty] - private string _layerGenStats = "Click on 'Generate' to start layer generation."; - - [ObservableProperty] - private string _selectedItemInfo = string.Empty; - - [ObservableProperty] - private bool _showGeometryOptimized; - public bool CanOptimizeGeometry - { - get - { - var l = SelectedLayer; - if (l == null) return false; - var name = l.Name?.ToLowerInvariant() ?? string.Empty; - bool isBlf = name.Contains("blf"); - bool isStrip = name.Contains("strip"); - return isBlf || isStrip; - } - } - - // Rectangles for 2D preview (in pallet coordinate units) - [ObservableProperty] - private ObservableCollection _layerRectangles = []; - - private ViewportController? _viewportController; - public ViewportController? ViewportController => _viewportController; - - public Model3DGroup Scene { get; } = new(); - - private Pallet? _selectedInternationalPallet; - private Pallet? _selectedAmericanPallet; - private bool _suppressCrossClear; - - private Layer? _optimizedViewLayer; - - private Point3D CurrentPalletCenter => new(PalletLength / 2.0, 0, PalletWidth / 2.0); - - partial void OnPalletLengthChanged(int value) - { - RecenterCameraTarget(); - if (SelectedLayer != null) - { - _ = UpdateSceneForLayerAsync(SelectedLayer); - Update2DPreview(); - } - } - - partial void OnPalletWidthChanged(int value) - { - RecenterCameraTarget(); - if (SelectedLayer != null) - { - _ = UpdateSceneForLayerAsync(SelectedLayer); - Update2DPreview(); - } - } - - partial void OnShowGeometryOptimizedChanged(bool value) - { - if (SelectedLayer != null) - { - _optimizedViewLayer = null; - _ = UpdateSceneForLayerAsync(SelectedLayer); - OutputText = BuildLayerText(SelectedLayer); - Update2DPreview(); - } - } - - private void RecenterCameraTarget() - { - if (_viewportController != null) - { - _viewportController.Target = CurrentPalletCenter; - } - } - - public Pallet? SelectedInternationalPallet - { - get => _selectedInternationalPallet; - set - { - if (SetProperty(ref _selectedInternationalPallet, value)) - { - if (value != null) - { - SelectPallet(value); - if (!_suppressCrossClear && _selectedAmericanPallet != null) - { - try - { - _suppressCrossClear = true; - _selectedAmericanPallet = null; - OnPropertyChanged(nameof(SelectedAmericanPallet)); - } - finally { _suppressCrossClear = false; } - } - } - } - } - } - - public Pallet? SelectedAmericanPallet - { - get => _selectedAmericanPallet; - set - { - if (SetProperty(ref _selectedAmericanPallet, value)) - { - if (value != null) - { - SelectPallet(value); - if (!_suppressCrossClear && _selectedInternationalPallet != null) - { - try - { - _suppressCrossClear = true; - _selectedInternationalPallet = null; - OnPropertyChanged(nameof(SelectedInternationalPallet)); - } - finally { _suppressCrossClear = false; } - } - } - } - } - } - - public ObservableCollection CommonPalletsInternational { get; } = []; - public ObservableCollection CommonPalletsAmerica { get; } = []; - - private static List? allLayers = []; - - partial void OnSelectedLayerChanged(Layer? value) - { - _optimizedViewLayer = null; - OnPropertyChanged(nameof(CanOptimizeGeometry)); - if (value != null) - { - if (!CanOptimizeGeometry && ShowGeometryOptimized) - ShowGeometryOptimized = false; - - OutputText = BuildLayerText(SelectedLayer); - _ = UpdateSceneForLayerAsync(SelectedLayer); - Update2DPreview(); - } - } - - public ICommand ZoomCommand { get; } - public ICommand BeginPanCommand { get; } - public ICommand PanCommand { get; } - - private void Zoom(double delta) => ViewportController?.Zoom(delta); - private void BeginPan(Point p) => ViewportController?.BeginPan(p); - private void Pan(Point p) => ViewportController?.Pan(p); - - public PalletBuilderViewModel(ISkuRepository skuRepository) - { - ZoomCommand = new RelayCommand(Zoom); - BeginPanCommand = new RelayCommand(BeginPan); - PanCommand = new RelayCommand(Pan); - - _skuRepository = skuRepository; - _skuRepository.SkuAdded += OnSkuAdded; - _skuRepository.SkuUpdated += OnSkuUpdated; - _skuRepository.SkuDeleted += OnSkuDeleted; - _ = InitializeAsync(); - } - - public void AttachCamera(PerspectiveCamera camera) - { - if (camera == null) return; - if (_viewportController == null) - { - _viewportController = new ViewportController(camera, CurrentPalletCenter); - OnPropertyChanged(nameof(ViewportController)); - } - else - { - _viewportController.Target = CurrentPalletCenter; - } - } - - private async Task UpdateSceneForLayerAsync(Layer layer) - { - _sceneBuildCts?.Cancel(); - _sceneBuildCts?.Dispose(); - _sceneBuildCts = new CancellationTokenSource(); - var ct = _sceneBuildCts.Token; - try - { - await _sceneBuilder.BuildAsync(Scene, layer, PalletLength, PalletWidth, PalletHeight, ct); - } - catch (OperationCanceledException) { } - catch (Exception ex) - { - OutputText = $"Scene build error: {ex.Message}"; - } - } - - private void OnSkuAdded(object? sender, SKU sku) - { - if (!Skus.Any(s => s.SkuId == sku.SkuId)) - { - App.Current?.Dispatcher.BeginInvoke(() => Skus.Add(sku)); - } - } - - private void OnSkuUpdated(object? sender, SKU sku) - { - var existing = Skus.FirstOrDefault(s => s.SkuId == sku.SkuId); - if (existing != null) - { - App.Current?.Dispatcher.BeginInvoke(() => - { - existing.Name = sku.Name; - existing.Length = sku.Length; - existing.Width = sku.Width; - existing.Height = sku.Height; - existing.Weight = sku.Weight; - existing.Notes = sku.Notes; - existing.Rotatable = sku.Rotatable; - }); - } - else - { - OnSkuAdded(sender, sku); - } - } - - private async void OnSkuDeleted(object? sender, string skuId) - { - try - { - await App.Current!.Dispatcher.InvokeAsync(async () => - { - var existing = Skus.FirstOrDefault(s => string.Equals(s.SkuId, skuId, StringComparison.OrdinalIgnoreCase)); - if (existing != null) - { - Skus.Remove(existing); - } - else - { - var latest = await _skuRepository.GetAllAsync(); - SyncSkuCollection(latest); - } - }); - } - catch { } - } - - private void SyncSkuCollection(IList latest) - { - for (int i = Skus.Count - 1; i >= 0; i--) - { - if (!latest.Any(s => s.SkuId == Skus[i].SkuId)) - Skus.RemoveAt(i); - } - foreach (var sku in latest) - { - var existing = Skus.FirstOrDefault(s => s.SkuId == sku.SkuId); - if (existing == null) - { - Skus.Add(sku); - } - else - { - existing.Name = sku.Name; - existing.Length = sku.Length; - existing.Width = sku.Width; - existing.Height = sku.Height; - existing.Weight = sku.Weight; - existing.Notes = sku.Notes; - existing.Rotatable = sku.Rotatable; - } - } - } + public PalletBuilderSettingsViewModel Settings { get; } = new PalletBuilderSettingsViewModel(skuRepository, events); + public LayerAnalyzerViewModel LayerAnalyzer { get; } = new LayerAnalyzerViewModel(events); + public PalletAnalyzerViewModel PalletAnalyzer { get; } = new PalletAnalyzerViewModel(events); public async Task OnNavigatedToAsync() { - if (!_isInitialized) - await InitializeAsync(); + await Settings.InitializeAsync(); } public static Task OnNavigatedFromAsync() => Task.CompletedTask; - - private async Task InitializeAsync() - { - var list = await _skuRepository.GetAllAsync(); - Skus = new ObservableCollection(list); - - if (CommonPalletsInternational.Count == 0) - { - foreach (var p in PalletCatalog.International) - CommonPalletsInternational.Add(p); - } - - if (CommonPalletsAmerica.Count == 0) - { - foreach (var p in PalletCatalog.America) - CommonPalletsAmerica.Add(p); - } - - _isInitialized = true; - } - - [RelayCommand] - private void SelectPallet(Pallet? pallet) - { - if (pallet is null) return; - PalletLength = pallet.Length; - PalletWidth = pallet.Width; - } - - public async Task UpdateSkuAsync(SKU sku, CancellationToken ct = default) - { - if (sku == null) return; - await _skuRepository.UpdateAsync(sku, ct); - } - - [RelayCommand] - private async Task Generate() - { - if (IsGenerating) - return; - - var localCts = new CancellationTokenSource(); - _generationCts = localCts; - IsGenerating = true; - HasLayers = false; - Layers.Clear(); - SelectedLayer = null; - OutputText = "Generating..."; - try - { - var selectedSkus = Skus.Where(s => s.Quantity > 0).ToList(); - if (selectedSkus.Count == 0) - { - OutputText = "No SKUs with quantity > 0."; - return; - } - - var pallet = new Pallet("Pallet", PalletLength, PalletWidth, (int)Math.Round(PalletHeight)); - var options = new GenerationOptions(SolverTimeLimit, MaxCpsatCandidates); - var ct = localCts.Token; - - var strategiesList = new List - { - new BLFGenerationStrategy(), - new HomogeneousGenerationStrategy(), - new StripFillGenerationStrategy(), - new RadialPlacementGenerationStrategy() - }; - - if (UseCpsat) - { - strategiesList.Add(new CPSATGenerationStrategy()); - } - var strategies = strategiesList.ToArray(); - - allLayers = await Task.Run(() => - { - var aggregate = new List(); - foreach (var strat in strategies) - { - if (ct.IsCancellationRequested) break; - try - { - var produced = strat.Generate(selectedSkus, pallet, options); - if (produced != null && produced.Count > 0) - aggregate.AddRange(produced); - } - catch (OperationCanceledException) { throw; } - catch { } - } - return aggregate; - }, ct); - - if (ct.IsCancellationRequested) return; - - if (allLayers == null || allLayers.Count == 0) - { - OutputText = "No layers generated."; - return; - } - - allLayers = [.. allLayers - .Where(l => - l?.Metadata != null && - !double.IsNaN(l.Metadata.Utilization) && - !double.IsInfinity(l.Metadata.Utilization) && - l.Metadata.Utilization > 0.0 && - l.Metadata.Utilization <= 1.0)]; - - foreach (var layer in allLayers) - LayerGeometryOptimizer.CenterLayer(layer); - - var topLayers = allLayers - .OrderByDescending(l => l.Metadata.Utilization) - .ThenBy(l => l.Name) - .Take(10) - .ToList(); - - foreach (var layer in topLayers) - Layers.Add(layer); - - HasLayers = Layers.Count > 0; - - SelectedLayer = Layers.OrderByDescending(l => l.Metadata.Utilization).FirstOrDefault(); - - if (SelectedLayer == null) - { - OutputText = "No layers after filtering."; - } - - LayerGenStats = $"Generated {allLayers.Count} candidate layers using"; - foreach (var strat in strategies) - { - LayerGenStats += $" {strat.Name},"; - } - LayerGenStats = LayerGenStats.TrimEnd(',') + "."; - } - catch (OperationCanceledException) - { - OutputText = "Generation canceled."; - } - catch (Exception ex) - { - OutputText = $"Error: {ex.Message}"; - } - finally - { - IsGenerating = false; - _generationCts?.Dispose(); - _generationCts = null; - } - } - - [RelayCommand] - private void Cancel() - { - if (_generationCts != null && !_generationCts.IsCancellationRequested) - { - _generationCts.Cancel(); - OutputText = "Canceling..."; - } - } - - private static string BuildLayerText(Layer layer) - { - var sb = new StringBuilder(); - sb.AppendLine($"{layer.Name}\n"); - sb.AppendLine($"Utilization: {layer.Metadata.Utilization:F3}"); - sb.AppendLine($"Height: {layer.Metadata.Height}"); - double totalWeight = 0; - foreach (var g in layer.Items) - { - totalWeight += g.SkuType.Weight; - } - sb.AppendLine($"Total weight: {totalWeight} kg"); - sb.AppendLine($"Total placed items: {layer.Items.Count}"); - foreach (var g in layer.Items.GroupBy(i => i.SkuType.SkuId)) - { - var sku = g.First().SkuType; - sb.AppendLine($" {sku.Name} x {g.Count()} [{sku.Length}x{sku.Width}x{sku.Height}]"); - } - sb.AppendLine("=================="); - sb.AppendLine("Full details are included in the PDF report."); - - return sb.ToString(); - } - - private Model3DGroup? _selectionHighlight; - - public void UpdateSelectedItem(PositionedItem? item) - { - if (item?.SkuType != null) - { - var sku = item.SkuType; - SelectedItemInfo = $" > {sku.Name} ({sku.Length}x{sku.Width}x{sku.Height}) positioned at {item.X}, {item.Y}"; - HighlightItem(item); - } - else - { - SelectedItemInfo = string.Empty; - HighlightItem(null); - } - } - - private void HighlightItem(PositionedItem? item) - { - if (_selectionHighlight != null) - { - Scene.Children.Remove(_selectionHighlight); - _selectionHighlight = null; - } - if (item == null) return; - var sku = item.SkuType; - double boxLength = item.Rotated ? sku.Width : sku.Length; - double boxWidth = item.Rotated ? sku.Length : sku.Width; - double boxHeight = sku.Height; - double inflate = 0.6; - var origin = new Point3D(item.X - inflate / 2.0, PalletHeight + 0.01, item.Y - inflate / 2.0); - var fillBrush = new SolidColorBrush(Color.FromArgb(40, 255, 255, 0)); - var edgeColor = Colors.Yellow; - _selectionHighlight = GeometryCreator.CreateBoxWithEdges(origin, boxLength + inflate, boxHeight + inflate, boxWidth + inflate, fillBrush, edgeColor, 0.6); - Scene.Children.Add(_selectionHighlight); - } - - private void Update2DPreview() - { - if (SelectedLayer == null) - { - LayerRectangles.Clear(); - return; - } - - var layer = SelectedLayer; - var pallet = new Pallet("Pallet", PalletLength, PalletWidth, (int)Math.Round(PalletHeight)); - // Ensure geometry is built for the current layer/pallet - LayerGeometryBuilder.Build(layer, pallet, 1); - - LayerRectangles.Clear(); - if (layer.Geometry?.ItemRectangles != null) - { - // Flip Y so UI top-left origin matches pallet bottom-left origin - double canvasHeight = PalletWidth; - foreach (var r in layer.Geometry.ItemRectangles) - { - var display = new Rect(r.X, canvasHeight - (r.Y + r.Height), r.Width, r.Height); - LayerRectangles.Add(display); - } - } - } } } diff --git a/Views/Pages/PalletBuilderPage.xaml b/Views/Pages/PalletBuilderPage.xaml index 85fca2e..6599f49 100644 --- a/Views/Pages/PalletBuilderPage.xaml +++ b/Views/Pages/PalletBuilderPage.xaml @@ -32,8 +32,8 @@ - - + + @@ -58,7 +58,7 @@ - + @@ -78,15 +78,15 @@ - + - + - + - + - + - + @@ -195,18 +195,18 @@ - + - - + + + behaviors:ViewportBehaviors.ZoomCommand="{Binding ViewModel.LayerAnalyzer.ZoomCommand}" + behaviors:ViewportBehaviors.BeginPanCommand="{Binding ViewModel.LayerAnalyzer.BeginPanCommand}" + behaviors:ViewportBehaviors.PanCommand="{Binding ViewModel.LayerAnalyzer.PanCommand}"> @@ -215,13 +215,13 @@ - + - + @@ -240,20 +240,15 @@ - + - + - - - - - - - - - + + + + diff --git a/Views/Pages/PalletBuilderPage.xaml.cs b/Views/Pages/PalletBuilderPage.xaml.cs index 185e5bf..50e6e0b 100644 --- a/Views/Pages/PalletBuilderPage.xaml.cs +++ b/Views/Pages/PalletBuilderPage.xaml.cs @@ -22,11 +22,12 @@ public PalletBuilderPage(PalletBuilderViewModel viewModel) MainViewPort.MouseLeftButtonDown += MainViewPort_MouseLeftButtonDown; } - private void OnLoaded(object? sender, RoutedEventArgs e) + private async void OnLoaded(object? sender, RoutedEventArgs e) { - if (ViewModel.ViewportController == null && MainPerspectiveCamera is PerspectiveCamera cam) + await ViewModel.OnNavigatedToAsync(); + if (ViewModel.LayerAnalyzer.ViewportController == null && MainPerspectiveCamera is PerspectiveCamera cam) { - ViewModel.AttachCamera(cam); + ViewModel.LayerAnalyzer.AttachCamera(cam); } } @@ -36,15 +37,14 @@ private void MainViewPort_MouseLeftButtonDown(object sender, System.Windows.Inpu var hitParams = new PointHitTestParameters(pos); PositionedItem? selected = null; - HitTestResultCallback resultCallback = r => + HitTestResultBehavior resultCallback(HitTestResult r) { if (r is RayHitTestResult rayResult) { if (rayResult.ModelHit is GeometryModel3D geo) { - // Access scene builder mapping via reflection (quick solution) or store in Tag - var builderField = typeof(PalletBuilderViewModel).GetField("_sceneBuilder", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - if (builderField?.GetValue(ViewModel) is LayerSceneBuilder builder) + var builderField = typeof(LayerAnalyzerViewModel).GetField("_sceneBuilder", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (builderField?.GetValue(ViewModel.LayerAnalyzer) is LayerSceneBuilder builder) { if (builder.TryGetItemForGeometry(geo, out var item)) { @@ -55,10 +55,10 @@ private void MainViewPort_MouseLeftButtonDown(object sender, System.Windows.Inpu } } return HitTestResultBehavior.Continue; - }; + } VisualTreeHelper.HitTest(MainViewPort, null, resultCallback, hitParams); - ViewModel.UpdateSelectedItem(selected); + ViewModel.LayerAnalyzer.UpdateSelectedItem(selected); } private async void SkuSelectionGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) @@ -75,7 +75,7 @@ private async void SkuSelectionGrid_CellEditEnding(object sender, DataGridCellEd try { - await ViewModel.UpdateSkuAsync(sku); + await ViewModel.Settings.UpdateSkuAsync(sku); } catch { From a857b2dadb0a24c3c1cfda566f893b08cd22373b Mon Sep 17 00:00:00 2001 From: VladM7 Date: Sat, 6 Dec 2025 00:34:03 +0100 Subject: [PATCH 2/4] extracted default settings in new file --- App.xaml.cs | 23 +++++-- Models/GenerationOptions.cs | 10 ++- Models/PalletDefaultsOptions.cs | 13 ++++ Services/LayerVisualizationService.cs | 56 ++++++++++++++++ Stack-Solver.csproj | 4 ++ ViewModels/Pages/LayerAnalyzerViewModel.cs | 64 ++++++------------- .../Pages/PalletBuilderSettingsViewModel.cs | 48 +++++++++++++- ViewModels/Pages/PalletBuilderViewModel.cs | 21 ++++-- ViewModels/Pages/SettingsViewModel.cs | 2 +- Views/Pages/PalletBuilderPage.xaml | 16 +++-- Views/Pages/PalletBuilderPage.xaml.cs | 10 +-- defaults.json | 15 +++++ 12 files changed, 212 insertions(+), 70 deletions(-) create mode 100644 Models/PalletDefaultsOptions.cs create mode 100644 Services/LayerVisualizationService.cs create mode 100644 defaults.json diff --git a/App.xaml.cs b/App.xaml.cs index 2e89a34..da79e32 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -2,9 +2,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Stack_Solver.Data; using Stack_Solver.Data.Repositories; using Stack_Solver.Infrastructure; +using Stack_Solver.Models; using Stack_Solver.Services; using Stack_Solver.ViewModels.Pages; using Stack_Solver.ViewModels.Windows; @@ -29,7 +31,11 @@ public partial class App // https://docs.microsoft.com/dotnet/core/extensions/logging private static readonly IHost _host = Host .CreateDefaultBuilder() - .ConfigureAppConfiguration(c => { c.SetBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)); }) + .ConfigureAppConfiguration(c => + { + c.SetBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)!); + c.AddJsonFile("defaults.json", optional: true, reloadOnChange: true); + }) .ConfigureServices((context, services) => { services.AddNavigationViewPageProvider(); @@ -56,24 +62,31 @@ public partial class App services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(sp => new PalletBuilderViewModel( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>())); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddDbContextFactory(options => { options.UseSqlite($"Data Source={AppPaths.DatabaseFile}"); }); - // Repositories services.AddSingleton(); - - // Initializer services.AddSingleton(); + + // Bind options from host configuration + services.Configure(context.Configuration.GetSection("LayerGeneration")); + services.Configure(context.Configuration.GetSection("PalletDefaults")); }).Build(); /// diff --git a/Models/GenerationOptions.cs b/Models/GenerationOptions.cs index 96357bb..166ed21 100644 --- a/Models/GenerationOptions.cs +++ b/Models/GenerationOptions.cs @@ -2,8 +2,8 @@ { public class GenerationOptions { - public int MaxSolverTime { get; set; } = 60; - public int MaxCandidates { get; set; } = 2000; + public int MaxSolverTime { get; set; } + public int MaxCandidates { get; set; } public GenerationOptions() { } @@ -12,5 +12,11 @@ public GenerationOptions(int maxSolverTime, int maxCandidates) MaxSolverTime = maxSolverTime; MaxCandidates = maxCandidates; } + + public static GenerationOptions From(GenerationOptions? source) + { + if (source == null) return new GenerationOptions(); + return new GenerationOptions(source.MaxSolverTime, source.MaxCandidates); + } } } diff --git a/Models/PalletDefaultsOptions.cs b/Models/PalletDefaultsOptions.cs new file mode 100644 index 0000000..6c68951 --- /dev/null +++ b/Models/PalletDefaultsOptions.cs @@ -0,0 +1,13 @@ +namespace Stack_Solver.Models +{ + public class PalletDefaultsOptions + { + public string? DefaultCatalog { get; set; } + public string? DefaultPalletName { get; set; } + public int PalletLength { get; set; } = 120; + public int PalletWidth { get; set; } = 80; + public double PalletHeight { get; set; } = 14.4; + public int MaxStackHeight { get; set; } = 180; + public int MaxStackWeight { get; set; } = 950; + } +} diff --git a/Services/LayerVisualizationService.cs b/Services/LayerVisualizationService.cs new file mode 100644 index 0000000..506814c --- /dev/null +++ b/Services/LayerVisualizationService.cs @@ -0,0 +1,56 @@ +using Stack_Solver.Helpers.Rendering; +using Stack_Solver.Models.Layering; +using Stack_Solver.Models.Supports; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Media3D; + +namespace Stack_Solver.Services +{ + public interface ILayerVisualizationService + { + void HighlightItem(Model3DGroup scene, PositionedItem? item, double palletHeight); + void Build2DRectangles(Layer layer, SupportSurface pallet, int gridStep, ObservableCollection target); + } + + public class LayerVisualizationService : ILayerVisualizationService + { + private Model3DGroup? _selectionHighlight; + + public void HighlightItem(Model3DGroup scene, PositionedItem? item, double palletHeight) + { + if (_selectionHighlight != null) + { + scene.Children.Remove(_selectionHighlight); + _selectionHighlight = null; + } + if (item == null) return; + var sku = item.SkuType; + double boxLength = item.Rotated ? sku.Width : sku.Length; + double boxWidth = item.Rotated ? sku.Length : sku.Width; + double boxHeight = sku.Height; + double inflate = 0.6; + var origin = new Point3D(item.X - inflate / 2.0, palletHeight + 0.01, item.Y - inflate / 2.0); + var fillBrush = new SolidColorBrush(Color.FromArgb(40, 255, 255, 0)); + var edgeColor = Colors.Yellow; + _selectionHighlight = GeometryCreator.CreateBoxWithEdges(origin, boxLength + inflate, boxHeight + inflate, boxWidth + inflate, fillBrush, edgeColor, 0.6); + scene.Children.Add(_selectionHighlight); + } + + public void Build2DRectangles(Layer layer, SupportSurface pallet, int gridStep, ObservableCollection target) + { + LayerGeometryBuilder.Build(layer, pallet, gridStep); + target.Clear(); + if (layer.Geometry?.ItemRectangles != null) + { + double canvasHeight = pallet.Width; + foreach (var r in layer.Geometry.ItemRectangles) + { + var display = new Rect(r.X, canvasHeight - (r.Y + r.Height), r.Width, r.Height); + target.Add(display); + } + } + } + } +} diff --git a/Stack-Solver.csproj b/Stack-Solver.csproj index a0ef0d5..b0cf1fb 100644 --- a/Stack-Solver.csproj +++ b/Stack-Solver.csproj @@ -25,6 +25,9 @@ + + PreserveNewest + @@ -45,6 +48,7 @@ + diff --git a/ViewModels/Pages/LayerAnalyzerViewModel.cs b/ViewModels/Pages/LayerAnalyzerViewModel.cs index a4cc19c..d4cd624 100644 --- a/ViewModels/Pages/LayerAnalyzerViewModel.cs +++ b/ViewModels/Pages/LayerAnalyzerViewModel.cs @@ -1,3 +1,5 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using Stack_Solver.Helpers.Rendering; using Stack_Solver.Infrastructure; using Stack_Solver.Models; @@ -7,6 +9,7 @@ using Stack_Solver.Services.Layering; using System.Collections.ObjectModel; using System.Text; +using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Media3D; @@ -17,6 +20,7 @@ public partial class LayerAnalyzerViewModel : ObservableObject { private readonly IEventAggregator _events; private readonly LayerSceneBuilder _sceneBuilder = new(); + private readonly ILayerVisualizationService _viz; private CancellationTokenSource? _sceneBuildCts; private CancellationTokenSource? _generationCts; @@ -64,21 +68,26 @@ public bool CanOptimizeGeometry private Layer? _optimizedViewLayer; private static List? _allLayers = []; - public LayerAnalyzerViewModel(IEventAggregator events) + public LayerAnalyzerViewModel(IEventAggregator events, ILayerVisualizationService viz) { _events = events; + _viz = viz; _events.Subscribe(OnSettingsChanged); ZoomCommand = new RelayCommand(Zoom); BeginPanCommand = new RelayCommand(BeginPan); PanCommand = new RelayCommand(Pan); } + public bool TryGetItemFromGeometry(GeometryModel3D geo, out PositionedItem item) => _sceneBuilder.TryGetItemForGeometry(geo, out item); + private int _palletLength; private int _palletWidth; private double _palletHeight; private bool _useCpsat; private int _maxCpsatCandidates; private int _solverTimeLimit; + private int _maxStackHeight; + private int _maxStackWeight; private List _selectedSkus = new(); private void OnSettingsChanged(SettingsChangedMessage msg) @@ -89,6 +98,8 @@ private void OnSettingsChanged(SettingsChangedMessage msg) _useCpsat = msg.UseCpsat; _maxCpsatCandidates = msg.MaxCpsatCandidates; _solverTimeLimit = msg.SolverTimeLimit; + _maxStackHeight = msg.MaxStackHeight; + _maxStackWeight = msg.MaxStackWeight; _selectedSkus = [.. msg.Skus.Where(s => s.Quantity > 0)]; RecenterCameraTarget(); if (SelectedLayer != null) @@ -102,9 +113,9 @@ private void OnSettingsChanged(SettingsChangedMessage msg) public ICommand BeginPanCommand { get; } public ICommand PanCommand { get; } - private void Zoom(double delta) => ViewportController?.Zoom(delta); - private void BeginPan(Point p) => ViewportController?.BeginPan(p); - private void Pan(Point p) => ViewportController?.Pan(p); + private void Zoom(double delta) => _viewportController?.Zoom(delta); + private void BeginPan(Point p) => _viewportController?.BeginPan(p); + private void Pan(Point p) => _viewportController?.Pan(p); public void AttachCamera(PerspectiveCamera camera) { @@ -330,7 +341,8 @@ private static string BuildLayerText(Layer layer) return sb.ToString(); } - private Model3DGroup? _selectionHighlight; + [ObservableProperty] + private string _selectedItemInfo = string.Empty; public void UpdateSelectedItem(PositionedItem? item) { @@ -338,38 +350,15 @@ public void UpdateSelectedItem(PositionedItem? item) { var sku = item.SkuType; SelectedItemInfo = $" > {sku.Name} ({sku.Length}x{sku.Width}x{sku.Height}) positioned at {item.X}, {item.Y}"; - HighlightItem(item); + _viz.HighlightItem(Scene, item, _palletHeight); } else { SelectedItemInfo = string.Empty; - HighlightItem(null); + _viz.HighlightItem(Scene, null, _palletHeight); } } - [ObservableProperty] - private string _selectedItemInfo = string.Empty; - - private void HighlightItem(PositionedItem? item) - { - if (_selectionHighlight != null) - { - Scene.Children.Remove(_selectionHighlight); - _selectionHighlight = null; - } - if (item == null) return; - var sku = item.SkuType; - double boxLength = item.Rotated ? sku.Width : sku.Length; - double boxWidth = item.Rotated ? sku.Length : sku.Width; - double boxHeight = sku.Height; - double inflate = 0.6; - var origin = new Point3D(item.X - inflate / 2.0, _palletHeight + 0.01, item.Y - inflate / 2.0); - var fillBrush = new SolidColorBrush(Color.FromArgb(40, 255, 255, 0)); - var edgeColor = Colors.Yellow; - _selectionHighlight = GeometryCreator.CreateBoxWithEdges(origin, boxLength + inflate, boxHeight + inflate, boxWidth + inflate, fillBrush, edgeColor, 0.6); - Scene.Children.Add(_selectionHighlight); - } - private void Update2DPreview() { if (SelectedLayer == null) @@ -377,21 +366,8 @@ private void Update2DPreview() LayerRectangles.Clear(); return; } - - var layer = SelectedLayer; var pallet = new Pallet("Pallet", _palletLength, _palletWidth, (int)Math.Round(_palletHeight)); - LayerGeometryBuilder.Build(layer, pallet, 1); - - LayerRectangles.Clear(); - if (layer.Geometry?.ItemRectangles != null) - { - double canvasHeight = _palletWidth; - foreach (var r in layer.Geometry.ItemRectangles) - { - var display = new Rect(r.X, canvasHeight - (r.Y + r.Height), r.Width, r.Height); - LayerRectangles.Add(display); - } - } + _viz.Build2DRectangles(SelectedLayer, pallet, 1, LayerRectangles); } } diff --git a/ViewModels/Pages/PalletBuilderSettingsViewModel.cs b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs index 5d5849c..6084677 100644 --- a/ViewModels/Pages/PalletBuilderSettingsViewModel.cs +++ b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Options; using Stack_Solver.Data.Repositories; using Stack_Solver.Infrastructure; using Stack_Solver.Models; @@ -10,6 +11,8 @@ public partial class PalletBuilderSettingsViewModel : ObservableObject { private readonly ISkuRepository _skuRepository; private readonly IEventAggregator _events; + private readonly GenerationOptions _defaults; + private readonly PalletDefaultsOptions _palletDefaults; private bool _isInitialized; [ObservableProperty] @@ -33,6 +36,12 @@ public partial class PalletBuilderSettingsViewModel : ObservableObject [ObservableProperty] private int _solverTimeLimit = 60; + [ObservableProperty] + private int _maxStackHeight; + + [ObservableProperty] + private int _maxStackWeight; + public ObservableCollection CommonPalletsInternational { get; } = []; public ObservableCollection CommonPalletsAmerica { get; } = []; @@ -62,13 +71,25 @@ public Pallet? SelectedAmericanPallet } } - public PalletBuilderSettingsViewModel(ISkuRepository skuRepository, IEventAggregator events) + public PalletBuilderSettingsViewModel(ISkuRepository skuRepository, IEventAggregator events, IOptions genOptions, IOptions palletDefaults) { _skuRepository = skuRepository; _events = events; + _defaults = GenerationOptions.From(genOptions.Value); + _palletDefaults = palletDefaults.Value ?? new PalletDefaultsOptions(); _skuRepository.SkuAdded += OnSkuAdded; _skuRepository.SkuUpdated += OnSkuUpdated; _skuRepository.SkuDeleted += OnSkuDeleted; + + SolverTimeLimit = _defaults.MaxSolverTime; + MaxCpsatCandidates = _defaults.MaxCandidates; + + PalletLength = _palletDefaults.PalletLength; + PalletWidth = _palletDefaults.PalletWidth; + PalletHeight = _palletDefaults.PalletHeight; + + MaxStackHeight = _palletDefaults.MaxStackHeight; + MaxStackWeight = _palletDefaults.MaxStackWeight; } public async Task InitializeAsync() @@ -89,6 +110,23 @@ public async Task InitializeAsync() CommonPalletsAmerica.Add(p); } + // If a specific catalog/name is set, select it + if (!string.IsNullOrWhiteSpace(_palletDefaults.DefaultPalletName)) + { + if (string.Equals(_palletDefaults.DefaultCatalog, "America", StringComparison.OrdinalIgnoreCase)) + { + SelectedAmericanPallet = CommonPalletsAmerica.FirstOrDefault(p => string.Equals(p.Name, _palletDefaults.DefaultPalletName, StringComparison.OrdinalIgnoreCase)); + } + else + { + SelectedInternationalPallet = CommonPalletsInternational.FirstOrDefault(p => string.Equals(p.Name, _palletDefaults.DefaultPalletName, StringComparison.OrdinalIgnoreCase)); + } + } + + // initialize defaults for solver inputs + SolverTimeLimit = _defaults.MaxSolverTime; + MaxCpsatCandidates = _defaults.MaxCandidates; + _isInitialized = true; PublishSettingsChanged(); } @@ -115,12 +153,16 @@ public async Task UpdateSkuAsync(SKU sku, CancellationToken ct = default) partial void OnUseCpsatChanged(bool value) => PublishSettingsChanged(); partial void OnMaxCpsatCandidatesChanged(int value) => PublishSettingsChanged(); partial void OnSolverTimeLimitChanged(int value) => PublishSettingsChanged(); + partial void OnMaxStackHeightChanged(int value) => PublishSettingsChanged(); + partial void OnMaxStackWeightChanged(int value) => PublishSettingsChanged(); private void PublishSettingsChanged() { _events.Publish(new SettingsChangedMessage( PalletLength, PalletWidth, PalletHeight, - UseCpsat, MaxCpsatCandidates, SolverTimeLimit, [.. Skus])); + UseCpsat, MaxCpsatCandidates, SolverTimeLimit, + MaxStackHeight, MaxStackWeight, + [.. Skus])); } private void OnSkuAdded(object? sender, SKU sku) @@ -209,5 +251,7 @@ public record SettingsChangedMessage( bool UseCpsat, int MaxCpsatCandidates, int SolverTimeLimit, + int MaxStackHeight, + int MaxStackWeight, List Skus); } diff --git a/ViewModels/Pages/PalletBuilderViewModel.cs b/ViewModels/Pages/PalletBuilderViewModel.cs index 11b1b71..5bebe6b 100644 --- a/ViewModels/Pages/PalletBuilderViewModel.cs +++ b/ViewModels/Pages/PalletBuilderViewModel.cs @@ -1,13 +1,24 @@ -using Stack_Solver.Data.Repositories; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Options; +using Stack_Solver.Data.Repositories; using Stack_Solver.Infrastructure; +using Stack_Solver.Models; +using Stack_Solver.Services; namespace Stack_Solver.ViewModels.Pages { - public partial class PalletBuilderViewModel(ISkuRepository skuRepository, IEventAggregator events) : ObservableObject + public partial class PalletBuilderViewModel : ObservableObject { - public PalletBuilderSettingsViewModel Settings { get; } = new PalletBuilderSettingsViewModel(skuRepository, events); - public LayerAnalyzerViewModel LayerAnalyzer { get; } = new LayerAnalyzerViewModel(events); - public PalletAnalyzerViewModel PalletAnalyzer { get; } = new PalletAnalyzerViewModel(events); + public PalletBuilderSettingsViewModel Settings { get; } + public LayerAnalyzerViewModel LayerAnalyzer { get; } + public PalletAnalyzerViewModel PalletAnalyzer { get; } + + public PalletBuilderViewModel(ISkuRepository skuRepository, IEventAggregator events, ILayerVisualizationService viz, IOptions genOptions, IOptions palletDefaults) + { + Settings = new PalletBuilderSettingsViewModel(skuRepository, events, genOptions, palletDefaults); + LayerAnalyzer = new LayerAnalyzerViewModel(events, viz); + PalletAnalyzer = new PalletAnalyzerViewModel(events); + } public async Task OnNavigatedToAsync() { diff --git a/ViewModels/Pages/SettingsViewModel.cs b/ViewModels/Pages/SettingsViewModel.cs index 9f6b442..fd76311 100644 --- a/ViewModels/Pages/SettingsViewModel.cs +++ b/ViewModels/Pages/SettingsViewModel.cs @@ -31,7 +31,7 @@ private void InitializeViewModel() _isInitialized = true; } - private string GetAssemblyVersion() + private static string GetAssemblyVersion() { return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? String.Empty; diff --git a/Views/Pages/PalletBuilderPage.xaml b/Views/Pages/PalletBuilderPage.xaml index 6599f49..a79436c 100644 --- a/Views/Pages/PalletBuilderPage.xaml +++ b/Views/Pages/PalletBuilderPage.xaml @@ -155,10 +155,16 @@ - + - + @@ -173,9 +179,11 @@ - 180 + - 950 + diff --git a/Views/Pages/PalletBuilderPage.xaml.cs b/Views/Pages/PalletBuilderPage.xaml.cs index 50e6e0b..3813d66 100644 --- a/Views/Pages/PalletBuilderPage.xaml.cs +++ b/Views/Pages/PalletBuilderPage.xaml.cs @@ -43,14 +43,10 @@ HitTestResultBehavior resultCallback(HitTestResult r) { if (rayResult.ModelHit is GeometryModel3D geo) { - var builderField = typeof(LayerAnalyzerViewModel).GetField("_sceneBuilder", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - if (builderField?.GetValue(ViewModel.LayerAnalyzer) is LayerSceneBuilder builder) + if (ViewModel.LayerAnalyzer.TryGetItemFromGeometry(geo, out var item)) { - if (builder.TryGetItemForGeometry(geo, out var item)) - { - selected = item; - return HitTestResultBehavior.Stop; - } + selected = item; + return HitTestResultBehavior.Stop; } } } diff --git a/defaults.json b/defaults.json new file mode 100644 index 0000000..c33dcf6 --- /dev/null +++ b/defaults.json @@ -0,0 +1,15 @@ +{ + "LayerGeneration": { + "MaxSolverTime": 60, + "MaxCandidates": 2000 + }, + "PalletDefaults": { + "DefaultCatalog": "International", + "DefaultPalletName": "EUR 120x80", + "PalletLength": 120, + "PalletWidth": 80, + "PalletHeight": 14.4, + "MaxStackHeight": 180, + "MaxStackWeight": 950 + } +} From d2871cb21a92f95aba7eddc6646a1608a5be6f67 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Sat, 6 Dec 2025 00:52:27 +0100 Subject: [PATCH 3/4] slightly improved help text readability --- Views/Pages/PalletBuilderPage.xaml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Views/Pages/PalletBuilderPage.xaml b/Views/Pages/PalletBuilderPage.xaml index a79436c..37f990e 100644 --- a/Views/Pages/PalletBuilderPage.xaml +++ b/Views/Pages/PalletBuilderPage.xaml @@ -39,20 +39,24 @@ - Here you can adjust different parameters and see the results. + - 1. Set the pallet dimensions manually or by selecting a pallet type. + - 2. Adjust the quantity for each SKU type. + - (or set it to 0 to ignore that specific SKU) + - You can add more SKU types (or edit them) in the Library page. + + + - 3. Change the layer generation and stacking options. + - 4. Click Generate and see the results in the analyzer pages. + + + @@ -190,7 +194,7 @@ - + @@ -234,7 +238,7 @@ - + From 82e167cedb6f928ba46aea0904b3951e2822f635 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Sat, 6 Dec 2025 13:05:01 +0100 Subject: [PATCH 4/4] small cleanups --- ViewModels/Pages/LayerAnalyzerViewModel.cs | 9 ++------- ViewModels/Pages/PalletBuilderSettingsViewModel.cs | 12 +++++------- ViewModels/Pages/SKULibraryViewModel.cs | 2 +- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/ViewModels/Pages/LayerAnalyzerViewModel.cs b/ViewModels/Pages/LayerAnalyzerViewModel.cs index d4cd624..f2f2e27 100644 --- a/ViewModels/Pages/LayerAnalyzerViewModel.cs +++ b/ViewModels/Pages/LayerAnalyzerViewModel.cs @@ -1,5 +1,3 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; using Stack_Solver.Helpers.Rendering; using Stack_Solver.Infrastructure; using Stack_Solver.Models; @@ -9,9 +7,7 @@ using Stack_Solver.Services.Layering; using System.Collections.ObjectModel; using System.Text; -using System.Windows; using System.Windows.Input; -using System.Windows.Media; using System.Windows.Media.Media3D; namespace Stack_Solver.ViewModels.Pages @@ -88,7 +84,7 @@ public LayerAnalyzerViewModel(IEventAggregator events, ILayerVisualizationServic private int _solverTimeLimit; private int _maxStackHeight; private int _maxStackWeight; - private List _selectedSkus = new(); + private List _selectedSkus = []; private void OnSettingsChanged(SettingsChangedMessage msg) { @@ -204,7 +200,7 @@ private async Task Generate() { if (_selectedSkus.Count == 0) { - OutputText = "No SKUs with quantity > 0."; + OutputText = "No SKUs with quantity greater than 0."; return; } @@ -288,7 +284,6 @@ private async Task Generate() } LayerGenStats = LayerGenStats.TrimEnd(',') + "."; - // notify pallet analyzer _events.Publish(new LayersGeneratedMessage(_allLayers)); } catch (OperationCanceledException) diff --git a/ViewModels/Pages/PalletBuilderSettingsViewModel.cs b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs index 6084677..f3777d2 100644 --- a/ViewModels/Pages/PalletBuilderSettingsViewModel.cs +++ b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs @@ -19,22 +19,22 @@ public partial class PalletBuilderSettingsViewModel : ObservableObject private ObservableCollection _skus = []; [ObservableProperty] - private int _palletLength = 120; + private int _palletLength; [ObservableProperty] - private int _palletWidth = 80; + private int _palletWidth; [ObservableProperty] - private double _palletHeight = 14.4; + private double _palletHeight; [ObservableProperty] private bool _useCpsat; [ObservableProperty] - private int _maxCpsatCandidates = 2000; + private int _maxCpsatCandidates; [ObservableProperty] - private int _solverTimeLimit = 60; + private int _solverTimeLimit; [ObservableProperty] private int _maxStackHeight; @@ -110,7 +110,6 @@ public async Task InitializeAsync() CommonPalletsAmerica.Add(p); } - // If a specific catalog/name is set, select it if (!string.IsNullOrWhiteSpace(_palletDefaults.DefaultPalletName)) { if (string.Equals(_palletDefaults.DefaultCatalog, "America", StringComparison.OrdinalIgnoreCase)) @@ -123,7 +122,6 @@ public async Task InitializeAsync() } } - // initialize defaults for solver inputs SolverTimeLimit = _defaults.MaxSolverTime; MaxCpsatCandidates = _defaults.MaxCandidates; diff --git a/ViewModels/Pages/SKULibraryViewModel.cs b/ViewModels/Pages/SKULibraryViewModel.cs index 846338f..5e2e51b 100644 --- a/ViewModels/Pages/SKULibraryViewModel.cs +++ b/ViewModels/Pages/SKULibraryViewModel.cs @@ -55,7 +55,7 @@ public async Task OnNavigatedToAsync() await InitializeViewModelAsync(); } - public Task OnNavigatedFromAsync() => Task.CompletedTask; + public static Task OnNavigatedFromAsync() => Task.CompletedTask; private async Task InitializeViewModelAsync() {