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",