Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public partial class App
.ConfigureAppConfiguration(c =>
{
c.SetBasePath(Path.GetDirectoryName(AppContext.BaseDirectory)!);
c.AddJsonFile("defaults.json", optional: true, reloadOnChange: true);
c.AddJsonFile("defaults.json", optional: true, reloadOnChange: false);
c.AddJsonFile(AppPaths.UserSettingsFile, optional: true, reloadOnChange: false);
})
.ConfigureServices((context, services) =>
{
Expand Down Expand Up @@ -70,14 +71,17 @@ public partial class App
services.AddSingleton<SKULibraryPage>();
services.AddSingleton<SKULibraryViewModel>();
services.AddSingleton<PalletBuilderPage>();
services.AddSingleton<IUserSettingsService, UserSettingsService>();

services.AddSingleton<PalletBuilderViewModel>(sp => new PalletBuilderViewModel(
sp.GetRequiredService<ISkuRepository>(),
sp.GetRequiredService<IEventAggregator>(),
sp.GetRequiredService<ILayerVisualizationService>(),
sp.GetRequiredService<IOptions<GenerationOptions>>(),
sp.GetRequiredService<IOptions<PalletDefaultsOptions>>(),
sp.GetRequiredService<IValidator<PalletSettingsDto>>(),
sp.GetRequiredService<IValidator<SkuQuantityDto>>()));
sp.GetRequiredService<IValidator<SkuQuantityDto>>(),
sp.GetRequiredService<IUserSettingsService>()));
services.AddSingleton<TruckLoadingPage>();
services.AddSingleton<TruckLoadingViewModel>();
services.AddSingleton<JobManagerPage>();
Expand Down
2 changes: 2 additions & 0 deletions Data/AppPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public static class AppPaths

public static readonly string DatabaseFile = Path.Combine(AppDataDirectory, "stacksolver.db");

public static readonly string UserSettingsFile = Path.Combine(AppDataDirectory, "user-settings.json");

public static void EnsureAppData()
{
if (!Directory.Exists(AppDataDirectory))
Expand Down
88 changes: 88 additions & 0 deletions Helpers/Rendering/PalletSceneBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Stack_Solver.Models.Supports;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace Stack_Solver.Helpers.Rendering
{
public class PalletSceneBuilder
{
private readonly Dictionary<string, Brush> _skuBrushCache = [];
private readonly Lock _cacheLock = new();

public async Task BuildAsync(
Model3DGroup target,
PalletTemplate template,
int palletLength, int palletWidth, double palletHeight,
CancellationToken ct = default)
{
if (target == null || template == null) return;

var tempGroup = await Task.Run(() =>
{
ct.ThrowIfCancellationRequested();
var g = new Model3DGroup();

g.Children.Add(new AmbientLight(Colors.DimGray));
g.Children.Add(new DirectionalLight(Colors.White, new Vector3D(-1, -2, -1)));

var palletBrush = new SolidColorBrush(Color.FromRgb(160, 120, 80));
palletBrush.Freeze();
g.Children.Add(GeometryCreator.CreateBoxWithEdges(
new Point3D(0, 0, 0), palletLength, palletHeight, palletWidth,
palletBrush, Colors.Black, 0.4));

double currentY = palletHeight;
foreach (var layer in template.Layers)
{
ct.ThrowIfCancellationRequested();

foreach (var item in layer.Items)
{
var sku = item.SkuType;
double boxLength = item.Rotated ? sku.Width : sku.Length;
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));
}

currentY += layer.Metadata.Height;
}

TryFreezeRecursive(g);
return g;
}, ct).ConfigureAwait(true);

ct.ThrowIfCancellationRequested();
target.Children.Clear();
foreach (var child in tempGroup.Children)
target.Children.Add(child);
}

private Brush GetBrushForSku(string skuId)
{
lock (_cacheLock)
{
if (_skuBrushCache.TryGetValue(skuId, out var b)) return b;
int hash = skuId.GetHashCode();
byte r = (byte)(50 + (hash & 0x7F));
byte g = (byte)(50 + ((hash >> 7) & 0x7F));
byte bl = (byte)(50 + ((hash >> 14) & 0x7F));
var brush = new SolidColorBrush(Color.FromRgb(r, g, bl));
if (brush.CanFreeze) brush.Freeze();
_skuBrushCache[skuId] = brush;
return brush;
}
}

private static void TryFreezeRecursive(Model3D model)
{
if (model is Model3DGroup group)
foreach (var child in group.Children)
TryFreezeRecursive(child);
if (model is Freezable f && f.CanFreeze && !f.IsFrozen)
try { f.Freeze(); } catch { }
}
}
}
9 changes: 9 additions & 0 deletions Helpers/Rendering/ViewportController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ public void Pan(Point current)
UpdateCameraPosition();
}

public void ResetView(Point3D target, double distance)
{
Target = target;
Distance = distance;
Azimuth = Math.PI / 4;
Elevation = Math.PI / 6;
UpdateCameraPosition();
}

private void UpdateCameraPosition()
{
double x = Target.X + Distance * Math.Cos(Elevation) * Math.Sin(Azimuth);
Expand Down
25 changes: 25 additions & 0 deletions Infrastructure/UserSettingsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Stack_Solver.Data;
using Stack_Solver.Models;
using System.IO;
using System.Text.Json;

namespace Stack_Solver.Infrastructure
{
public interface IUserSettingsService
{
Task SaveAsync(PalletDefaultsOptions palletDefaults, GenerationOptions genOptions);
}

public class UserSettingsService : IUserSettingsService
{
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };

public async Task SaveAsync(PalletDefaultsOptions palletDefaults, GenerationOptions genOptions)
{
AppPaths.EnsureAppData();
var data = new { LayerGeneration = genOptions, PalletDefaults = palletDefaults };
var json = JsonSerializer.Serialize(data, _jsonOptions);
await File.WriteAllTextAsync(AppPaths.UserSettingsFile, json);
}
}
}
13 changes: 13 additions & 0 deletions Models/Assignment/AssignmentResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Stack_Solver.Models.Supports;

namespace Stack_Solver.Models.Assignment
{
public class AssignmentResult
{
public IReadOnlyList<(PalletTemplate Template, int Count)> Assignments { get; init; } = [];
public IReadOnlyDictionary<string, int> Leftovers { get; init; } = new Dictionary<string, int>();

public int TotalPallets => Assignments.Sum(a => a.Count);
public bool HasLeftovers => Leftovers.Values.Any(v => v > 0);
}
}
4 changes: 4 additions & 0 deletions Models/SKU.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Stack_Solver.Models
{
Expand All @@ -20,6 +21,9 @@ public class SKU

public int Quantity { get; set; } = 0;

[NotMapped]
public bool IsSelected { get; set; } = false;

public SKU(string skuId, string name, int length, int width, int height, double weight, bool rotatable, string notes)
{
SkuId = skuId;
Expand Down
7 changes: 5 additions & 2 deletions Models/Supports/Pallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ namespace Stack_Solver.Models.Supports
/// <param name="height">The height of the pallet.</param>
public class Pallet(string name, int length, int width, int height) : SupportSurface(name, length, width, height)
{
List<Layer> Layers { get; } = [];
public int MaxStackHeight { get; init; } = 180;
public int MaxStackWeight { get; init; } = 950;
public double MaxSkuOverhang { get; init; } = 0;

PalletMetadata Metadata { get; set; } = new();
public List<Layer> Layers { get; } = [];
public PalletMetadata Metadata { get; set; } = new();
}
}
33 changes: 33 additions & 0 deletions Models/Supports/PalletTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Stack_Solver.Models.Layering;

namespace Stack_Solver.Models.Supports
{
public class PalletTemplate
{
public string Id { get; } = Guid.NewGuid().ToString();
public IReadOnlyList<Layer> Layers { get; init; } = [];
public double TotalHeight { get; init; }
public double TotalWeight { get; init; }
public IReadOnlyDictionary<string, int> SkuCounts { get; init; } = new Dictionary<string, int>();
public int TotalBoxCount { get; init; }
public double AverageLayerUtilization { get; init; }

public static PalletTemplate FromLayers(IReadOnlyList<Layer> layers)
{
var skuCounts = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var layer in layers)
foreach (var item in layer.Items)
skuCounts[item.SkuType.SkuId] = skuCounts.GetValueOrDefault(item.SkuType.SkuId) + 1;

return new PalletTemplate
{
Layers = layers,
TotalHeight = layers.Sum(l => l.Metadata.Height),
TotalWeight = layers.Sum(l => l.Metrics.TotalWeight),
SkuCounts = skuCounts,
TotalBoxCount = layers.Sum(l => l.Items.Count),
AverageLayerUtilization = layers.Count > 0 ? layers.Average(l => l.Metadata.Utilization) : 0
};
}
}
}
26 changes: 26 additions & 0 deletions Properties/Settings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Properties/Settings.settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
</SettingsFile>
Loading
Loading