Add project files.
This commit is contained in:
+314
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace BoxPacker3D
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class LayerPacker
|
||||
{
|
||||
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);
|
||||
|
||||
// ── 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<Item>(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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static void FillRectangle(
|
||||
Queue<Item> queue, List<PackedItem> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two allowed orientations: original and rotated 90° around 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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user