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