using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace BoxPacker3D
{
///
/// Layer-based packing algorithm.
///
/// 1. Compute total padded volume: each item slot = (W+p)×(H+p)×(D+p), sum all types.
/// 2. t = paddedVol / maxInnerVol → estimate box via linear interpolation
/// dim(t) = dim_min + t*(dim_max – dim_min) for W, H, D
/// 3. Start with the type whose single item has the largest volume.
/// Items may only rotate around the vertical axis (W↔D), H is always preserved.
/// 4. Snap D: fit a whole number of items into estimated D.
/// 5. Snap W: fit a whole number of items into estimated W.
/// 6. Stack H: add as many layers upward as needed for all items of this type.
/// Cap at maxBox.H; excess items → unpacked.
/// 7. Subsequent types: add full horizontal layers on top (same W×D footprint).
/// If the type does not fill a complete layer in original orientation, try W↔D rotation.
/// 8. After every placement step, verify that no dimension exceeds maxBox.
/// 9. Tighten box so wall insulation touches items on all six sides.
/// 10. Items that do not fit → unpacked list (shown in PDF export).
///
public class LayerPacker
{
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);
// ── Step 1: total padded volume ───────────────────────────────────────
// Each item occupies (W+p)×(H+p)×(D+p) — padding is shared between
// neighbours so only one layer per side is counted.
double totalPaddedVol = allItems.Sum(i =>
(i.W + itemPad) * (i.H + itemPad) * (i.D + itemPad));
// ── Step 2: estimated box size via linear interpolation ───────────────
// t = ratio of needed inner volume to maximum available inner volume
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);
double maxInnerVol = maxIW * maxIH * maxID;
double t = Math.Min(1.0, Math.Max(0.0, totalPaddedVol / maxInnerVol));
// a(t) = a_min + t*(a_max – a_min)
double estW = minBox.W + t * (maxBox.W - minBox.W);
double estH = minBox.H + t * (maxBox.H - minBox.H);
double estD = minBox.D + t * (maxBox.D - minBox.D);
// Inner (usable) dimensions from the estimate
double estIW = Math.Max(0, estW);
double estIH = Math.Max(0, estH);
double estID = Math.Max(0, estD);
// ── Step 3: sort types by single-item volume descending ───────────────
var types = allItems
.GroupBy(i => i.Name)
.Select(g => (Proto: g.First(), Items: g.ToList()))
.OrderByDescending(g => g.Proto.Volume)
.ToList();
// ── Steps 4–6: place first type ───────────────────────────────────────
var (proto0, items0) = types[0];
bool firstPlaced = false;
double extW = 0, extH = 0, extD = 0;
// Open (incomplete) last layer state
double openLayerY = double.NaN;
int openFilled = 0; // how many slots are already occupied
int openCapacity = 0; // total slots in that layer (nW×nD)
double openIW = 0, openIH = 0, openID = 0;
int openNW = 0, openND = 0;
foreach (var (iW, iH, iD) in Orientations(proto0))
{
// Step 4: snap D to whole number of items (based on estimated D)
int nD = Math.Max(1, (int)((estID + itemPad) / (iD + itemPad)));
nD = Math.Min(nD, (int)((maxID + itemPad) / (iD + itemPad)));
if (nD < 1) continue;
// Step 5: snap W to whole number of items (based on estimated W)
int nW = Math.Max(1, (int)((estIW + itemPad) / (iW + itemPad)));
nW = Math.Min(nW, (int)((maxIW + itemPad) / (iW + itemPad)));
if (nW < 1) continue;
// Step 6: H layers needed for all items; cap at maxBox.H
int nH = (int)Math.Ceiling((double)items0.Count / (nD * nW));
nH = Math.Min(nH, (int)((maxIH + itemPad) / (iH + itemPad)));
if (nH < 1) continue;
// Place items layer by layer from bottom to top
int idx = 0;
for (int iy = 0; iy < nH && idx < items0.Count; iy++)
for (int iz = 0; iz < nD && idx < items0.Count; iz++)
for (int ix = 0; ix < nW && idx < items0.Count; ix++)
{
packed.Add(new PackedItem(items0[idx++],
edge + ix * (iW + itemPad),
edge + iy * (iH + itemPad),
edge + iz * (iD + itemPad),
iW, iH, iD));
}
// Items that didn't fit (nH was capped) → unpacked
for (int i = idx; i < items0.Count; i++)
unpacked.Add(items0[i]);
extW = nW * (iW + itemPad) - itemPad;
extH = nH * (iH + itemPad) - itemPad;
extD = nD * (iD + itemPad) - itemPad;
// Track open last layer if it was not completely filled.
// lastFilled == 0 when: (a) last layer is perfectly full, OR
// (b) nH was capped so idx = nH*perLayer0 (exact multiple) — in
// that case the last placed layer IS full, nothing to fill.
// In both cases there is no open layer → openLayerY stays NaN.
int perLayer0 = nW * nD;
int lastFilled = idx % perLayer0;
if (lastFilled > 0)
{
openLayerY = edge + (nH - 1) * (iH + itemPad);
openFilled = lastFilled;
openCapacity = perLayer0;
openIW = iW; openIH = iH; openID = iD;
openNW = nW; openND = nD;
}
Debug.WriteLine(
$"[LP] Type0 nW={nW} nD={nD} nH={nH} perLayer={perLayer0} " +
$"idx={idx} lastFilled={lastFilled} " +
$"openLayerY={openLayerY:F1} openIW={openIW} openIH={openIH} openID={openID}");
Debug.WriteLine($"[LayerPacker] Type0: nW={nW} nD={nD} nH={nH} perLayer={nW*nD} idx={idx} lastFilled={idx%(nW*nD)} openLayerY={openLayerY:F1} openIW={openIW} openIH={openIH} openID={openID}");
firstPlaced = true;
break;
}
if (!firstPlaced)
{
unpacked.AddRange(allItems);
return (packed, minBox, unpacked);
}
// ── Step 7: subsequent types — fill open layer then full layers on top ─
for (int ti = 1; ti < types.Count; ti++)
{
var (proto, typeItems) = types[ti];
var queue = new Queue
- (typeItems);
// ── 7a: fill the open (incomplete) last layer of the previous type ─
Debug.WriteLine(
$"[LP] Type{ti} '{proto.Name}': openLayerY={openLayerY:F1} " +
$"openFilled={openFilled} openCap={openCapacity} " +
$"openIW={openIW} openIH={openIH} openID={openID}");
if (!double.IsNaN(openLayerY) && queue.Count > 0)
{
int ixPartial = openFilled % openNW;
int izPartial = openFilled / openNW;
// Region A: X-remainder of the partially-filled Z-row.
// Items must fit within openID depth to avoid overlapping Region B.
if (ixPartial > 0)
{
double aXBase = edge + ixPartial * (openIW + itemPad);
double aZBase = edge + izPartial * (openID + itemPad);
FillRectangle(queue, packed,
aXBase, aZBase,
extW - ixPartial * (openIW + itemPad), openID,
openLayerY, openIH, itemPad, proto);
}
// Region B: completely free Z-rows (uses both orientations via FillRectangle).
int izFree = izPartial + (ixPartial > 0 ? 1 : 0);
double bZOff = izFree * (openID + itemPad);
double bRectD = extD - bZOff;
if (bRectD > EPS)
FillRectangle(queue, packed,
edge, edge + bZOff, extW, bRectD,
openLayerY, openIH, itemPad, proto);
openLayerY = double.NaN;
}
// ── 7b: add layers on top, filling each layer with both orientations ─
foreach (var (iW, iH, iD) in Orientations(proto))
{
if (queue.Count == 0) break;
// Step 8: check remaining height
double availH = maxBox.H - 2 * edge - extH;
int nHavail = (int)((availH + itemPad) / (iH + itemPad));
if (nHavail < 1) continue;
// Verify at least one item fits per layer in this orientation
if ((int)((extW + itemPad) / (iW + itemPad)) < 1) continue;
if ((int)((extD + itemPad) / (iD + itemPad)) < 1) continue;
while (queue.Count > 0 && nHavail > 0)
{
int before = queue.Count;
double y0 = edge + extH + itemPad;
// Fill the full extW×extD footprint using both orientations.
// FillRectangle recurses into remaining strips with rotated items.
FillRectangle(queue, packed, edge, edge, extW, extD,
y0, iH, itemPad, proto);
if (queue.Count == before) break; // items too large for remaining space
extH += iH + itemPad;
nHavail--;
}
openLayerY = double.NaN;
break;
}
// Leftover items → unpacked
unpacked.AddRange(queue);
}
// ── Step 9: tighten box ───────────────────────────────────────────────
var box = TightenBox(packed, edge, minBox, maxBox, wallThickness);
return (packed, box, unpacked);
}
// ── Helpers ───────────────────────────────────────────────────────────────
///
/// Fills a rectangular horizontal region (xBase,zBase)+(rectW×rectD) at height y
/// with items from queue, trying both W/D orientations. After placing the primary
/// grid the remaining two strips (Z-remainder and X-remainder) are filled recursively,
/// so rotated items can be used to fill gaps left by the primary orientation.
/// layerH caps the maximum item height that may be placed.
///
private static void FillRectangle(
Queue
- queue, List packed,
double xBase, double zBase, double rectW, double rectD,
double y, double layerH, double itemPad, Item proto)
{
if (queue.Count == 0 || rectW < EPS || rectD < EPS) return;
foreach (var (iW, iH, iD) in Orientations(proto))
{
if (iH > layerH + EPS) continue;
int nW = (int)((rectW + itemPad) / (iW + itemPad));
int nD = (int)((rectD + itemPad) / (iD + itemPad));
if (nW < 1 || nD < 1) continue;
for (int iz = 0; iz < nD && queue.Count > 0; iz++)
for (int ix = 0; ix < nW && queue.Count > 0; ix++)
packed.Add(new PackedItem(queue.Dequeue(),
xBase + ix * (iW + itemPad),
y,
zBase + iz * (iD + itemPad),
iW, iH, iD));
// Z-strip: depth remaining after primary grid rows
double zRemain = rectD - nD * (iD + itemPad);
if (zRemain > EPS && queue.Count > 0)
FillRectangle(queue, packed,
xBase, zBase + nD * (iD + itemPad), rectW, zRemain,
y, layerH, itemPad, proto);
// X-strip: width remaining alongside primary grid's Z range
double xRemain = rectW - nW * (iW + itemPad);
if (xRemain > EPS && queue.Count > 0)
FillRectangle(queue, packed,
xBase + nW * (iW + itemPad), zBase,
xRemain, nD * (iD + itemPad),
y, layerH, itemPad, proto);
break; // orientation chosen — done
}
}
///
/// Two allowed orientations: original and rotated 90° around 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);
}
///
/// 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)
{
if (packed.Count == 0) return minBox;
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);
}
}
}