using System;
using System.Collections.Generic;
using System.Linq;
namespace BoxPacker3D
{
///
/// Extreme Points packing algorithm — an alternative to (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.
///
public class Packer3D : IPacker
{
private const double EPS = 0.001;
public (List packed, Box box, List- unpacked) Pack(
List
- allItems, Box minBox, Box maxBox,
double wallPad, double itemPad, double wallThickness)
{
var packed = new List();
var unpacked = new List
- ();
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);
}
///
/// Two allowed orientations: original and rotated 90° around the vertical axis (W↔D).
/// H is always preserved — items cannot be tilted sideways.
///
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 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;
}
///
/// Shrinks the box to just enclose all packed items plus edge on every side.
///
private static Box TightenBox(
List 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);
}
}
}