- 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>
128 lines
5.4 KiB
C#
128 lines
5.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace BoxPacker3D
|
|
{
|
|
/// <summary>
|
|
/// Extreme Points packing algorithm — an alternative to <see cref="LayerPacker"/> (skyline).
|
|
/// Unlike LayerPacker, item types are free to mix at any position; each item is placed
|
|
/// greedily at the best available candidate point. This trades the tidy, layer-by-layer
|
|
/// look of skyline packing for potentially tighter volume usage.
|
|
///
|
|
/// Same input contract as LayerPacker.Pack: items rotate only around the vertical axis
|
|
/// (W↔D, H preserved), itemPad separates items, wallPad+wallThickness (edge) separates
|
|
/// items from the walls, the resulting box is tightened to [minBox, maxBox], and an item
|
|
/// only ends up unpacked if it truly does not fit anywhere within maxBox. Items are
|
|
/// processed in whatever order allItems is given (manual priority — e.g. table row
|
|
/// order in the UI), not sorted by volume.
|
|
/// </summary>
|
|
public class Packer3D : IPacker
|
|
{
|
|
private const double EPS = 0.001;
|
|
|
|
public (List<PackedItem> packed, Box box, List<Item> unpacked) Pack(
|
|
List<Item> allItems, Box minBox, Box maxBox,
|
|
double wallPad, double itemPad, double wallThickness)
|
|
{
|
|
var packed = new List<PackedItem>();
|
|
var unpacked = new List<Item>();
|
|
double edge = wallPad + wallThickness;
|
|
|
|
if (allItems.Count == 0)
|
|
return (packed, minBox, unpacked);
|
|
|
|
double maxIW = Math.Max(EPS, maxBox.W - 2 * edge);
|
|
double maxIH = Math.Max(EPS, maxBox.H - 2 * edge);
|
|
double maxID = Math.Max(EPS, maxBox.D - 2 * edge);
|
|
|
|
// Manual priority — whatever order allItems is already in (e.g. table row
|
|
// order in the UI), same as LayerPacker; no volume-based sorting.
|
|
var items = allItems;
|
|
|
|
// Extreme points: candidate positions where the next item's corner may start.
|
|
var points = new List<(double X, double Y, double Z)> { (edge, edge, edge) };
|
|
|
|
foreach (var item in items)
|
|
{
|
|
(double x, double y, double z, double w, double h, double d)? best = null;
|
|
|
|
// Prefer low, then near-origin points — keeps the pack compact/stable.
|
|
foreach (var p in points.OrderBy(p => p.Y).ThenBy(p => p.X).ThenBy(p => p.Z))
|
|
{
|
|
foreach (var (iw, ih, id) in Orientations(item))
|
|
{
|
|
if (p.X + iw > edge + maxIW + EPS) continue;
|
|
if (p.Y + ih > edge + maxIH + EPS) continue;
|
|
if (p.Z + id > edge + maxID + EPS) continue;
|
|
|
|
if (Overlaps(packed, p.X, p.Y, p.Z, iw, ih, id, itemPad)) continue;
|
|
|
|
best = (p.X, p.Y, p.Z, iw, ih, id);
|
|
break;
|
|
}
|
|
if (best != null) break;
|
|
}
|
|
|
|
if (best == null)
|
|
{
|
|
unpacked.Add(item);
|
|
continue;
|
|
}
|
|
|
|
var (bx, by, bz, bw, bh, bd) = best.Value;
|
|
packed.Add(new PackedItem(item, bx, by, bz, bw, bh, bd));
|
|
|
|
// New extreme points projected from the placed item's far corners.
|
|
points.Add((bx + bw + itemPad, by, bz));
|
|
points.Add((bx, by + bh + itemPad, bz));
|
|
points.Add((bx, by, bz + bd + itemPad));
|
|
}
|
|
|
|
if (packed.Count == 0)
|
|
return (packed, minBox, unpacked);
|
|
|
|
var box = TightenBox(packed, edge, minBox, maxBox, wallThickness);
|
|
return (packed, box, unpacked);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Two allowed orientations: original and rotated 90° around the vertical axis (W↔D).
|
|
/// H is always preserved — items cannot be tilted sideways.
|
|
/// </summary>
|
|
private static IEnumerable<(double iW, double iH, double iD)> Orientations(Item proto)
|
|
{
|
|
yield return (proto.W, proto.H, proto.D);
|
|
if (Math.Abs(proto.W - proto.D) > EPS)
|
|
yield return (proto.D, proto.H, proto.W);
|
|
}
|
|
|
|
private static bool Overlaps(
|
|
List<PackedItem> packed, double x, double y, double z, double w, double h, double d,
|
|
double itemPad)
|
|
{
|
|
foreach (var p in packed)
|
|
{
|
|
bool sepX = x + w + itemPad <= p.X + EPS || p.X + p.RW + itemPad <= x + EPS;
|
|
bool sepY = y + h + itemPad <= p.Y + EPS || p.Y + p.RH + itemPad <= y + EPS;
|
|
bool sepZ = z + d + itemPad <= p.Z + EPS || p.Z + p.RD + itemPad <= z + EPS;
|
|
if (!(sepX || sepY || sepZ)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shrinks the box to just enclose all packed items plus edge on every side.
|
|
/// </summary>
|
|
private static Box TightenBox(
|
|
List<PackedItem> packed, double edge,
|
|
Box minBox, Box maxBox, double wallThickness)
|
|
{
|
|
double tw = Math.Clamp(Math.Ceiling(packed.Max(p => p.X + p.RW) + edge), minBox.W, maxBox.W);
|
|
double th = Math.Clamp(Math.Ceiling(packed.Max(p => p.Y + p.RH) + edge), minBox.H, maxBox.H);
|
|
double td = Math.Clamp(Math.Ceiling(packed.Max(p => p.Z + p.RD) + edge), minBox.D, maxBox.D);
|
|
return new Box(tw, th, td, wallThickness);
|
|
}
|
|
}
|
|
}
|