- Replace the old layer-grid LayerPacker with a skyline (lowest-slot-first) algorithm: grows the footprint only when items truly can't fit within maxBox, lets shorter types stack multiple sub-layers into taller leftover space, cascades unfinished layers to later types, and allows a staircase profile to minimize wasted volume. - Fix ItemDatabaseEditor NumericUpDown crash (Value set before Minimum/Maximum) and ItemDatabase.Upsert doing INSERT instead of UPDATE (was keyed by Name, now by Id). - Add Packer3D as a working Extreme Points algorithm (was dead/commented-out code) behind a shared IPacker interface, selectable in the UI alongside Skyline. - Add manual footprint (Skyline) / box-size (Extreme Points) adjustment controls for the first box, with correct min/max clamping against the configured max box size. - Add a "Poradie" column with up/down buttons to reorder packing items; both packers now honor that manual order instead of re-sorting by volume. - Drop multi-box packing (single box only), add a pre-pack volume warning, and make unpacked items exportable to PDF / inspectable via a result-label tooltip, both grouped by type with dimensions, count and total volume. - Merge the min/max box dimension groups, hide the now-unused box navigation group, and misc UI cleanup (typo fix, group renames, wider item name column). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
204 lines
7.3 KiB
C#
204 lines
7.3 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using Microsoft.Data.Sqlite;
|
||
|
||
namespace BoxPacker3D
|
||
{
|
||
/// <summary>
|
||
/// Persistent template of an item that can be re-used across packing sessions.
|
||
/// Stored in an on-disk SQLite database.
|
||
/// </summary>
|
||
public class ItemTemplate
|
||
{
|
||
public int Id { get; set; }
|
||
public string Name { get; set; } = "";
|
||
public double Width { get; set; }
|
||
public double Height { get; set; }
|
||
public double Depth { get; set; }
|
||
|
||
public override string ToString() =>
|
||
$"{Name} ({Width}×{Height}×{Depth} mm)";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Simple SQLite-backed repository for <see cref="ItemTemplate"/>s.
|
||
/// Database schema is created on first use.
|
||
/// </summary>
|
||
public class ItemDatabase
|
||
{
|
||
private readonly string _connectionString;
|
||
|
||
public ItemDatabase(string dbPath)
|
||
{
|
||
// Ensure directory exists
|
||
var dir = Path.GetDirectoryName(dbPath);
|
||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||
Directory.CreateDirectory(dir);
|
||
|
||
_connectionString = new SqliteConnectionStringBuilder
|
||
{
|
||
DataSource = dbPath,
|
||
Mode = SqliteOpenMode.ReadWriteCreate
|
||
}.ToString();
|
||
|
||
EnsureSchema();
|
||
}
|
||
|
||
private void EnsureSchema()
|
||
{
|
||
using var conn = Open();
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = @"
|
||
CREATE TABLE IF NOT EXISTS items (
|
||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
Name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||
Width REAL NOT NULL,
|
||
Height REAL NOT NULL,
|
||
Depth REAL NOT NULL
|
||
);";
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
|
||
private SqliteConnection Open()
|
||
{
|
||
var conn = new SqliteConnection(_connectionString);
|
||
conn.Open();
|
||
return conn;
|
||
}
|
||
|
||
/// <summary>Returns all templates ordered alphabetically by name.</summary>
|
||
public List<ItemTemplate> GetAll()
|
||
{
|
||
var list = new List<ItemTemplate>();
|
||
using var conn = Open();
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = "SELECT Id, Name, Width, Height, Depth FROM items ORDER BY Name COLLATE NOCASE;";
|
||
using var rdr = cmd.ExecuteReader();
|
||
while (rdr.Read())
|
||
list.Add(new ItemTemplate
|
||
{
|
||
Id = rdr.GetInt32(0),
|
||
Name = rdr.GetString(1),
|
||
Width = rdr.GetDouble(2),
|
||
Height = rdr.GetDouble(3),
|
||
Depth = rdr.GetDouble(4)
|
||
});
|
||
return list;
|
||
}
|
||
|
||
/// <summary>Returns the template with the given name or null if none exists.</summary>
|
||
public ItemTemplate? FindByName(string name)
|
||
{
|
||
using var conn = Open();
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = "SELECT Id, Name, Width, Height, Depth FROM items WHERE Name = $n COLLATE NOCASE LIMIT 1;";
|
||
cmd.Parameters.AddWithValue("$n", name);
|
||
using var rdr = cmd.ExecuteReader();
|
||
if (rdr.Read())
|
||
return new ItemTemplate
|
||
{
|
||
Id = rdr.GetInt32(0),
|
||
Name = rdr.GetString(1),
|
||
Width = rdr.GetDouble(2),
|
||
Height = rdr.GetDouble(3),
|
||
Depth = rdr.GetDouble(4)
|
||
};
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Inserts a new template (Id == 0) or updates the existing row by Id (Id > 0).
|
||
/// Using Id rather than Name for the update key means renaming an existing item
|
||
/// during edit correctly updates that same row instead of leaving the old-named
|
||
/// row behind and inserting a new one.
|
||
/// </summary>
|
||
public void Upsert(ItemTemplate item)
|
||
{
|
||
using var conn = Open();
|
||
using var cmd = conn.CreateCommand();
|
||
if (item.Id > 0)
|
||
{
|
||
cmd.CommandText = @"
|
||
UPDATE items SET Name = $n, Width = $w, Height = $h, Depth = $d
|
||
WHERE Id = $id;";
|
||
cmd.Parameters.AddWithValue("$id", item.Id);
|
||
}
|
||
else
|
||
{
|
||
cmd.CommandText = @"
|
||
INSERT INTO items (Name, Width, Height, Depth)
|
||
VALUES ($n, $w, $h, $d);";
|
||
}
|
||
cmd.Parameters.AddWithValue("$n", item.Name);
|
||
cmd.Parameters.AddWithValue("$w", item.Width);
|
||
cmd.Parameters.AddWithValue("$h", item.Height);
|
||
cmd.Parameters.AddWithValue("$d", item.Depth);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
|
||
public void DeleteByName(string name)
|
||
{
|
||
using var conn = Open();
|
||
using var cmd = conn.CreateCommand();
|
||
cmd.CommandText = "DELETE FROM items WHERE Name = $n COLLATE NOCASE;";
|
||
cmd.Parameters.AddWithValue("$n", name);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Checks if a template with the same name or exact dimensions already exists.
|
||
/// Returns the existing template if found, null otherwise.
|
||
/// </summary>
|
||
public ItemTemplate? FindDuplicate(ItemTemplate item)
|
||
{
|
||
using var conn = Open();
|
||
using var cmd = conn.CreateCommand();
|
||
|
||
// Check for same name (case-insensitive)
|
||
cmd.CommandText = "SELECT Id, Name, Width, Height, Depth FROM items WHERE Name = $n COLLATE NOCASE LIMIT 1;";
|
||
cmd.Parameters.AddWithValue("$n", item.Name);
|
||
using (var rdr = cmd.ExecuteReader())
|
||
{
|
||
if (rdr.Read())
|
||
{
|
||
return new ItemTemplate
|
||
{
|
||
Id = rdr.GetInt32(0),
|
||
Name = rdr.GetString(1),
|
||
Width = rdr.GetDouble(2),
|
||
Height = rdr.GetDouble(3),
|
||
Depth = rdr.GetDouble(4)
|
||
};
|
||
}
|
||
}
|
||
|
||
// Check for same dimensions (exact match)
|
||
cmd.CommandText = @"
|
||
SELECT Id, Name, Width, Height, Depth FROM items
|
||
WHERE Width = $w AND Height = $h AND Depth = $d
|
||
LIMIT 1;";
|
||
cmd.Parameters.Clear();
|
||
cmd.Parameters.AddWithValue("$w", item.Width);
|
||
cmd.Parameters.AddWithValue("$h", item.Height);
|
||
cmd.Parameters.AddWithValue("$d", item.Depth);
|
||
using (var rdr = cmd.ExecuteReader())
|
||
{
|
||
if (rdr.Read())
|
||
{
|
||
return new ItemTemplate
|
||
{
|
||
Id = rdr.GetInt32(0),
|
||
Name = rdr.GetString(1),
|
||
Width = rdr.GetDouble(2),
|
||
Height = rdr.GetDouble(3),
|
||
Depth = rdr.GetDouble(4)
|
||
};
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
}
|