diff --git a/Helpers/Behaviors/ParentScrollBehavior.cs b/Helpers/Behaviors/ParentScrollBehavior.cs new file mode 100644 index 0000000..6df1668 --- /dev/null +++ b/Helpers/Behaviors/ParentScrollBehavior.cs @@ -0,0 +1,66 @@ +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace Stack_Solver.Helpers.Behaviors; + +/// +/// Attached behavior that forwards mouse wheel events to the nearest scrollable parent . +/// Use this when nested controls (DataGrid, NumberBox, etc.) consume wheel events and prevent parent scrolling. +/// +public static class ParentScrollBehavior +{ + public static readonly DependencyProperty EnabledProperty = + DependencyProperty.RegisterAttached( + "Enabled", + typeof(bool), + typeof(ParentScrollBehavior), + new PropertyMetadata(false, OnEnabledChanged)); + + public static bool GetEnabled(DependencyObject obj) => (bool)obj.GetValue(EnabledProperty); + public static void SetEnabled(DependencyObject obj, bool value) => obj.SetValue(EnabledProperty, value); + + private static void OnEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not UIElement element) + return; + + if ((bool)e.NewValue) + { + element.AddHandler(UIElement.PreviewMouseWheelEvent, new MouseWheelEventHandler(OnPreviewMouseWheel), handledEventsToo: true); + } + else + { + element.RemoveHandler(UIElement.PreviewMouseWheelEvent, new MouseWheelEventHandler(OnPreviewMouseWheel)); + } + } + + private static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (FindScrollableParent(sender as DependencyObject) is { } scrollViewer) + { + scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta); + e.Handled = true; + } + } + + private static ScrollViewer? FindScrollableParent(DependencyObject? element) + { + var parent = element is null ? null : VisualTreeHelper.GetParent(element); + + while (parent is not null) + { + if (parent is ScrollViewer sv && CanScroll(sv)) + return sv; + + parent = VisualTreeHelper.GetParent(parent); + } + + return null; + } + + private static bool CanScroll(ScrollViewer sv) => + sv.ScrollableHeight > 0 || + (sv.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled && + sv.ComputedVerticalScrollBarVisibility == Visibility.Visible); +} diff --git a/Helpers/Layering/LayerCandidateHelper.cs b/Helpers/Layering/LayerCandidateHelper.cs new file mode 100644 index 0000000..0602191 --- /dev/null +++ b/Helpers/Layering/LayerCandidateHelper.cs @@ -0,0 +1,59 @@ +using Stack_Solver.Models.Layering; + +namespace Stack_Solver.Helpers.Layering +{ + public static class LayerCandidateHelper + { + private const double UtilizationTolerance = 1e-6; + + public static List SelectBestBySkuCounts(IEnumerable layers) + { + if (layers == null) + return []; + + var bestByKey = new Dictionary(StringComparer.Ordinal); + + foreach (var layer in layers) + { + if (layer == null) + continue; + + var key = BuildSkuCountKey(layer); + if (!bestByKey.TryGetValue(key, out var incumbent) || IsBetter(layer, incumbent)) + bestByKey[key] = layer; + } + + return [.. bestByKey.Values + .OrderByDescending(l => l.Metadata.Utilization) + .ThenByDescending(l => l.Items.Count)]; + } + + public static bool IsBetter(Layer candidate, Layer incumbent, double tolerance = UtilizationTolerance) + { + if (incumbent == null) + return true; + if (candidate == null) + return false; + + double delta = candidate.Metadata.Utilization - incumbent.Metadata.Utilization; + if (delta > tolerance) + return true; + + if (Math.Abs(delta) <= tolerance) + return candidate.Items.Count > incumbent.Items.Count; + + return false; + } + + public static string BuildSkuCountKey(Layer layer) + { + if (layer == null) + return string.Empty; + + return string.Join(",", layer.Items + .GroupBy(i => i.SkuType.SkuId) + .OrderBy(g => g.Key, StringComparer.Ordinal) + .Select(g => $"{g.Key}:{g.Count()}")); + } + } +} diff --git a/Helpers/Layering/SkuVariantFactory.cs b/Helpers/Layering/SkuVariantFactory.cs new file mode 100644 index 0000000..c39be77 --- /dev/null +++ b/Helpers/Layering/SkuVariantFactory.cs @@ -0,0 +1,31 @@ +using Stack_Solver.Models; + +namespace Stack_Solver.Helpers.Layering +{ + public readonly record struct SkuVariant(string VariantId, SKU Sku, int SpanX, int SpanY, bool Rotated); + + public static class SkuVariantFactory + { + public static List CreateAllOrientations(IEnumerable? skus) + { + var variants = new List(); + if (skus == null) + return variants; + + foreach (var sku in skus) + { + if (sku == null) + continue; + + variants.Add(new SkuVariant($"{sku.SkuId}_n", sku, sku.Length, sku.Width, false)); + + if (sku.Rotatable && sku.Length != sku.Width) + { + variants.Add(new SkuVariant($"{sku.SkuId}_r", sku, sku.Width, sku.Length, true)); + } + } + + return variants; + } + } +} diff --git a/Models/GenerationOptions.cs b/Models/GenerationOptions.cs index 166ed21..c85554c 100644 --- a/Models/GenerationOptions.cs +++ b/Models/GenerationOptions.cs @@ -3,20 +3,23 @@ public class GenerationOptions { public int MaxSolverTime { get; set; } - public int MaxCandidates { get; set; } + public int MaxCPSATCandidates { get; set; } + + public int BLFAttempts { get; set; } public GenerationOptions() { } - public GenerationOptions(int maxSolverTime, int maxCandidates) + public GenerationOptions(int maxSolverTime, int maxCandidates, int blfAttempts) { MaxSolverTime = maxSolverTime; - MaxCandidates = maxCandidates; + MaxCPSATCandidates = maxCandidates; + BLFAttempts = blfAttempts; } public static GenerationOptions From(GenerationOptions? source) { if (source == null) return new GenerationOptions(); - return new GenerationOptions(source.MaxSolverTime, source.MaxCandidates); + return new GenerationOptions(source.MaxSolverTime, source.MaxCPSATCandidates, source.BLFAttempts); } } } diff --git a/Services/Layering/BLFGenerationStrategy.cs b/Services/Layering/BLFGenerationStrategy.cs index bffe5c5..384ea84 100644 --- a/Services/Layering/BLFGenerationStrategy.cs +++ b/Services/Layering/BLFGenerationStrategy.cs @@ -1,4 +1,5 @@ -using Stack_Solver.Models; +using Stack_Solver.Helpers.Layering; +using Stack_Solver.Models; using Stack_Solver.Models.Layering; using Stack_Solver.Models.Metadata; using Stack_Solver.Models.Supports; @@ -11,90 +12,143 @@ public class BLFGenerationStrategy : ILayerGenerationStrategy public List Generate(List skus, SupportSurface supportSurface, GenerationOptions options) { - var px = supportSurface.Length; - var py = supportSurface.Width; - int attempts = 200; // to be replaced with a value from generation options - int seed = Environment.TickCount; + if (skus == null || skus.Count == 0 || supportSurface == null) + return []; - var rand = new Random(seed); + int px = supportSurface.Length; + int py = supportSurface.Width; + if (px <= 0 || py <= 0) + return []; + int attempts = options?.BLFAttempts > 0 ? options.BLFAttempts : 200; double area = px * py; + var variants = SkuVariantFactory.CreateAllOrientations(skus); + if (variants.Count == 0) + return []; - var variants = new List<(string skuId, int w, int h, bool Rotated, SKU refSku)>(); - foreach (var s in skus) + var rand = new Random(Environment.TickCount); + var candidateLayers = new List(); + + for (int attempt = 0; attempt < attempts; attempt++) { - variants.Add((s.SkuId, s.Length, s.Width, false, s)); - if (s.Rotatable && s.Length != s.Width) - variants.Add((s.SkuId, s.Width, s.Length, true, s)); + var layer = BuildLayerAttempt(variants, skus, px, py, area, attempt, rand, supportSurface); + if (layer != null) + candidateLayers.Add(layer); } - var foundLayers = new Dictionary(); + return LayerCandidateHelper.SelectBestBySkuCounts(candidateLayers); + } + + private static Layer? BuildLayerAttempt(IReadOnlyList variants, List skus, int maxWidth, int maxDepth, double area, int attemptIndex, Random rand, SupportSurface supportSurface) + { + if (variants.Count == 0) + return null; - for (int attempt = 0; attempt < attempts; attempt++) + var randomized = variants.OrderBy(_ => rand.Next()).ToList(); + var counts = skus.ToDictionary(s => s.SkuId, _ => 0); + var placements = new List(); + int y = 0; + double usedArea = 0; + + while (y < maxDepth) + { + if (!TrySelectRowSeed(randomized, maxDepth - y, maxWidth, counts, out var seed)) + break; + + int rowHeight = seed.SpanY; + if (rowHeight <= 0) + break; + + double rowArea = FillRow(randomized, rowHeight, maxWidth, y, counts, placements); + if (rowArea <= 0) + break; + + usedArea += rowArea; + y += rowHeight; + } + + if (placements.Count == 0) + return null; + + double utilization = area <= 0 ? 0 : usedArea / area; + int layerHeight = placements.Max(p => p.SkuType.Height); + int boxes = placements.Count; + + var metadata = new LayerMetadata(utilization, layerHeight, $"BLF attempt {attemptIndex}, boxes={boxes}, util={utilization:F3}"); + var layer = new Layer($"blf_attempt_{attemptIndex}", placements, metadata); + layer.Geometry = LayerGeometryBuilder.Build(layer, supportSurface); + return layer; + } + + private static bool TrySelectRowSeed(IEnumerable variants, int remainingDepth, int maxWidth, IDictionary counts, out SkuVariant seed) + { + foreach (var variant in variants) + { + if (!Fits(variant, remainingDepth, maxWidth)) + continue; + + if (!HasInventory(variant, counts)) + continue; + + seed = variant; + return true; + } + + seed = default; + return false; + } + + private static double FillRow(IReadOnlyList order, int rowHeight, int maxWidth, int yOffset, IDictionary counts, ICollection placements) + { + int x = 0; + double area = 0; + + while (x < maxWidth) { - var order = variants.OrderBy(_ => rand.Next()).ToList(); - var placements = new List(); - var counts = skus.ToDictionary(s => s.SkuId, _ => 0); - int y = 0; - - while (true) - { - var startVar = order.FirstOrDefault(v => v.h <= py - y && v.w <= px); - if (startVar == default) - break; - - int rowH = startVar.h; - int x = 0; - - while (true) - { - bool placedAny = false; - foreach (var (skuId, w, h, Rotated, refSku) in order) - { - if (h <= rowH && w <= px - x) - { - var skuObj = skus.First(s => s.SkuId == skuId); - if (counts[skuId] + 1 > skuObj.Quantity) - continue; - - placements.Add(new PositionedItem(refSku, x, y, Rotated)); - - counts[skuId]++; - x += w; - placedAny = true; - break; - } - } - if (!placedAny) break; - } - - y += rowH; - if (y >= py) - break; - } - - int boxes = counts.Values.Sum(); - if (boxes == 0) + if (!TryFindPlacementCandidate(order, rowHeight, maxWidth - x, counts, out var candidate)) + break; + + placements.Add(new PositionedItem(candidate.Sku, x, yOffset, candidate.Rotated)); + counts[candidate.Sku.SkuId]++; + x += candidate.SpanX; + area += candidate.SpanX * candidate.SpanY; + } + + return area; + } + + private static bool TryFindPlacementCandidate(IEnumerable variants, int rowHeight, int remainingWidth, IDictionary counts, out SkuVariant candidate) + { + foreach (var variant in variants) + { + if (!Fits(variant, rowHeight, remainingWidth)) + continue; + + if (!HasInventory(variant, counts)) continue; - double usedArea = placements.Sum(p => p.SkuType.Length * p.SkuType.Width); - double util = usedArea / area; - var usedSkus = skus.Where(s => counts[s.SkuId] > 0).ToList(); - int layerHeight = usedSkus.Count != 0 ? usedSkus.Max(s => s.Height) : 0; - - var key = string.Join(",", counts.Where(kv => kv.Value > 0) - .OrderBy(kv => kv.Key) - .Select(kv => $"{kv.Key}:{kv.Value}")); - - if (!foundLayers.TryGetValue(key, out Layer? value) || value.Metadata.Utilization < util) - { - value = new Layer($"blf_attempt_{attempt}", placements, new LayerMetadata(util, layerHeight, $"BLF attempt {attempt}, boxes={boxes}, util={util:F3}")); - value.Geometry = LayerGeometryBuilder.Build(value, supportSurface); - foundLayers[key] = value; - } + candidate = variant; + return true; } - return [.. foundLayers.Values]; + candidate = default; + return false; + } + + private static bool Fits(SkuVariant variant, int maxHeight, int maxWidth) + { + return variant.Sku != null && variant.SpanY <= maxHeight && variant.SpanX <= maxWidth && variant.SpanY > 0 && variant.SpanX > 0; + } + + private static bool HasInventory(SkuVariant variant, IDictionary counts) + { + if (!counts.TryGetValue(variant.Sku.SkuId, out var placed)) + return false; + + if (variant.Sku.Quantity <= 0) + return true; + + return placed < variant.Sku.Quantity; } } } diff --git a/Services/Layering/CPSATGenerationStrategy.cs b/Services/Layering/CPSATGenerationStrategy.cs index 71292ae..143a97b 100644 --- a/Services/Layering/CPSATGenerationStrategy.cs +++ b/Services/Layering/CPSATGenerationStrategy.cs @@ -19,7 +19,7 @@ public List Generate(List skus, SupportSurface supportSurface, Gener int py = supportSurface.Width; int gridStep = ComputeGridStep(skus); int maxTime = options.MaxSolverTime > 0 ? options.MaxSolverTime : 60; - int maxCandidates = options.MaxCandidates > 0 ? options.MaxCandidates : 2000; + int maxCandidates = options.MaxCPSATCandidates > 0 ? options.MaxCPSATCandidates : 2000; var candidates = new List<(int skuIndex, string skuId, int x, int y, int w, int h, bool rotated)>(); for (int si = 0; si < skus.Count; si++) diff --git a/Services/Layering/HomogeneousGenerationStrategy.cs b/Services/Layering/HomogeneousGenerationStrategy.cs index 86a07c1..f943484 100644 --- a/Services/Layering/HomogeneousGenerationStrategy.cs +++ b/Services/Layering/HomogeneousGenerationStrategy.cs @@ -1,4 +1,5 @@ -using Stack_Solver.Models; +using Stack_Solver.Helpers.Layering; +using Stack_Solver.Models; using Stack_Solver.Models.Layering; using Stack_Solver.Models.Metadata; using Stack_Solver.Models.Supports; @@ -11,59 +12,72 @@ public class HomogeneousGenerationStrategy : ILayerGenerationStrategy public List Generate(List skus, SupportSurface supportSurface, GenerationOptions options) { - var layers = new List(); + if (skus == null || skus.Count == 0 || supportSurface == null) + return []; int px = supportSurface.Length; int py = supportSurface.Width; - double area = px * py; - - foreach (var s in skus) - { - var orientations = new List<(int bw, int bh, string desc)> - { - (s.Length, s.Width, "normal") - }; + if (px <= 0 || py <= 0) + return []; - if (s.Rotatable && s.Length != s.Width) - orientations.Add((s.Width, s.Length, "rotated")); + double area = px * py; + if (area <= 0) + return []; - foreach (var (bw, bh, desc) in orientations) - { - int nx = px / bw; - int ny = py / bh; - int count = nx * ny; + var variants = SkuVariantFactory.CreateAllOrientations(skus); + if (variants.Count == 0) + return []; - if (count <= 0) - continue; + var candidateLayers = new List(); - double usedArea = count * bw * bh; - var placements = new List(); + foreach (var variant in variants) + { + var layer = BuildLayerForVariant(variant, px, py, area, supportSurface); + if (layer != null) + candidateLayers.Add(layer); + } - for (int ix = 0; ix < nx; ix++) - { - for (int iy = 0; iy < ny; iy++) - { - int x = ix * bw; - int y = iy * bh; - bool rotated = (desc == "rotated"); - placements.Add(new PositionedItem(s, x, y, rotated)); - } - } + return candidateLayers; + } - double utilization = usedArea / area; - string description = $"homogeneous {s.Name} ({desc}) {nx}x{ny}"; - int height = s.Height; + private static Layer? BuildLayerForVariant(SkuVariant variant, int palletLength, int palletWidth, double palletArea, SupportSurface supportSurface) + { + if (variant.Sku == null || variant.SpanX <= 0 || variant.SpanY <= 0) + return null; - var metadata = new LayerMetadata(utilization, height, description); + int nx = palletLength / variant.SpanX; + int ny = palletWidth / variant.SpanY; + int capacity = nx * ny; + if (capacity <= 0) + return null; - var layer = new Layer($"hom_grid_{s.Name.Replace(' ', '_')}_{desc}", placements, metadata); - layer.Geometry = LayerGeometryBuilder.Build(layer, supportSurface); + int allowed = variant.Sku.Quantity > 0 ? Math.Min(capacity, variant.Sku.Quantity) : capacity; + if (allowed <= 0) + return null; - layers.Add(layer); + var placements = new List(allowed); + int placed = 0; + for (int ix = 0; ix < nx && placed < allowed; ix++) + { + for (int iy = 0; iy < ny && placed < allowed; iy++) + { + placements.Add(new PositionedItem(variant.Sku, ix * variant.SpanX, iy * variant.SpanY, variant.Rotated)); + placed++; } } - return layers; + if (placements.Count == 0) + return null; + + double usedArea = placements.Count * variant.SpanX * variant.SpanY; + double utilization = palletArea > 0 ? usedArea / palletArea : 0; + string orientation = variant.Rotated ? "rotated" : "normal"; + 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.SkuId}_{orientation}", placements, metadata); + layer.Geometry = LayerGeometryBuilder.Build(layer, supportSurface); + return layer; } } } diff --git a/Services/Layering/StripFillGenerationStrategy.cs b/Services/Layering/StripFillGenerationStrategy.cs index a9e04b3..4fd0746 100644 --- a/Services/Layering/StripFillGenerationStrategy.cs +++ b/Services/Layering/StripFillGenerationStrategy.cs @@ -1,4 +1,5 @@ -using Stack_Solver.Models; +using Stack_Solver.Helpers.Layering; +using Stack_Solver.Models; using Stack_Solver.Models.Layering; using Stack_Solver.Models.Metadata; using Stack_Solver.Models.Supports; @@ -7,127 +8,181 @@ namespace Stack_Solver.Services.Layering { public class StripFillGenerationStrategy : ILayerGenerationStrategy { + private const int DefaultMaxRows = 50; + public string Name => "Strip-Fill"; public List Generate(List skus, SupportSurface supportSurface, GenerationOptions options) { - var layers = new List(); + if (skus == null || skus.Count == 0) + return []; + + int px = supportSurface.Length; int py = supportSurface.Width; + if (px <= 0 || py <= 0) + return []; + + double area = px * py; + var variants = SkuVariantFactory.CreateAllOrientations(skus); + if (variants.Count == 0) + return []; + - int maxRows = 50; - int randomSequences = 50; - int seed = Environment.TickCount; - var rand = new Random(seed); + var candidateLayers = new List(); + int maxRows = Math.Max(1, Math.Min(py, DefaultMaxRows)); - var variants = new List<(string sid, int w, int h, SKU sref)>(); - foreach (var s in skus) + for (int rowCount = 1; rowCount <= maxRows; rowCount++) { - variants.Add((s.SkuId, s.Length, s.Width, s)); - if (s.Rotatable && s.Length != s.Width) - variants.Add((s.SkuId, s.Width, s.Length, s)); + foreach (var sequence in BuildCandidateSequences(variants, rowCount)) + { + if (!SequenceFitsSurface(sequence, py)) + continue; + + var layer = TryBuildLayer(sequence, px, area, supportSurface); + if (layer != null) + candidateLayers.Add(layer); + } } - for (int nrows = 1; nrows <= maxRows; nrows++) + return LayerCandidateHelper.SelectBestBySkuCounts(candidateLayers); + } + + + private static IEnumerable> BuildCandidateSequences(IReadOnlyList variants, int rowCount) + { + if (variants.Count == 0) + yield break; + + var seen = new HashSet(StringComparer.Ordinal); + + foreach (var variant in variants) { - var candidateSequences = new List>(); + var seq = Enumerable.Repeat(variant, rowCount).ToList(); + if (seen.Add(BuildSequenceFingerprint(seq))) + yield return seq; + } - foreach (var v in variants) + for (int i = 0; i < variants.Count - 1; i++) + { + for (int j = i + 1; j < variants.Count; j++) { - candidateSequences.Add([.. Enumerable.Repeat(v, nrows)]); + var seq = BuildAlternatingSequence(variants[i], variants[j], rowCount); + if (seen.Add(BuildSequenceFingerprint(seq))) + yield return seq; } + } - if (variants.Count >= 2) - { - for (int i = 0; i < variants.Count; i++) - { - for (int j = i + 1; j < variants.Count; j++) - { - var seq = new List<(string sid, int w, int h, SKU sref)>(); - for (int k = 0; k < nrows; k++) - seq.Add(k % 2 == 0 ? variants[i] : variants[j]); - candidateSequences.Add(seq); - } - } - } + var ordered = variants + .OrderByDescending(v => v.SpanX * v.SpanY) + .ThenByDescending(v => v.SpanX) + .ThenBy(v => v.Sku.SkuId, StringComparer.Ordinal) + .ToList(); - for (int r = 0; r < randomSequences; r++) - { - var seq = new List<(string sid, int w, int h, SKU sref)>(); - for (int k = 0; k < nrows; k++) - seq.Add(variants[rand.Next(variants.Count)]); - candidateSequences.Add(seq); - } + for (int start = 0; start < ordered.Count; start++) + { + var seq = BuildWindowSequence(ordered, start, rowCount); + if (seen.Add(BuildSequenceFingerprint(seq))) + yield return seq; + } + } - foreach (var seq in candidateSequences) - { - int totalRowHeight = seq.Sum(v => v.h); - if (totalRowHeight > py) - continue; + private static List BuildAlternatingSequence(SkuVariant first, SkuVariant second, int length) + { + var seq = new List(length); + for (int idx = 0; idx < length; idx++) + { + seq.Add(idx % 2 == 0 ? first : second); + } - var itemCounts = new Dictionary(); - var placements = new List(); - double usedArea = 0; - int boxes = 0; - int yOffset = 0; - bool feasible = true; - - foreach (var (sid, w, h, sref) in seq) - { - int nx = px / w; - if (nx <= 0) - { - feasible = false; - break; - } - - for (int ix = 0; ix < nx; ix++) - { - placements.Add(new PositionedItem(sref, ix * w, yOffset, sref.Length != w)); - } - - int count = nx; - if (itemCounts.ContainsKey(sid)) - itemCounts[sid] += count; - else - itemCounts[sid] = count; - - usedArea += count * w * h; - boxes += count; - yOffset += h; - } - - if (!feasible) - continue; + return seq; + } - double util = usedArea / area; - string desc = $"rows={nrows} seq=" + string.Join(",", seq.Select(v => $"{v.sref.Name}:{v.w}x{v.h}")); - string lid = $"strip_r{nrows}"; + private static List BuildWindowSequence(IReadOnlyList ordered, int startIndex, int length) + { + var seq = new List(length); + for (int idx = 0; idx < length; idx++) + { + int sourceIndex = (startIndex + idx) % ordered.Count; + seq.Add(ordered[sourceIndex]); + } - int layerHeight = seq.Count != 0 ? seq.Max(v => v.sref.Height) : 0; - var metadata = new LayerMetadata(util, layerHeight, desc); + return seq; + } - var layer = new Layer(lid, placements, metadata); - layer.Geometry = LayerGeometryBuilder.Build(layer, supportSurface); + private static bool SequenceFitsSurface(IEnumerable sequence, int maxDepth) + { + int accumulated = 0; + foreach (var variant in sequence) + { + accumulated += variant.SpanY; + if (accumulated > maxDepth) + return false; + } + + return true; + } + + private static Layer? TryBuildLayer(IReadOnlyList sequence, int palletLength, double area, SupportSurface supportSurface) + { + var placements = new List(); + int yOffset = 0; + double usedArea = 0; + + foreach (var variant in sequence) + { + int perRow = palletLength / variant.SpanX; + if (perRow <= 0) + return null; - layers.Add(layer); + for (int ix = 0; ix < perRow; ix++) + { + placements.Add(new PositionedItem(variant.Sku, ix * variant.SpanX, yOffset, variant.Rotated)); } + + usedArea += perRow * variant.SpanX * variant.SpanY; + yOffset += variant.SpanY; } - var unique = new Dictionary(); - foreach (var layer in layers) - { - var key = string.Join(",", layer.Items - .GroupBy(i => i.SkuType.SkuId) - .OrderBy(g => g.Key) - .Select(g => $"{g.Key}:{g.Count()}")); + if (placements.Count == 0) + return null; + + double utilization = usedArea / area; + int layerHeight = sequence.Count != 0 ? sequence.Max(v => v.Sku.Height) : 0; + string description = BuildSequenceDescription(sequence); + string layerId = BuildLayerId(sequence); + + var metadata = new LayerMetadata(utilization, layerHeight, description); + var layer = new Layer(layerId, placements, metadata); + layer.Geometry = LayerGeometryBuilder.Build(layer, supportSurface); + return layer; + } + + private static string BuildSequenceDescription(IReadOnlyList sequence) + { + var parts = sequence + .Select(v => $"{v.Sku.Name}:{v.SpanX}x{v.SpanY}"); - if (!unique.TryGetValue(key, out Layer? value) || value.Metadata.Utilization < layer.Metadata.Utilization) - unique[key] = layer; + return $"rows={sequence.Count} seq=" + string.Join(",", parts); + } + + private static string BuildLayerId(IReadOnlyList sequence) + { + var fingerprint = BuildSequenceFingerprint(sequence); + int hash = 17; + foreach (char ch in fingerprint) + { + hash = (hash * 23) + ch; } - return [.. unique.Values]; + hash = Math.Abs(hash); + return $"strip_r{sequence.Count}_{hash}"; + } + + private static string BuildSequenceFingerprint(IEnumerable sequence) + { + return string.Join("|", sequence.Select(v => $"{v.VariantId}:{v.SpanX}x{v.SpanY}")); } } } diff --git a/ViewModels/Pages/LayerAnalyzerViewModel.cs b/ViewModels/Pages/LayerAnalyzerViewModel.cs index f2f2e27..e578b28 100644 --- a/ViewModels/Pages/LayerAnalyzerViewModel.cs +++ b/ViewModels/Pages/LayerAnalyzerViewModel.cs @@ -81,6 +81,7 @@ public LayerAnalyzerViewModel(IEventAggregator events, ILayerVisualizationServic private double _palletHeight; private bool _useCpsat; private int _maxCpsatCandidates; + private int _blfAttempts; private int _solverTimeLimit; private int _maxStackHeight; private int _maxStackWeight; @@ -93,6 +94,7 @@ private void OnSettingsChanged(SettingsChangedMessage msg) _palletHeight = msg.PalletHeight; _useCpsat = msg.UseCpsat; _maxCpsatCandidates = msg.MaxCpsatCandidates; + _blfAttempts = msg.BlfAttempts; _solverTimeLimit = msg.SolverTimeLimit; _maxStackHeight = msg.MaxStackHeight; _maxStackWeight = msg.MaxStackWeight; @@ -205,7 +207,7 @@ private async Task Generate() } var pallet = new Pallet("Pallet", _palletLength, _palletWidth, (int)Math.Round(_palletHeight)); - var options = new GenerationOptions(_solverTimeLimit, _maxCpsatCandidates); + var options = new GenerationOptions(_solverTimeLimit, _maxCpsatCandidates, _blfAttempts); var ct = localCts.Token; var strategiesList = new List diff --git a/ViewModels/Pages/PalletBuilderSettingsViewModel.cs b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs index f3777d2..a6f0e38 100644 --- a/ViewModels/Pages/PalletBuilderSettingsViewModel.cs +++ b/ViewModels/Pages/PalletBuilderSettingsViewModel.cs @@ -33,6 +33,9 @@ public partial class PalletBuilderSettingsViewModel : ObservableObject [ObservableProperty] private int _maxCpsatCandidates; + [ObservableProperty] + private int _blfAttempts; + [ObservableProperty] private int _solverTimeLimit; @@ -82,7 +85,8 @@ public PalletBuilderSettingsViewModel(ISkuRepository skuRepository, IEventAggreg _skuRepository.SkuDeleted += OnSkuDeleted; SolverTimeLimit = _defaults.MaxSolverTime; - MaxCpsatCandidates = _defaults.MaxCandidates; + MaxCpsatCandidates = _defaults.MaxCPSATCandidates; + BlfAttempts = _defaults.BLFAttempts; PalletLength = _palletDefaults.PalletLength; PalletWidth = _palletDefaults.PalletWidth; @@ -123,7 +127,7 @@ public async Task InitializeAsync() } SolverTimeLimit = _defaults.MaxSolverTime; - MaxCpsatCandidates = _defaults.MaxCandidates; + MaxCpsatCandidates = _defaults.MaxCPSATCandidates; _isInitialized = true; PublishSettingsChanged(); @@ -158,7 +162,7 @@ private void PublishSettingsChanged() { _events.Publish(new SettingsChangedMessage( PalletLength, PalletWidth, PalletHeight, - UseCpsat, MaxCpsatCandidates, SolverTimeLimit, + UseCpsat, MaxCpsatCandidates, BlfAttempts, SolverTimeLimit, MaxStackHeight, MaxStackWeight, [.. Skus])); } @@ -248,6 +252,7 @@ public record SettingsChangedMessage( double PalletHeight, bool UseCpsat, int MaxCpsatCandidates, + int BlfAttempts, int SolverTimeLimit, int MaxStackHeight, int MaxStackWeight, diff --git a/Views/Pages/PalletBuilderPage.xaml b/Views/Pages/PalletBuilderPage.xaml index 37f990e..480d0de 100644 --- a/Views/Pages/PalletBuilderPage.xaml +++ b/Views/Pages/PalletBuilderPage.xaml @@ -6,6 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:conv="clr-namespace:Stack_Solver.Converters" + xmlns:behaviors="clr-namespace:Stack_Solver.Helpers.Behaviors" Title="PalletBuilderPage" d:DataContext="{d:DesignInstance local:PalletBuilderPage, IsDesignTimeCreatable=False}" @@ -65,134 +66,135 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/Pages/PalletBuilderPage.xaml.cs b/Views/Pages/PalletBuilderPage.xaml.cs index 3813d66..3d60713 100644 --- a/Views/Pages/PalletBuilderPage.xaml.cs +++ b/Views/Pages/PalletBuilderPage.xaml.cs @@ -1,8 +1,8 @@ -using Stack_Solver.Helpers.Rendering; -using Stack_Solver.Models; +using Stack_Solver.Models; using Stack_Solver.Models.Layering; using Stack_Solver.ViewModels.Pages; using System.Windows.Controls; +using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Media3D; using Wpf.Ui.Abstractions.Controls; @@ -31,7 +31,7 @@ private async void OnLoaded(object? sender, RoutedEventArgs e) } } - private void MainViewPort_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + private void MainViewPort_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { var pos = e.GetPosition(MainViewPort); var hitParams = new PointHitTestParameters(pos); diff --git a/defaults.json b/defaults.json index c33dcf6..b020653 100644 --- a/defaults.json +++ b/defaults.json @@ -1,7 +1,8 @@ { "LayerGeneration": { "MaxSolverTime": 60, - "MaxCandidates": 2000 + "MaxCPSATCandidates": 2000, + "BLFAttempts": 200 }, "PalletDefaults": { "DefaultCatalog": "International",