Files
BoxPacker3D/ItemDatabase.cs
T
romanandClaude Sonnet 5 654e1b79d0 Rewrite LayerPacker as skyline packer, add Extreme Points alternative, manual footprint tuning and reordering UI
- 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>
2026-08-19 14:23:54 +02:00

204 lines
7.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 &gt; 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;
}
}
}