diff --git a/App.xaml b/App.xaml
index 6c0f5dc..3d9f69b 100644
--- a/App.xaml
+++ b/App.xaml
@@ -11,6 +11,7 @@
+
diff --git a/App.xaml.cs b/App.xaml.cs
index 96bc8b6..3d682ea 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -60,6 +60,8 @@ public partial class App
// Service containing navigation, same as INavigationWindow... but without window
services.AddSingleton();
+ services.AddSingleton();
+
// Main window with navigation
services.AddSingleton();
services.AddSingleton();
@@ -81,7 +83,8 @@ public partial class App
sp.GetRequiredService>(),
sp.GetRequiredService>(),
sp.GetRequiredService>(),
- sp.GetRequiredService()));
+ sp.GetRequiredService(),
+ sp.GetRequiredService()));
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/Converters/BoolToInfoBarSeverityConverter.cs b/Converters/BoolToInfoBarSeverityConverter.cs
new file mode 100644
index 0000000..04ae261
--- /dev/null
+++ b/Converters/BoolToInfoBarSeverityConverter.cs
@@ -0,0 +1,15 @@
+using System.Globalization;
+using System.Windows.Data;
+using Wpf.Ui.Controls;
+
+namespace Stack_Solver.Converters
+{
+ public class BoolToInfoBarSeverityConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ => value is bool b && b ? InfoBarSeverity.Success : InfoBarSeverity.Error;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+ }
+}
diff --git a/Helpers/Rendering/PalletSceneBuilder.cs b/Helpers/Rendering/PalletSceneBuilder.cs
index 3fded59..083d74f 100644
--- a/Helpers/Rendering/PalletSceneBuilder.cs
+++ b/Helpers/Rendering/PalletSceneBuilder.cs
@@ -9,6 +9,14 @@ public class PalletSceneBuilder
private readonly Dictionary _skuBrushCache = [];
private readonly Lock _cacheLock = new();
+ private Dictionary _geometryToLayerId = [];
+ private IReadOnlyList<(string LayerId, double Y, double Height)> _layerPositions = [];
+
+ public bool TryGetLayerIdForGeometry(GeometryModel3D geo, out string layerId)
+ => _geometryToLayerId.TryGetValue(geo, out layerId!);
+
+ public IReadOnlyList<(string LayerId, double Y, double Height)> LayerPositions => _layerPositions;
+
public async Task BuildAsync(
Model3DGroup target,
PalletTemplate template,
@@ -21,6 +29,8 @@ public async Task BuildAsync(
{
ct.ThrowIfCancellationRequested();
var g = new Model3DGroup();
+ var mapping = new Dictionary();
+ var positions = new List<(string LayerId, double Y, double Height)>();
g.Children.Add(new AmbientLight(Colors.DimGray));
g.Children.Add(new DirectionalLight(Colors.White, new Vector3D(-1, -2, -1)));
@@ -35,6 +45,7 @@ public async Task BuildAsync(
foreach (var layer in template.Layers)
{
ct.ThrowIfCancellationRequested();
+ positions.Add((layer.Id, currentY, layer.Metadata.Height));
foreach (var item in layer.Items)
{
@@ -43,21 +54,32 @@ public async Task BuildAsync(
double boxWidth = item.Rotated ? sku.Length : sku.Width;
var origin = new Point3D(item.X, currentY, item.Y);
var brush = GetBrushForSku(sku.SkuId);
- g.Children.Add(GeometryCreator.CreateBoxWithEdges(
- origin, boxLength, sku.Height, boxWidth, brush, Colors.Black, 0.25));
+ var boxGroup = GeometryCreator.CreateBoxWithEdges(
+ origin, boxLength, sku.Height, boxWidth, brush, Colors.Black, 0.25);
+ g.Children.Add(boxGroup);
+ if (boxGroup is Model3DGroup boxModelGroup)
+ {
+ foreach (var child in boxModelGroup.Children)
+ {
+ if (child is GeometryModel3D geo)
+ mapping[geo] = layer.Id;
+ }
+ }
}
currentY += layer.Metadata.Height;
}
TryFreezeRecursive(g);
- return g;
+ return (Group: g, Mapping: mapping, Positions: positions);
}, ct).ConfigureAwait(true);
ct.ThrowIfCancellationRequested();
target.Children.Clear();
- foreach (var child in tempGroup.Children)
+ foreach (var child in tempGroup.Group.Children)
target.Children.Add(child);
+ _geometryToLayerId = tempGroup.Mapping;
+ _layerPositions = tempGroup.Positions;
}
private Brush GetBrushForSku(string skuId)
diff --git a/Services/CPSATAssignmentService.cs b/Services/CPSATAssignmentService.cs
new file mode 100644
index 0000000..ed3e405
--- /dev/null
+++ b/Services/CPSATAssignmentService.cs
@@ -0,0 +1,172 @@
+using Google.OrTools.Sat;
+using Stack_Solver.Models;
+using Stack_Solver.Models.Assignment;
+using Stack_Solver.Models.Supports;
+
+namespace Stack_Solver.Services
+{
+ ///
+ /// Assigns pre-built pallet templates to demand using CP-SAT with strict
+ /// three-tier lexicographic optimization: minimize leftovers (Phase 1),
+ /// then total pallets (Phase 2), then distinct templates (Phase 3). Each
+ /// phase pins the previous objective as an equality constraint before the
+ /// next solve. A greedy AssignmentResult may be supplied as a warm-start;
+ /// hints are best-effort and silently ignored by the solver if infeasible.
+ ///
+ public static class CPSATAssignmentService
+ {
+ public static AssignmentResult Assign(
+ IReadOnlyList templates,
+ IReadOnlyDictionary demand,
+ Pallet pallet,
+ GenerationOptions options,
+ AssignmentResult? warmStart = null,
+ CancellationToken ct = default)
+ {
+ ArgumentNullException.ThrowIfNull(templates);
+ ArgumentNullException.ThrowIfNull(demand);
+ ArgumentNullException.ThrowIfNull(pallet);
+ ArgumentNullException.ThrowIfNull(options);
+
+ if (templates.Count == 0 || demand.Count == 0)
+ return new AssignmentResult();
+
+ // Drop templates that pack SKUs not present in demand. Should not
+ // arise in normal flow, but keeps the model self-consistent.
+ var validTemplates = templates
+ .Where(t => t.SkuCounts.Keys.All(demand.ContainsKey))
+ .ToList();
+ if (validTemplates.Count == 0)
+ return new AssignmentResult();
+
+ var skuOrder = demand.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
+ int dMax = Math.Max(1, demand.Values.Sum());
+
+ var model = new CpModel();
+ var x = new IntVar[validTemplates.Count];
+ var y = new BoolVar[validTemplates.Count];
+
+ for (int t = 0; t < validTemplates.Count; t++)
+ {
+ x[t] = model.NewIntVar(0, dMax, $"x_{t}");
+ y[t] = model.NewBoolVar($"y_{t}");
+ // y_t = 1 ⟺ x_t ≥ 1
+ model.Add(x[t] >= 1).OnlyEnforceIf(y[t]);
+ model.Add(x[t] == 0).OnlyEnforceIf(y[t].Not());
+ }
+
+ var l = new Dictionary(StringComparer.Ordinal);
+ foreach (var sku in skuOrder)
+ l[sku] = model.NewIntVar(0, demand[sku], $"l_{sku}");
+
+ // Demand satisfaction with leftover slack: Σ_t a_{t,i} · x_t + l_i = d_i
+ foreach (var sku in skuOrder)
+ {
+ var terms = new List { l[sku] };
+ for (int t = 0; t < validTemplates.Count; t++)
+ {
+ int count = validTemplates[t].SkuCounts.GetValueOrDefault(sku);
+ if (count > 0)
+ terms.Add(LinearExpr.Term(x[t], count));
+ }
+ model.Add(LinearExpr.Sum(terms) == demand[sku]);
+ }
+
+ if (warmStart != null)
+ ApplyWarmStart(model, warmStart, validTemplates, x, y, l);
+
+ int totalBudget = options.MaxSolverTime > 0 ? options.MaxSolverTime : 30;
+ int phaseBudget = Math.Max(5, totalBudget / 3);
+
+ var solver = new CpSolver
+ {
+ StringParameters = $"max_time_in_seconds:{phaseBudget},num_search_workers:8"
+ };
+
+ // Phase 1: minimize leftovers.
+ ct.ThrowIfCancellationRequested();
+ var leftoversSum = LinearExpr.Sum(l.Values);
+ model.Minimize(leftoversSum);
+ if (!Solved(solver.Solve(model))) return new AssignmentResult();
+ long leftoversStar = (long)Math.Round(solver.ObjectiveValue);
+ model.Add(leftoversSum == leftoversStar);
+
+ // Phase 2: minimize total pallets.
+ ct.ThrowIfCancellationRequested();
+ var palletsSum = LinearExpr.Sum(x);
+ model.Minimize(palletsSum);
+ if (!Solved(solver.Solve(model))) return new AssignmentResult();
+ long palletsStar = (long)Math.Round(solver.ObjectiveValue);
+ model.Add(palletsSum == palletsStar);
+
+ // Phase 3: minimize distinct templates.
+ ct.ThrowIfCancellationRequested();
+ model.Minimize(LinearExpr.Sum(y));
+ if (!Solved(solver.Solve(model))) return new AssignmentResult();
+
+ return ExtractResult(solver, validTemplates, x, l);
+ }
+
+ private static bool Solved(CpSolverStatus status) =>
+ status == CpSolverStatus.Optimal || status == CpSolverStatus.Feasible;
+
+ private static AssignmentResult ExtractResult(
+ CpSolver solver,
+ IReadOnlyList templates,
+ IntVar[] x,
+ IReadOnlyDictionary l)
+ {
+ var assignments = new List<(PalletTemplate Template, int Count)>();
+ for (int t = 0; t < templates.Count; t++)
+ {
+ int count = (int)solver.Value(x[t]);
+ if (count > 0)
+ assignments.Add((templates[t], count));
+ }
+
+ var leftovers = new Dictionary(StringComparer.Ordinal);
+ foreach (var (sku, lVar) in l)
+ {
+ int count = (int)solver.Value(lVar);
+ if (count > 0)
+ leftovers[sku] = count;
+ }
+
+ return new AssignmentResult { Assignments = assignments, Leftovers = leftovers };
+ }
+
+ private static void ApplyWarmStart(
+ CpModel model,
+ AssignmentResult warmStart,
+ IReadOnlyList templates,
+ IntVar[] x,
+ BoolVar[] y,
+ IReadOnlyDictionary l)
+ {
+ // Greedy templates won't share IDs with the enumerator pool, so match
+ // by SKU-count signature (the same signature the enumerator dedups on).
+ var greedyCounts = new Dictionary(StringComparer.Ordinal);
+ foreach (var (template, count) in warmStart.Assignments)
+ {
+ var sig = Signature(template);
+ greedyCounts[sig] = greedyCounts.GetValueOrDefault(sig) + count;
+ }
+
+ for (int t = 0; t < templates.Count; t++)
+ {
+ int hint = greedyCounts.GetValueOrDefault(Signature(templates[t]));
+ model.AddHint(x[t], hint);
+ model.AddHint(y[t], hint > 0 ? 1 : 0);
+ }
+
+ foreach (var (sku, count) in warmStart.Leftovers)
+ if (l.TryGetValue(sku, out var lVar))
+ model.AddHint(lVar, count);
+ }
+
+ private static string Signature(PalletTemplate t) =>
+ string.Join("|", t.SkuCounts
+ .OrderBy(kvp => kvp.Key, StringComparer.Ordinal)
+ .Select(kvp => $"{kvp.Key}:{kvp.Value}"));
+ }
+}
diff --git a/Services/Layering/HomogeneousGenerationStrategy.cs b/Services/Layering/HomogeneousGenerationStrategy.cs
index d703668..bae309e 100644
--- a/Services/Layering/HomogeneousGenerationStrategy.cs
+++ b/Services/Layering/HomogeneousGenerationStrategy.cs
@@ -76,7 +76,7 @@ public List Generate(List skus, SupportSurface supportSurface, Gener
string description = $"homogeneous {variant.Sku.Name} ({orientation}) {nx}x{ny}";
var metadata = new LayerMetadata(utilization, variant.Sku.Height, description);
- var layer = new Layer($"hom_grid_{variant.Sku.Name}", placements, metadata);
+ var layer = new Layer($"hom_grid_{variant.Sku.Name}_{orientation}", placements, metadata);
layer.Geometry = LayerGeometryBuilder.Build(layer, supportSurface);
layer.Metrics = LayerMetricsCalculator.Compute(layer, supportSurface);
return layer;
diff --git a/Services/Stacking/PalletTemplateEnumerator.cs b/Services/Stacking/PalletTemplateEnumerator.cs
index 385c661..b3953cc 100644
--- a/Services/Stacking/PalletTemplateEnumerator.cs
+++ b/Services/Stacking/PalletTemplateEnumerator.cs
@@ -5,58 +5,168 @@
namespace Stack_Solver.Services.Stacking
{
///
- /// Generates a diverse pool of pallet templates from a filtered layer set by running
- /// the greedy stacking strategy with different layer orderings.
+ /// Generates a pool of pallet templates from a filtered layer set in three classes:
+ /// (1) pure homogeneous chains, (2) stacked-homogeneous pairs,
+ /// (3) mixed-layer combinations
///
public static class PalletTemplateEnumerator
{
- private static readonly GreedyStackingStrategy _strategy = new();
+ private const int MaxDistinctSkusPerTemplate = 3;
+ private const int MaxLayersPerTemplate = 6;
public static List Enumerate(
Pallet pallet,
- IReadOnlyList layers,
- GenerationOptions options)
+ IReadOnlyList layers)
{
if (layers.Count == 0)
return [];
var templates = new List();
+ var seen = new HashSet(StringComparer.Ordinal);
+ var supportCache = new Dictionary<(string, string), bool>();
- // Ordering 1: heaviest layers first (plan §5: heavy on bottom)
- TryAdd(pallet, layers.OrderByDescending(l => l.Metrics.TotalWeight).ToList(), options, templates);
+ var homogeneous = layers.Where(IsHomogeneous).ToList();
+ var mixed = layers.Where(l => !IsHomogeneous(l)).ToList();
- // Ordering 2: highest utilization first
- TryAdd(pallet, layers.OrderByDescending(l => l.Metadata.Utilization).ToList(), options, templates);
+ foreach (var layer in homogeneous)
+ TryAddRepeated(pallet, layer, templates, seen);
- // Ordering 3: each layer as mandatory base, rest sorted by weight
- foreach (var baseLayer in layers)
+ foreach (var bottom in homogeneous)
{
- var ordered = layers
- .Where(l => !ReferenceEquals(l, baseLayer))
- .OrderByDescending(l => l.Metrics.TotalWeight)
- .Prepend(baseLayer)
- .ToList();
- TryAdd(pallet, ordered, options, templates);
+ foreach (var top in homogeneous)
+ {
+ if (ReferenceEquals(bottom, top)) continue;
+ if (SkuOf(bottom) == SkuOf(top)) continue;
+ if (bottom.Metrics.TotalWeight < top.Metrics.TotalWeight) continue;
+ if (!IsTransitionValid(bottom, top, pallet, supportCache)) continue;
+
+ EnumerateStackedPair(pallet, bottom, top, templates, seen);
+ }
+ }
+
+ foreach (var m in mixed)
+ {
+ TryAddRepeated(pallet, m, templates, seen);
+
+ foreach (var hom in homogeneous)
+ {
+ if (DistinctSkuUnion(m, hom) > MaxDistinctSkusPerTemplate) continue;
+
+ if (m.Metrics.TotalWeight >= hom.Metrics.TotalWeight &&
+ IsTransitionValid(m, hom, pallet, supportCache))
+ {
+ EnumerateStackedPair(pallet, m, hom, templates, seen);
+ }
+
+ if (hom.Metrics.TotalWeight >= m.Metrics.TotalWeight &&
+ IsTransitionValid(hom, m, pallet, supportCache))
+ {
+ EnumerateStackedPair(pallet, hom, m, templates, seen);
+ }
+ }
}
return templates;
}
- private static void TryAdd(
+ private static void TryAddRepeated(
+ Pallet pallet,
+ Layer layer,
+ List templates,
+ HashSet seen)
+ {
+ int n = Math.Min(MaxRepeats(pallet, layer), MaxLayersPerTemplate);
+ if (n < 1) return;
+
+ var stack = new List(n);
+ for (int i = 0; i < n; i++) stack.Add(layer);
+ AddIfDistinct(stack, templates, seen);
+ }
+
+ private static void EnumerateStackedPair(
+ Pallet pallet,
+ Layer bottom,
+ Layer top,
+ List templates,
+ HashSet seen)
+ {
+ int availH = pallet.MaxStackHeight - pallet.Height;
+ int availW = pallet.MaxStackWeight;
+ int maxBottom = Math.Min(MaxRepeats(pallet, bottom), MaxLayersPerTemplate - 1);
+ int maxTop = Math.Min(MaxRepeats(pallet, top), MaxLayersPerTemplate - 1);
+
+ for (int nB = 1; nB <= maxBottom; nB++)
+ {
+ int hUsedB = nB * bottom.Metadata.Height;
+ double wUsedB = nB * bottom.Metrics.TotalWeight;
+ if (hUsedB > availH || wUsedB > availW) break;
+
+ int remainingLayers = MaxLayersPerTemplate - nB;
+ int topCap = Math.Min(maxTop, remainingLayers);
+
+ for (int nT = 1; nT <= topCap; nT++)
+ {
+ int hUsed = hUsedB + nT * top.Metadata.Height;
+ double wUsed = wUsedB + nT * top.Metrics.TotalWeight;
+ if (hUsed > availH || wUsed > availW) break;
+
+ var stack = new List(nB + nT);
+ for (int i = 0; i < nB; i++) stack.Add(bottom);
+ for (int i = 0; i < nT; i++) stack.Add(top);
+ AddIfDistinct(stack, templates, seen);
+ }
+ }
+ }
+
+ private static int MaxRepeats(Pallet pallet, Layer layer)
+ {
+ if (layer.Metadata.Height <= 0) return 0;
+ int availH = pallet.MaxStackHeight - pallet.Height;
+ if (availH <= 0) return 0;
+
+ int byH = availH / layer.Metadata.Height;
+ int byW = layer.Metrics.TotalWeight > 0
+ ? (int)Math.Floor(pallet.MaxStackWeight / layer.Metrics.TotalWeight)
+ : int.MaxValue;
+ return Math.Min(byH, byW);
+ }
+
+ private static bool IsTransitionValid(
+ Layer lower,
+ Layer upper,
Pallet pallet,
- IReadOnlyList ordered,
- GenerationOptions options,
- List templates)
+ Dictionary<(string, string), bool> cache)
+ {
+ var key = (lower.Id, upper.Id);
+ if (cache.TryGetValue(key, out bool cached)) return cached;
+
+ var support = LayerSupportAnalyzer.Analyze(lower, upper, pallet);
+ bool ok = support.MaximumSkuOverhangArea <= pallet.MaxSkuOverhang;
+ cache[key] = ok;
+ return ok;
+ }
+
+ private static int DistinctSkuUnion(Layer a, Layer b)
{
- var t = _strategy.Build(pallet, ordered, options);
- if (t != null && IsDistinct(t, templates))
- templates.Add(t);
+ var union = new HashSet(a.Metrics.UsedSkuTypes, StringComparer.Ordinal);
+ foreach (var s in b.Metrics.UsedSkuTypes) union.Add(s);
+ return union.Count;
}
- private static bool IsDistinct(PalletTemplate candidate, IReadOnlyList existing)
+ private static bool IsHomogeneous(Layer l) =>
+ l.Metrics.UsedSkuTypes.Count == 1;
+
+ private static string SkuOf(Layer l) =>
+ l.Metrics.UsedSkuTypes.First();
+
+ private static void AddIfDistinct(
+ IReadOnlyList layers,
+ List templates,
+ HashSet seen)
{
- var sig = Signature(candidate);
- return existing.All(t => Signature(t) != sig);
+ var template = PalletTemplate.FromLayers(layers);
+ if (seen.Add(Signature(template)))
+ templates.Add(template);
}
private static string Signature(PalletTemplate t) =>
diff --git a/Services/Stacking/TemplateFilter.cs b/Services/Stacking/TemplateFilter.cs
new file mode 100644
index 0000000..bff2f61
--- /dev/null
+++ b/Services/Stacking/TemplateFilter.cs
@@ -0,0 +1,101 @@
+using Stack_Solver.Models.Supports;
+
+namespace Stack_Solver.Services.Stacking
+{
+ ///
+ /// Prunes the enumerated template pool before it reaches the assignment stage.
+ /// Applies a complexity cap (≤3 distinct SKUs) and a minimum-utilization floor,
+ /// then drops Pareto-dominated templates: T dominates U iff T packs ≥ U's count
+ /// of every SKU at ≤ U's height and weight, with at least one strict inequality.
+ ///
+ public static class TemplateFilter
+ {
+ private const int MaxDistinctSkusPerTemplate = 3;
+ private const double MinUtilization = 0.60;
+
+ public static List Filter(IReadOnlyList templates, Pallet pallet)
+ {
+ ArgumentNullException.ThrowIfNull(templates);
+ ArgumentNullException.ThrowIfNull(pallet);
+ if (templates.Count == 0) return [];
+
+ double palletVolume = (double)pallet.Length * pallet.Width * Math.Max(0, pallet.MaxStackHeight - pallet.Height);
+ double maxWeight = pallet.MaxStackWeight;
+
+ var afterCaps = new List(templates.Count);
+ foreach (var t in templates)
+ {
+ if (t.SkuCounts.Count == 0 || t.SkuCounts.Count > MaxDistinctSkusPerTemplate) continue;
+ if (!PassesUtilizationFloor(t, palletVolume, maxWeight)) continue;
+ afterCaps.Add(t);
+ }
+
+ return RemoveDominated(afterCaps);
+ }
+
+ private static bool PassesUtilizationFloor(PalletTemplate t, double palletVolume, double maxWeight)
+ {
+ double volumeUtil = palletVolume > 0 ? UsedVolume(t) / palletVolume : 0;
+ double weightUtil = maxWeight > 0 ? t.TotalWeight / maxWeight : 0;
+ return Math.Max(volumeUtil, weightUtil) >= MinUtilization;
+ }
+
+ private static double UsedVolume(PalletTemplate t)
+ {
+ double v = 0;
+ foreach (var layer in t.Layers)
+ foreach (var item in layer.Items)
+ v += (double)item.SkuType.Length * item.SkuType.Width * item.SkuType.Height;
+ return v;
+ }
+
+ private static List RemoveDominated(List templates)
+ {
+ var kept = new List(templates.Count);
+ for (int i = 0; i < templates.Count; i++)
+ {
+ bool dominated = false;
+ for (int j = 0; j < templates.Count; j++)
+ {
+ if (i == j) continue;
+ if (Dominates(templates[j], templates[i]))
+ {
+ dominated = true;
+ break;
+ }
+ }
+ if (!dominated) kept.Add(templates[i]);
+ }
+ return kept;
+ }
+
+ private static bool Dominates(PalletTemplate t, PalletTemplate u)
+ {
+ if (t.TotalHeight > u.TotalHeight) return false;
+ if (t.TotalWeight > u.TotalWeight) return false;
+
+ bool strict = t.TotalHeight < u.TotalHeight || t.TotalWeight < u.TotalWeight;
+
+ foreach (var (sku, uCount) in u.SkuCounts)
+ {
+ int tCount = t.SkuCounts.GetValueOrDefault(sku);
+ if (tCount < uCount) return false;
+ if (tCount > uCount) strict = true;
+ }
+
+ if (!strict)
+ {
+ foreach (var sku in t.SkuCounts.Keys)
+ {
+ if (!u.SkuCounts.ContainsKey(sku))
+ {
+ strict = true;
+ break;
+ }
+ }
+ }
+
+ return strict;
+ }
+ }
+}
diff --git a/Styles/SnackbarStyle.xaml b/Styles/SnackbarStyle.xaml
new file mode 100644
index 0000000..aa5289a
--- /dev/null
+++ b/Styles/SnackbarStyle.xaml
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ViewModels/Pages/LayerAnalyzerViewModel.cs b/ViewModels/Pages/LayerAnalyzerViewModel.cs
index cc7d090..9613d74 100644
--- a/ViewModels/Pages/LayerAnalyzerViewModel.cs
+++ b/ViewModels/Pages/LayerAnalyzerViewModel.cs
@@ -8,6 +8,8 @@
using System.Text;
using System.Windows.Input;
using System.Windows.Media.Media3D;
+using Wpf.Ui;
+using Wpf.Ui.Controls;
namespace Stack_Solver.ViewModels.Pages
{
@@ -16,6 +18,7 @@ public partial class LayerAnalyzerViewModel : ObservableObject
private readonly IEventAggregator _events;
private readonly LayerSceneBuilder _sceneBuilder = new();
private readonly ILayerVisualizationService _viz;
+ private readonly ISnackbarService _snackbarService;
private CancellationTokenSource? _sceneBuildCts;
private CancellationTokenSource? _generationCts;
@@ -63,10 +66,11 @@ public bool CanOptimizeGeometry
private Layer? _optimizedViewLayer;
private static List? _allLayers = [];
- public LayerAnalyzerViewModel(IEventAggregator events, ILayerVisualizationService viz)
+ public LayerAnalyzerViewModel(IEventAggregator events, ILayerVisualizationService viz, ISnackbarService snackbarService)
{
_events = events;
_viz = viz;
+ _snackbarService = snackbarService;
_events.Subscribe(OnSettingsChanged);
ZoomCommand = new RelayCommand(Zoom);
BeginPanCommand = new RelayCommand(BeginPan);
@@ -204,6 +208,7 @@ private async Task Generate()
if (_selectedSkus.Count == 0)
{
OutputText = "No SKUs selected with quantity greater than 0.";
+ _snackbarService.Show("Generation failed", "No SKUs selected with quantity > 0.", ControlAppearance.Danger, null, TimeSpan.FromSeconds(5));
return;
}
@@ -224,6 +229,7 @@ private async Task Generate()
if (_allLayers == null || _allLayers.Count == 0)
{
OutputText = "No layers generated.";
+ _snackbarService.Show("Generation failed", "No layers were generated.", ControlAppearance.Danger, null, TimeSpan.FromSeconds(5));
return;
}
@@ -245,7 +251,7 @@ private async Task Generate()
OutputText = "No layers after filtering.";
}
- LayerGenStats = $"Generated {_allLayers.Count} candidate layers using BLF, Homogeneous, StripFill, Radial{(_useCpsat ? ", CPSAT" : "")}.";
+ LayerGenStats = $"Generated {_allLayers.Count} candidate layers using BLF, Homogeneous, StripFill, Radial{(_useCpsat ? ", CPSAT" : "")}. Showing top 10 by utilization.";
_events.Publish(new LayersGeneratedMessage(_allLayers));
}
@@ -256,6 +262,7 @@ private async Task Generate()
catch (Exception ex)
{
OutputText = $"Error: {ex.Message}";
+ _snackbarService.Show("Generation failed", ex.Message, ControlAppearance.Danger, null, TimeSpan.FromSeconds(5));
}
finally
{
diff --git a/ViewModels/Pages/PalletAnalyzerViewModel.cs b/ViewModels/Pages/PalletAnalyzerViewModel.cs
index 676e2fc..ab8840c 100644
--- a/ViewModels/Pages/PalletAnalyzerViewModel.cs
+++ b/ViewModels/Pages/PalletAnalyzerViewModel.cs
@@ -5,9 +5,13 @@
using Stack_Solver.Models.Layering;
using Stack_Solver.Models.Supports;
using Stack_Solver.Services;
+using Stack_Solver.Services.Stacking;
+using Wpf.Ui;
+using Wpf.Ui.Controls;
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
@@ -31,7 +35,7 @@ public partial class PalletAnalyzerViewModel : ObservableObject
private bool _isBuilding;
[ObservableProperty]
- private string _outputText = "Click 'Generate' to start.";
+ private string _palletGenStats = "Click on 'Generate' to start solution generation.";
[ObservableProperty]
private ObservableCollection _assignments = [];
@@ -51,15 +55,29 @@ public partial class PalletAnalyzerViewModel : ObservableObject
[ObservableProperty]
private ObservableCollection _solutions = [];
+ [ObservableProperty]
+ private SolutionDisplay? _selectedSolution;
+
+ [ObservableProperty]
+ private string _assignmentDetailsText = string.Empty;
+
+ [ObservableProperty]
+ private LayerTypeDisplay? _selectedLayerType;
+
+ private readonly List _layerHighlights = [];
+
public Model3DGroup Scene { get; } = new();
public ViewportController? ViewportController => _viewportController;
public ICommand ZoomCommand { get; }
public ICommand BeginPanCommand { get; }
public ICommand PanCommand { get; }
- public PalletAnalyzerViewModel(IEventAggregator events)
+ private readonly ISnackbarService _snackbarService;
+
+ public PalletAnalyzerViewModel(IEventAggregator events, ISnackbarService snackbarService)
{
_events = events;
+ _snackbarService = snackbarService;
_events.Subscribe(OnLayersGenerated);
_events.Subscribe(OnSettingsChanged);
ZoomCommand = new RelayCommand(delta => _viewportController?.Zoom(delta));
@@ -90,7 +108,6 @@ private void OnLayersGenerated(LayersGeneratedMessage msg)
HasResults = false;
Assignments.Clear();
Solutions.Clear();
- OutputText = $"{_availableLayers.Count} candidate layers ready. Building pallets...";
_buildCts?.Cancel();
_buildCts?.Dispose();
@@ -108,15 +125,14 @@ private void OnSettingsChanged(SettingsChangedMessage msg)
_maxSkuOverhang = msg.MaxSkuOverhang;
_selectedSkus = [.. msg.Skus.Where(s => s.IsSelected && s.Quantity > 0)];
_generationOptions = new GenerationOptions(msg.SolverTimeLimit, msg.MaxCpsatCandidates, msg.BlfAttempts);
- if (_viewportController != null)
- _viewportController.Target = CurrentPalletCenter;
+ _viewportController?.Target = CurrentPalletCenter;
}
private async Task BuildPalletsAsync(CancellationToken ct)
{
if (_availableLayers.Count == 0 || _selectedSkus.Count == 0)
{
- OutputText = _availableLayers.Count == 0
+ PalletGenStats = _availableLayers.Count == 0
? "No layers available."
: "No SKUs selected with quantity > 0.";
return;
@@ -136,51 +152,68 @@ private async Task BuildPalletsAsync(CancellationToken ct)
var skus = _selectedSkus.ToList();
var layersSnapshot = _availableLayers.ToList();
- var result = await Task.Run(() =>
+ var greedyResult = await Task.Run(() =>
{
ct.ThrowIfCancellationRequested();
var filtered = LayerMetricsCalculator.FilterLayers(layersSnapshot, options);
- return GreedyAssignmentService.Assign(filtered, demand, pallet);
+ var greedy = GreedyAssignmentService.Assign(filtered, demand, pallet);
+ if (!greedy.HasLeftovers) return greedy;
+
+ var leftoverSkus = BuildLeftoverSkus(skus, greedy.Leftovers);
+ if (leftoverSkus.Count == 0) return greedy;
+ var tailLayers = LayerGenerator.Generate(leftoverSkus, pallet, options, ct: ct);
+ if (tailLayers.Count == 0) return greedy;
+ var tailOptions = new GenerationOptions(options.MaxSolverTime, options.MaxCPSATCandidates, options.BLFAttempts)
+ {
+ MaxLayerStability = options.MaxLayerStability,
+ PerSkuTopLayerFraction = 1.0
+ };
+ var tailFiltered = LayerMetricsCalculator.FilterLayers(tailLayers, tailOptions);
+ if (tailFiltered.Count == 0) return greedy;
+ return MergeResults(greedy, GreedyAssignmentService.Assign(tailFiltered, greedy.Leftovers, pallet));
}, ct);
ct.ThrowIfCancellationRequested();
- if (result.HasLeftovers)
+ AssignmentResult? cpsatResult = null;
+ try
{
- var tailInput = result;
- result = await Task.Run(() =>
+ cpsatResult = await Task.Run(() =>
{
- var leftoverSkus = BuildLeftoverSkus(skus, tailInput.Leftovers);
- if (leftoverSkus.Count == 0) return tailInput;
- var tailLayers = LayerGenerator.Generate(leftoverSkus, pallet, options, ct: ct);
- if (tailLayers.Count == 0) return tailInput;
- var tailOptions = new GenerationOptions(options.MaxSolverTime, options.MaxCPSATCandidates, options.BLFAttempts)
- {
- MaxLayerStability = options.MaxLayerStability,
- PerSkuTopLayerFraction = 1.0
- };
- var filtered = LayerMetricsCalculator.FilterLayers(tailLayers, tailOptions);
- if (filtered.Count == 0) return tailInput;
- return MergeResults(tailInput, GreedyAssignmentService.Assign(filtered, tailInput.Leftovers, pallet));
+ ct.ThrowIfCancellationRequested();
+ var filtered = LayerMetricsCalculator.FilterLayers(layersSnapshot, options);
+ var pool = PalletTemplateEnumerator.Enumerate(pallet, filtered);
+ var pruned = TemplateFilter.Filter(pool, pallet);
+ return CPSATAssignmentService.Assign(pruned, demand, pallet, options, greedyResult, ct);
}, ct);
}
+ catch (OperationCanceledException) { throw; }
+ catch { /* CP-SAT is best-effort; greedy result still shown */ }
ct.ThrowIfCancellationRequested();
- int index = 1;
- foreach (var (template, count) in result.Assignments)
- Assignments.Add(new TemplateAssignmentDisplay(template, count, skus, index++, _palletLength, _palletWidth, _palletHeight));
+ Solutions.Clear();
+ if (cpsatResult != null && cpsatResult.Assignments.Count > 0)
+ Solutions.Add(new SolutionDisplay(Solutions.Count + 1, "CP-SAT", cpsatResult, skus, _palletLength, _palletWidth, _palletHeight));
+ if (greedyResult.Assignments.Count > 0)
+ Solutions.Add(new SolutionDisplay(Solutions.Count + 1, "Greedy", greedyResult, skus, _palletLength, _palletWidth, _palletHeight));
- HasResults = Assignments.Count > 0;
- SelectedAssignment = Assignments.FirstOrDefault();
+ HasResults = Solutions.Count > 0;
if (HasResults)
- Solutions.Add(new SolutionDisplay(1, result));
- OutputText = BuildSummaryText(result, skus);
+ {
+ SelectedSolution = Solutions[0];
+ PalletGenStats = $"Generated {Solutions.Count} solutions.";
+ _snackbarService.Show("Generation complete",
+ $"{_availableLayers.Count} candidate layers and {Solutions.Count} final {(Solutions.Count == 1 ? "solution" : "solutions")} generated.",
+ ControlAppearance.Success, null, TimeSpan.FromSeconds(5));
+ }
+ else
+ PalletGenStats = "No assignments produced.";
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
- OutputText = $"Error: {ex.Message}";
+ PalletGenStats = $"Error: {ex.Message}";
}
finally
{
@@ -188,12 +221,69 @@ private async Task BuildPalletsAsync(CancellationToken ct)
}
}
+ partial void OnSelectedSolutionChanged(SolutionDisplay? value)
+ {
+ Assignments.Clear();
+
+ if (value == null)
+ {
+ SelectedAssignment = null;
+ return;
+ }
+
+ int idx = 1;
+ foreach (var (template, count) in value.Result.Assignments)
+ Assignments.Add(new TemplateAssignmentDisplay(template, count, value.Skus, idx++, value.PalletLength, value.PalletWidth, value.PalletHeight));
+
+ SelectedAssignment = Assignments.FirstOrDefault();
+ }
+
+ partial void OnSelectedLayerTypeChanged(LayerTypeDisplay? value)
+ {
+ foreach (var h in _layerHighlights)
+ Scene.Children.Remove(h);
+ _layerHighlights.Clear();
+
+ if (value == null) return;
+
+ double inflate = 0.1;
+ var fillBrush = new SolidColorBrush(Color.FromArgb(40, 255, 255, 0));
+
+ foreach (var (layerId, y, height) in _sceneBuilder.LayerPositions)
+ {
+ if (layerId != value.LayerId) continue;
+ var origin = new Point3D(-inflate / 2.0, y - inflate / 2.0, -inflate / 2.0);
+ var highlight = GeometryCreator.CreateBoxWithEdges(
+ origin,
+ _palletLength + inflate,
+ height + inflate,
+ _palletWidth + inflate,
+ fillBrush,
+ Colors.Yellow,
+ 0.6);
+ Scene.Children.Add(highlight);
+ _layerHighlights.Add(highlight);
+ }
+ }
+
+ public bool TryGetLayerTypeForGeometry(GeometryModel3D geo, out LayerTypeDisplay? layerType)
+ {
+ layerType = null;
+ if (!_sceneBuilder.TryGetLayerIdForGeometry(geo, out var layerId)) return false;
+ layerType = SelectedLayerTypes.FirstOrDefault(lt => lt.LayerId == layerId);
+ return layerType != null;
+ }
+
partial void OnSelectedAssignmentChanged(TemplateAssignmentDisplay? value)
{
+ SelectedLayerType = null;
+
SelectedLayerTypes = value != null
? new ObservableCollection(value.LayerTypes)
: [];
+ AssignmentDetailsText = value != null ? BuildAssignmentText(value) : string.Empty;
+
if (value != null && _viewportController != null)
{
double totalHeight = _palletHeight + value.Template.TotalHeight;
@@ -255,23 +345,24 @@ private static List BuildLeftoverSkus(List originalSkus, IReadOnlyDict
})];
}
- private static string BuildSummaryText(AssignmentResult result, List skus)
+ private string BuildAssignmentText(TemplateAssignmentDisplay assignment)
{
- var skuMap = skus.ToDictionary(s => s.SkuId, s => s.Name, StringComparer.Ordinal);
+ var t = assignment.Template;
+ int cargoHeight = (int)Math.Round(t.TotalHeight);
+ int totalHeight = _palletHeight + cargoHeight;
+
var sb = new StringBuilder();
- sb.AppendLine($"Total pallets: {result.TotalPallets}");
- sb.AppendLine($"Distinct templates: {result.Assignments.Count}");
- //if (result.HasLeftovers)
- //{
- // sb.AppendLine();
- // sb.AppendLine("Leftover boxes:");
- // foreach (var (skuId, count) in result.Leftovers)
- // sb.AppendLine($" {skuMap.GetValueOrDefault(skuId, skuId)}: {count}");
- //}
- //else
- //{
- // sb.AppendLine("All boxes packed.");
- //}
+ sb.AppendLine($"{assignment.Name} (x{assignment.Count} {(assignment.Count == 1 ? "pallet" : "pallets")})");
+ sb.AppendLine();
+ sb.AppendLine($"Load dimensions: {_palletLength}x{_palletWidth}x{totalHeight} cm");
+ sb.AppendLine($"Cargo height: {cargoHeight} cm");
+ sb.AppendLine($"Weight: {t.TotalWeight:N0} kg");
+ sb.AppendLine($"Boxes: {t.TotalBoxCount} per pallet");
+ sb.AppendLine($"Utilization: {t.AverageLayerUtilization:F3}");
+ sb.AppendLine($"Contents: {assignment.Contents}");
+ sb.AppendLine();
+ sb.AppendLine("==================");
+ sb.AppendLine("Full details are included in the PDF report.");
return sb.ToString();
}
}
@@ -312,6 +403,7 @@ public TemplateAssignmentDisplay(PalletTemplate template, int count, IEnumerable
public class LayerTypeDisplay(Layer layer, int count, IReadOnlyDictionary skuNames)
{
+ public string LayerId { get; } = layer.Id;
public string Name { get; } = layer.Name;
public int Count { get; } = count;
public string Contents { get; } = string.Join(", ", layer.Items
@@ -320,20 +412,36 @@ public class LayerTypeDisplay(Layer layer, int count, IReadOnlyDictionary Skus { get; }
+ public int PalletLength { get; }
+ public int PalletWidth { get; }
+ public int PalletHeight { get; }
public int TotalPallets { get; }
public int PalletTypes { get; }
public int TotalItemsPacked { get; }
public string Efficiency { get; }
- public bool IsActive { get; set; } = true;
- public SolutionDisplay(int number, AssignmentResult result)
+ public SolutionDisplay(
+ int number,
+ string name,
+ AssignmentResult result,
+ IReadOnlyList skus,
+ int palletLength,
+ int palletWidth,
+ int palletHeight)
{
Number = number;
- Name = "Greedy";
+ Name = name;
+ Result = result;
+ Skus = skus;
+ PalletLength = palletLength;
+ PalletWidth = palletWidth;
+ PalletHeight = palletHeight;
TotalPallets = result.TotalPallets;
PalletTypes = result.Assignments.Count;
TotalItemsPacked = result.Assignments.Sum(a => a.Template.TotalBoxCount * a.Count);
diff --git a/ViewModels/Pages/PalletBuilderViewModel.cs b/ViewModels/Pages/PalletBuilderViewModel.cs
index a2a9010..99ad1b2 100644
--- a/ViewModels/Pages/PalletBuilderViewModel.cs
+++ b/ViewModels/Pages/PalletBuilderViewModel.cs
@@ -5,6 +5,7 @@
using Stack_Solver.Models;
using Stack_Solver.Models.Inputs;
using Stack_Solver.Services;
+using Wpf.Ui;
namespace Stack_Solver.ViewModels.Pages
{
@@ -16,11 +17,12 @@ public partial class PalletBuilderViewModel(
IOptions palletDefaults,
IValidator settingsValidator,
IValidator skuQuantityValidator,
- IUserSettingsService userSettings) : ObservableObject
+ IUserSettingsService userSettings,
+ ISnackbarService snackbarService) : ObservableObject
{
public PalletBuilderSettingsViewModel Settings { get; } = new PalletBuilderSettingsViewModel(skuRepository, events, genOptions, palletDefaults, settingsValidator, skuQuantityValidator, userSettings);
- public LayerAnalyzerViewModel LayerAnalyzer { get; } = new LayerAnalyzerViewModel(events, viz);
- public PalletAnalyzerViewModel PalletAnalyzer { get; } = new PalletAnalyzerViewModel(events);
+ public LayerAnalyzerViewModel LayerAnalyzer { get; } = new LayerAnalyzerViewModel(events, viz, snackbarService);
+ public PalletAnalyzerViewModel PalletAnalyzer { get; } = new PalletAnalyzerViewModel(events, snackbarService);
public async Task OnNavigatedToAsync()
{
diff --git a/Views/Pages/PalletBuilderPage.xaml b/Views/Pages/PalletBuilderPage.xaml
index d7aee5c..17d0489 100644
--- a/Views/Pages/PalletBuilderPage.xaml
+++ b/Views/Pages/PalletBuilderPage.xaml
@@ -64,7 +64,7 @@
-
+
@@ -169,6 +169,15 @@
+
+
+
+
+
+
+
@@ -185,12 +194,6 @@
Minimum="1"
IsEnabled="{Binding ViewModel.Settings.UseCpsat}" SmallChange="100" LargeChange="1000"/>
-
-
-
@@ -233,21 +236,23 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -260,26 +265,32 @@
+
-
+
-
+
+
+
+
+
+
-
+
+
-
-
@@ -310,7 +319,8 @@
HeadersVisibility="Column"
ScrollViewer.VerticalScrollBarVisibility="Auto"
BorderThickness="0"
- ItemsSource="{Binding ViewModel.PalletAnalyzer.SelectedLayerTypes}">
+ ItemsSource="{Binding ViewModel.PalletAnalyzer.SelectedLayerTypes}"
+ SelectedItem="{Binding ViewModel.PalletAnalyzer.SelectedLayerType, Mode=TwoWay}">
@@ -320,15 +330,23 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+ ItemsSource="{Binding ViewModel.PalletAnalyzer.Solutions}"
+ SelectedItem="{Binding ViewModel.PalletAnalyzer.SelectedSolution, Mode=TwoWay}">
@@ -336,28 +354,29 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/Pages/PalletBuilderPage.xaml.cs b/Views/Pages/PalletBuilderPage.xaml.cs
index 2c5ef97..5677b29 100644
--- a/Views/Pages/PalletBuilderPage.xaml.cs
+++ b/Views/Pages/PalletBuilderPage.xaml.cs
@@ -20,7 +20,8 @@ public PalletBuilderPage(PalletBuilderViewModel viewModel)
DataContext = this;
InitializeComponent();
Loaded += OnLoaded;
- MainViewPort.MouseLeftButtonDown += MainViewPort_MouseLeftButtonDown;
+ MainViewPortHost.MouseLeftButtonDown += MainViewPort_MouseLeftButtonDown;
+ PalletViewPortHost.MouseLeftButtonDown += PalletViewPort_MouseLeftButtonDown;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
@@ -52,6 +53,28 @@ HitTestResultBehavior resultCallback(HitTestResult r)
ViewModel.LayerAnalyzer.UpdateSelectedItem(selected);
}
+ private void PalletViewPort_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var pos = e.GetPosition(PalletViewPort);
+ var hitParams = new PointHitTestParameters(pos);
+ LayerTypeDisplay? found = null;
+
+ HitTestResultBehavior resultCallback(HitTestResult r)
+ {
+ if (r is RayHitTestResult rayResult
+ && rayResult.ModelHit is GeometryModel3D geo
+ && ViewModel.PalletAnalyzer.TryGetLayerTypeForGeometry(geo, out var layerType))
+ {
+ found = layerType;
+ return HitTestResultBehavior.Stop;
+ }
+ return HitTestResultBehavior.Continue;
+ }
+
+ VisualTreeHelper.HitTest(PalletViewPort, null, resultCallback, hitParams);
+ ViewModel.PalletAnalyzer.SelectedLayerType = found;
+ }
+
private async void SkuSelectionGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (e.EditAction != DataGridEditAction.Commit)
diff --git a/Views/Pages/SKULibraryPage.xaml.cs b/Views/Pages/SKULibraryPage.xaml.cs
index b098593..de8de22 100644
--- a/Views/Pages/SKULibraryPage.xaml.cs
+++ b/Views/Pages/SKULibraryPage.xaml.cs
@@ -23,7 +23,7 @@ public SKULibraryPage(SKULibraryViewModel viewModel)
private void skuDataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
// Cancel generation of hidden columns
- if (e.PropertyName == nameof(SKU.SkuId) || e.PropertyName == nameof(SKU.Quantity))
+ if (e.PropertyName == nameof(SKU.SkuId) || e.PropertyName == nameof(SKU.Quantity) || e.PropertyName == nameof(SKU.IsSelected))
{
e.Cancel = true;
return;
diff --git a/Views/Windows/MainWindow.xaml b/Views/Windows/MainWindow.xaml
index bad9399..f3feb94 100644
--- a/Views/Windows/MainWindow.xaml
+++ b/Views/Windows/MainWindow.xaml
@@ -7,8 +7,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="{Binding ViewModel.ApplicationTitle, Mode=OneWay}"
- Width="1100"
- Height="650"
+ Width="1400"
+ Height="800"
d:DataContext="{d:DesignInstance local:MainWindow,
IsDesignTimeCreatable=True}"
d:DesignHeight="450"
diff --git a/Views/Windows/MainWindow.xaml.cs b/Views/Windows/MainWindow.xaml.cs
index 1d0f6fc..2e8311a 100644
--- a/Views/Windows/MainWindow.xaml.cs
+++ b/Views/Windows/MainWindow.xaml.cs
@@ -14,7 +14,8 @@ public partial class MainWindow : INavigationWindow
public MainWindow(
MainWindowViewModel viewModel,
INavigationViewPageProvider navigationViewPageProvider,
- INavigationService navigationService
+ INavigationService navigationService,
+ ISnackbarService snackbarService
)
{
ViewModel = viewModel;
@@ -26,6 +27,7 @@ INavigationService navigationService
SetPageService(navigationViewPageProvider);
navigationService.SetNavigationControl(RootNavigation);
+ snackbarService.SetSnackbarPresenter(SnackbarPresenter);
RootNavigation.Navigated += OnRootNavigationNavigated;
}