From f115343c75e7d05d802a5adb69a9cd40dafadc63 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Fri, 8 May 2026 23:02:33 +0200 Subject: [PATCH 1/7] implemented the pallet template enumerator --- Services/Stacking/PalletTemplateEnumerator.cs | 166 +++++++++++++++--- 1 file changed, 141 insertions(+), 25 deletions(-) diff --git a/Services/Stacking/PalletTemplateEnumerator.cs b/Services/Stacking/PalletTemplateEnumerator.cs index 385c661..bd4908d 100644 --- a/Services/Stacking/PalletTemplateEnumerator.cs +++ b/Services/Stacking/PalletTemplateEnumerator.cs @@ -5,12 +5,16 @@ 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 (no A-B-A-B alternation), + /// (3) mixed-layer combinations. Heaviest-on-bottom is enforced by ordering pairs by + /// total layer weight; inter-layer support is enforced via Pallet.MaxSkuOverhang. + /// Templates are deduplicated by their SKU-count signature. /// 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, @@ -21,42 +25,154 @@ public static List Enumerate( 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); + // Class 1: pure homogeneous chains (one SKU, one layer pattern repeated). + 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) + // Class 2: stacked-homogeneous pairs. Heavier layer on bottom, no alternation. + 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); + } + } + + // Class 3: mixed-layer chains and mixed-with-homogeneous combinations. + 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, - IReadOnlyList ordered, - GenerationOptions options, - List templates) + Layer layer, + List templates, + HashSet seen) { - var t = _strategy.Build(pallet, ordered, options); - if (t != null && IsDistinct(t, templates)) - templates.Add(t); + 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 bool IsDistinct(PalletTemplate candidate, IReadOnlyList existing) + 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, + 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 union = new HashSet(a.Metrics.UsedSkuTypes, StringComparer.Ordinal); + foreach (var s in b.Metrics.UsedSkuTypes) union.Add(s); + return union.Count; + } + + 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) => From 4c3789c4a7ea5d1906e7a500abe306a276663b56 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Fri, 8 May 2026 23:08:19 +0200 Subject: [PATCH 2/7] implemented template filtering --- Services/Stacking/TemplateFilter.cs | 103 ++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 Services/Stacking/TemplateFilter.cs diff --git a/Services/Stacking/TemplateFilter.cs b/Services/Stacking/TemplateFilter.cs new file mode 100644 index 0000000..98e884f --- /dev/null +++ b/Services/Stacking/TemplateFilter.cs @@ -0,0 +1,103 @@ +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) + { + // A heavy, dense template can have low volume utilization while being at weight capacity. + // Pass if either volume or weight utilization clears the floor. + 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; + } + } +} From a839fce4a81d1551064f88921e8efc707685f865 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Sat, 9 May 2026 00:07:31 +0200 Subject: [PATCH 3/7] implemented cp-sat assignment --- Services/CPSATAssignmentService.cs | 172 ++++++++++++++++++++ ViewModels/Pages/PalletAnalyzerViewModel.cs | 119 +++++++++----- Views/Pages/PalletBuilderPage.xaml | 4 +- 3 files changed, 252 insertions(+), 43 deletions(-) create mode 100644 Services/CPSATAssignmentService.cs 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/ViewModels/Pages/PalletAnalyzerViewModel.cs b/ViewModels/Pages/PalletAnalyzerViewModel.cs index 676e2fc..84bb879 100644 --- a/ViewModels/Pages/PalletAnalyzerViewModel.cs +++ b/ViewModels/Pages/PalletAnalyzerViewModel.cs @@ -5,6 +5,7 @@ using Stack_Solver.Models.Layering; using Stack_Solver.Models.Supports; using Stack_Solver.Services; +using Stack_Solver.Services.Stacking; using System.Collections.ObjectModel; using System.Text; using System.Windows.Input; @@ -51,6 +52,9 @@ public partial class PalletAnalyzerViewModel : ObservableObject [ObservableProperty] private ObservableCollection _solutions = []; + [ObservableProperty] + private SolutionDisplay? _selectedSolution; + public Model3DGroup Scene { get; } = new(); public ViewportController? ViewportController => _viewportController; public ICommand ZoomCommand { get; } @@ -136,46 +140,57 @@ 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, options); + 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]; + else + OutputText = "No assignments produced."; } catch (OperationCanceledException) { } catch (Exception ex) @@ -188,6 +203,24 @@ 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(); + OutputText = BuildSummaryText(value.Result); + } + partial void OnSelectedAssignmentChanged(TemplateAssignmentDisplay? value) { SelectedLayerTypes = value != null @@ -255,23 +288,11 @@ private static List BuildLeftoverSkus(List originalSkus, IReadOnlyDict })]; } - private static string BuildSummaryText(AssignmentResult result, List skus) + private static string BuildSummaryText(AssignmentResult result) { - var skuMap = skus.ToDictionary(s => s.SkuId, s => s.Name, StringComparer.Ordinal); 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."); - //} return sb.ToString(); } } @@ -320,20 +341,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/Views/Pages/PalletBuilderPage.xaml b/Views/Pages/PalletBuilderPage.xaml index d7aee5c..c9f84e5 100644 --- a/Views/Pages/PalletBuilderPage.xaml +++ b/Views/Pages/PalletBuilderPage.xaml @@ -328,7 +328,8 @@ CanUserDeleteRows="False" IsReadOnly="True" HeadersVisibility="Column" - ItemsSource="{Binding ViewModel.PalletAnalyzer.Solutions}"> + ItemsSource="{Binding ViewModel.PalletAnalyzer.Solutions}" + SelectedItem="{Binding ViewModel.PalletAnalyzer.SelectedSolution, Mode=TwoWay}"> @@ -336,7 +337,6 @@ - From 69a0b6e0ba9f31d0a66dda442af53656f15b85e1 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Sat, 9 May 2026 12:37:43 +0200 Subject: [PATCH 4/7] small bugfixes --- Services/Stacking/PalletTemplateEnumerator.cs | 12 ++------ Services/Stacking/TemplateFilter.cs | 2 -- ViewModels/Pages/PalletAnalyzerViewModel.cs | 2 +- Views/Pages/PalletBuilderPage.xaml | 29 ++++++++++--------- Views/Pages/SKULibraryPage.xaml.cs | 2 +- 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/Services/Stacking/PalletTemplateEnumerator.cs b/Services/Stacking/PalletTemplateEnumerator.cs index bd4908d..b3953cc 100644 --- a/Services/Stacking/PalletTemplateEnumerator.cs +++ b/Services/Stacking/PalletTemplateEnumerator.cs @@ -6,10 +6,8 @@ namespace Stack_Solver.Services.Stacking { /// /// Generates a pool of pallet templates from a filtered layer set in three classes: - /// (1) pure homogeneous chains, (2) stacked-homogeneous pairs (no A-B-A-B alternation), - /// (3) mixed-layer combinations. Heaviest-on-bottom is enforced by ordering pairs by - /// total layer weight; inter-layer support is enforced via Pallet.MaxSkuOverhang. - /// Templates are deduplicated by their SKU-count signature. + /// (1) pure homogeneous chains, (2) stacked-homogeneous pairs, + /// (3) mixed-layer combinations /// public static class PalletTemplateEnumerator { @@ -18,8 +16,7 @@ public static class PalletTemplateEnumerator public static List Enumerate( Pallet pallet, - IReadOnlyList layers, - GenerationOptions options) + IReadOnlyList layers) { if (layers.Count == 0) return []; @@ -31,11 +28,9 @@ public static List Enumerate( var homogeneous = layers.Where(IsHomogeneous).ToList(); var mixed = layers.Where(l => !IsHomogeneous(l)).ToList(); - // Class 1: pure homogeneous chains (one SKU, one layer pattern repeated). foreach (var layer in homogeneous) TryAddRepeated(pallet, layer, templates, seen); - // Class 2: stacked-homogeneous pairs. Heavier layer on bottom, no alternation. foreach (var bottom in homogeneous) { foreach (var top in homogeneous) @@ -49,7 +44,6 @@ public static List Enumerate( } } - // Class 3: mixed-layer chains and mixed-with-homogeneous combinations. foreach (var m in mixed) { TryAddRepeated(pallet, m, templates, seen); diff --git a/Services/Stacking/TemplateFilter.cs b/Services/Stacking/TemplateFilter.cs index 98e884f..bff2f61 100644 --- a/Services/Stacking/TemplateFilter.cs +++ b/Services/Stacking/TemplateFilter.cs @@ -35,8 +35,6 @@ public static List Filter(IReadOnlyList template private static bool PassesUtilizationFloor(PalletTemplate t, double palletVolume, double maxWeight) { - // A heavy, dense template can have low volume utilization while being at weight capacity. - // Pass if either volume or weight utilization clears the floor. double volumeUtil = palletVolume > 0 ? UsedVolume(t) / palletVolume : 0; double weightUtil = maxWeight > 0 ? t.TotalWeight / maxWeight : 0; return Math.Max(volumeUtil, weightUtil) >= MinUtilization; diff --git a/ViewModels/Pages/PalletAnalyzerViewModel.cs b/ViewModels/Pages/PalletAnalyzerViewModel.cs index 84bb879..4e7eb0f 100644 --- a/ViewModels/Pages/PalletAnalyzerViewModel.cs +++ b/ViewModels/Pages/PalletAnalyzerViewModel.cs @@ -170,7 +170,7 @@ private async Task BuildPalletsAsync(CancellationToken ct) { ct.ThrowIfCancellationRequested(); var filtered = LayerMetricsCalculator.FilterLayers(layersSnapshot, options); - var pool = PalletTemplateEnumerator.Enumerate(pallet, filtered, options); + var pool = PalletTemplateEnumerator.Enumerate(pallet, filtered); var pruned = TemplateFilter.Filter(pool, pallet); return CPSATAssignmentService.Assign(pruned, demand, pallet, options, greedyResult, ct); }, ct); diff --git a/Views/Pages/PalletBuilderPage.xaml b/Views/Pages/PalletBuilderPage.xaml index c9f84e5..571a0c6 100644 --- a/Views/Pages/PalletBuilderPage.xaml +++ b/Views/Pages/PalletBuilderPage.xaml @@ -169,6 +169,15 @@ + + + + + + + @@ -185,12 +194,6 @@ Minimum="1" IsEnabled="{Binding ViewModel.Settings.UseCpsat}" SmallChange="100" LargeChange="1000"/> - - - @@ -261,9 +264,8 @@ - - + @@ -273,12 +275,12 @@ - + - - - + + - - + 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; } From 6d597871588c61bd1c70da2a0b09cb7a1598df41 Mon Sep 17 00:00:00 2001 From: VladM7 Date: Thu, 14 May 2026 02:26:02 +0200 Subject: [PATCH 6/7] various UI improvements --- App.xaml | 1 + Helpers/Rendering/PalletSceneBuilder.cs | 30 +- Styles/SnackbarStyle.xaml | 295 ++++++++++++++++++++ ViewModels/Pages/LayerAnalyzerViewModel.cs | 3 +- ViewModels/Pages/PalletAnalyzerViewModel.cs | 92 +++++- ViewModels/Pages/PalletBuilderViewModel.cs | 2 +- Views/Pages/PalletBuilderPage.xaml | 96 ++++--- Views/Pages/PalletBuilderPage.xaml.cs | 25 +- Views/Windows/MainWindow.xaml | 4 +- 9 files changed, 489 insertions(+), 59 deletions(-) create mode 100644 Styles/SnackbarStyle.xaml 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/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/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 6d6fe26..9613d74 100644 --- a/ViewModels/Pages/LayerAnalyzerViewModel.cs +++ b/ViewModels/Pages/LayerAnalyzerViewModel.cs @@ -251,10 +251,9 @@ 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)); - _snackbarService.Show("Generation complete", $"{_allLayers.Count} candidate layers generated.", ControlAppearance.Success, null, TimeSpan.FromSeconds(5)); } catch (OperationCanceledException) { diff --git a/ViewModels/Pages/PalletAnalyzerViewModel.cs b/ViewModels/Pages/PalletAnalyzerViewModel.cs index 5d08b74..ab8840c 100644 --- a/ViewModels/Pages/PalletAnalyzerViewModel.cs +++ b/ViewModels/Pages/PalletAnalyzerViewModel.cs @@ -6,9 +6,12 @@ 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 @@ -32,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 = []; @@ -55,15 +58,26 @@ public partial class PalletAnalyzerViewModel : ObservableObject [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)); @@ -94,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(); @@ -119,7 +132,7 @@ 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; @@ -187,14 +200,20 @@ private async Task BuildPalletsAsync(CancellationToken ct) HasResults = Solutions.Count > 0; if (HasResults) + { 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 - OutputText = "No assignments produced."; + PalletGenStats = "No assignments produced."; } catch (OperationCanceledException) { } catch (Exception ex) { - OutputText = $"Error: {ex.Message}"; + PalletGenStats = $"Error: {ex.Message}"; } finally { @@ -217,15 +236,54 @@ partial void OnSelectedSolutionChanged(SolutionDisplay? value) Assignments.Add(new TemplateAssignmentDisplay(template, count, value.Skus, idx++, value.PalletLength, value.PalletWidth, value.PalletHeight)); SelectedAssignment = Assignments.FirstOrDefault(); - OutputText = BuildSummaryText(value.Result); + } + + 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; @@ -287,11 +345,24 @@ private static List BuildLeftoverSkus(List originalSkus, IReadOnlyDict })]; } - private static string BuildSummaryText(AssignmentResult result) + private string BuildAssignmentText(TemplateAssignmentDisplay assignment) { + 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}"); + 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(); } } @@ -332,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 diff --git a/ViewModels/Pages/PalletBuilderViewModel.cs b/ViewModels/Pages/PalletBuilderViewModel.cs index bfa67ea..99ad1b2 100644 --- a/ViewModels/Pages/PalletBuilderViewModel.cs +++ b/ViewModels/Pages/PalletBuilderViewModel.cs @@ -22,7 +22,7 @@ public partial class PalletBuilderViewModel( { public PalletBuilderSettingsViewModel Settings { get; } = new PalletBuilderSettingsViewModel(skuRepository, events, genOptions, palletDefaults, settingsValidator, skuQuantityValidator, userSettings); public LayerAnalyzerViewModel LayerAnalyzer { get; } = new LayerAnalyzerViewModel(events, viz, snackbarService); - public PalletAnalyzerViewModel PalletAnalyzer { get; } = new PalletAnalyzerViewModel(events); + 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 0050258..17d0489 100644 --- a/Views/Pages/PalletBuilderPage.xaml +++ b/Views/Pages/PalletBuilderPage.xaml @@ -236,21 +236,23 @@ - - - - - - - - - - - + + + + + + + + + + + + + @@ -263,6 +265,8 @@ + + @@ -272,7 +276,12 @@ - + + + + + + - + - - @@ -312,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}"> @@ -322,8 +330,16 @@ - - + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + 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/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" From 3b1a985a3d2d1c2424c0a3465f4b86329676e28a Mon Sep 17 00:00:00 2001 From: VladM7 Date: Thu, 14 May 2026 13:25:42 +0200 Subject: [PATCH 7/7] fixed layer name --- Services/Layering/HomogeneousGenerationStrategy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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;