Files
BoxPacker3D/Packer3D.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

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);
}
}
}