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