Rewrite LayerPacker as skyline packer, add Extreme Points alternative, manual footprint tuning and reordering UI
- Replace the old layer-grid LayerPacker with a skyline (lowest-slot-first) algorithm: grows the footprint only when items truly can't fit within maxBox, lets shorter types stack multiple sub-layers into taller leftover space, cascades unfinished layers to later types, and allows a staircase profile to minimize wasted volume. - Fix ItemDatabaseEditor NumericUpDown crash (Value set before Minimum/Maximum) and ItemDatabase.Upsert doing INSERT instead of UPDATE (was keyed by Name, now by Id). - Add Packer3D as a working Extreme Points algorithm (was dead/commented-out code) behind a shared IPacker interface, selectable in the UI alongside Skyline. - Add manual footprint (Skyline) / box-size (Extreme Points) adjustment controls for the first box, with correct min/max clamping against the configured max box size. - Add a "Poradie" column with up/down buttons to reorder packing items; both packers now honor that manual order instead of re-sorting by volume. - Drop multi-box packing (single box only), add a pre-pack volume warning, and make unpacked items exportable to PDF / inspectable via a result-label tooltip, both grouped by type with dimensions, count and total volume. - Merge the min/max box dimension groups, hide the now-unused box navigation group, and misc UI cleanup (typo fix, group renames, wider item name column). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,19 @@ namespace BoxPacker3D
|
||||
{
|
||||
public Box Box;
|
||||
public List<PackedItem> PackedItems;
|
||||
|
||||
// Inputs kept so the first box can later be re-packed with a manually adjusted
|
||||
// footprint (Skyline) or box size (Extreme Points) without re-running BtnPack_Click.
|
||||
public List<Item> SourceItems = new List<Item>();
|
||||
public Box MinBox = new Box(0, 0, 0);
|
||||
public Box MaxBox = new Box(0, 0, 0);
|
||||
public Box HardMaxBox = new Box(0, 0, 0); // configured max at pack time — never mutated, used as the ceiling for manual adjustment
|
||||
public double WallPad, ItemPad, WallThickness;
|
||||
public bool IsSkyline;
|
||||
public (double iW0, double iH0, double iD0) Orient0;
|
||||
public int NW, ND;
|
||||
public List<Item> Unpacked = new List<Item>();
|
||||
|
||||
public BoxSolution(Box box, List<PackedItem> packed)
|
||||
{ Box = box; PackedItems = packed; }
|
||||
}
|
||||
@@ -26,6 +39,7 @@ namespace BoxPacker3D
|
||||
private List<BoxSolution> _solutions = new List<BoxSolution>();
|
||||
private List<Item> _unpackedItems = new List<Item>(); // Track items that didn't fit
|
||||
private int _currentBoxIndex = 0;
|
||||
private ToolTip _unpackedTooltip = new ToolTip { AutoPopDelay = 30000, InitialDelay = 300, ReshowDelay = 100 };
|
||||
|
||||
private List<ItemType> itemTypes = new List<ItemType>();
|
||||
private Viewer3D viewer;
|
||||
@@ -40,6 +54,10 @@ namespace BoxPacker3D
|
||||
private ComboBox cmbItemName;
|
||||
private NumericUpDown nudNewW, nudNewH, nudNewD, nudNewCnt;
|
||||
private Button btnAddItem, btnRemoveItem, btnPack, btnSuggest, btnClear, btnSaveTemplate, btnEditDb, btnRefreshDb;
|
||||
private CheckBox chkSkyline, chkExtremePoints;
|
||||
private Label lblFootprintW, lblFootprintD;
|
||||
private NumericUpDown nudFootprintW, nudFootprintD;
|
||||
private bool _suppressFootprintEvents = false;
|
||||
private Button btnExportJpg, btnExportPng, btnExportPdf;
|
||||
private Button btnPrevBox, btnNextBox;
|
||||
private Label lblEfficiency, lblPacked, lblTotal, lblBoxNav;
|
||||
@@ -149,30 +167,6 @@ namespace BoxPacker3D
|
||||
tbl.Controls.Add(ctrl, 0, row);
|
||||
}
|
||||
|
||||
private TableLayoutPanel Make3ColNudRow(
|
||||
string lbl1, NumericUpDown nud1,
|
||||
string lbl2, NumericUpDown nud2,
|
||||
string lbl3, NumericUpDown nud3)
|
||||
{
|
||||
var tbl = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill, AutoSize = true,
|
||||
ColumnCount = 3, RowCount = 2, Margin = new Padding(2)
|
||||
};
|
||||
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
|
||||
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
|
||||
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 34f));
|
||||
tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
tbl.Controls.Add(MakeLabel(lbl1), 0, 0);
|
||||
tbl.Controls.Add(MakeLabel(lbl2), 1, 0);
|
||||
tbl.Controls.Add(MakeLabel(lbl3), 2, 0);
|
||||
tbl.Controls.Add(nud1, 0, 1);
|
||||
tbl.Controls.Add(nud2, 1, 1);
|
||||
tbl.Controls.Add(nud3, 2, 1);
|
||||
return tbl;
|
||||
}
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
this.Text = "3D Box Packer";
|
||||
@@ -235,56 +229,58 @@ namespace BoxPacker3D
|
||||
panelLeft.Controls.Add(stack);
|
||||
int r = 0;
|
||||
|
||||
// ── Min box dimensions (mm) ───────────────────────────────────────
|
||||
grpBox = MakeGroupBox("📦 Min rozmery škatule (mm)");
|
||||
// ── Min & Max box dimensions (mm) ─────────────────────────────────
|
||||
grpBox = MakeGroupBox("📦 Min a Max rozmery škatule (mm)");
|
||||
nudBoxW = MakeNUD((decimal)_settings.DefaultBox.Width, 10);
|
||||
nudBoxH = MakeNUD((decimal)_settings.DefaultBox.Height, 10);
|
||||
nudBoxD = MakeNUD((decimal)_settings.DefaultBox.Depth, 10);
|
||||
nudBoxWallThickness = MakeNUD((decimal)_settings.DefaultBox.WallThickness, 0, 500, 1, 0.5m);
|
||||
nudMaxW = MakeNUD((decimal)_settings.MaxBox.Width, 10);
|
||||
nudMaxH = MakeNUD((decimal)_settings.MaxBox.Height, 10);
|
||||
nudMaxD = MakeNUD((decimal)_settings.MaxBox.Depth, 10);
|
||||
|
||||
// 3-column label+NUD row (wall thickness is in Izolácia group)
|
||||
// Validate box dimensions when max changes
|
||||
nudMaxW.ValueChanged += (s, e) => {
|
||||
nudBoxW.Maximum = nudMaxW.Value;
|
||||
if (nudBoxW.Value > nudMaxW.Value) nudBoxW.Value = nudMaxW.Value;
|
||||
};
|
||||
nudMaxH.ValueChanged += (s, e) => {
|
||||
nudBoxH.Maximum = nudMaxH.Value;
|
||||
if (nudBoxH.Value > nudMaxH.Value) nudBoxH.Value = nudMaxH.Value;
|
||||
};
|
||||
nudMaxD.ValueChanged += (s, e) => {
|
||||
nudBoxD.Maximum = nudMaxD.Value;
|
||||
if (nudBoxD.Value > nudMaxD.Value) nudBoxD.Value = nudMaxD.Value;
|
||||
};
|
||||
|
||||
// 4-column table: label column + Šírka/Dĺžka/Výška, one row for Min, one for Max
|
||||
var boxTbl = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill, AutoSize = true,
|
||||
ColumnCount = 3, RowCount = 2, Margin = new Padding(2)
|
||||
ColumnCount = 4, RowCount = 3, Margin = new Padding(2)
|
||||
};
|
||||
boxTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 40f));
|
||||
boxTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
|
||||
boxTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
|
||||
boxTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 34f));
|
||||
boxTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
boxTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
boxTbl.Controls.Add(MakeLabel("Min šírka (mm):"), 0, 0);
|
||||
boxTbl.Controls.Add(MakeLabel("Min dĺžka (mm):"), 1, 0);
|
||||
boxTbl.Controls.Add(MakeLabel("Min výška (mm):"), 2, 0);
|
||||
boxTbl.Controls.Add(nudBoxW, 0, 1);
|
||||
boxTbl.Controls.Add(nudBoxD, 1, 1);
|
||||
boxTbl.Controls.Add(nudBoxH, 2, 1);
|
||||
boxTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
boxTbl.Controls.Add(MakeLabel(""), 0, 0);
|
||||
boxTbl.Controls.Add(MakeLabel("Šírka (mm):"), 1, 0);
|
||||
boxTbl.Controls.Add(MakeLabel("Dĺžka (mm):"), 2, 0);
|
||||
boxTbl.Controls.Add(MakeLabel("Výška (mm):"), 3, 0);
|
||||
boxTbl.Controls.Add(MakeLabel("Min:"), 0, 1);
|
||||
boxTbl.Controls.Add(nudBoxW, 1, 1);
|
||||
boxTbl.Controls.Add(nudBoxD, 2, 1);
|
||||
boxTbl.Controls.Add(nudBoxH, 3, 1);
|
||||
boxTbl.Controls.Add(MakeLabel("Max:"), 0, 2);
|
||||
boxTbl.Controls.Add(nudMaxW, 1, 2);
|
||||
boxTbl.Controls.Add(nudMaxD, 2, 2);
|
||||
boxTbl.Controls.Add(nudMaxH, 3, 2);
|
||||
grpBox.Controls.Add(boxTbl);
|
||||
PushRow(stack, grpBox, r++);
|
||||
|
||||
// ── Max dimensions ────────────────────────────────────────────────
|
||||
var grpMax = MakeGroupBox("📏 Max rozmery škatule (mm)");
|
||||
nudMaxW = MakeNUD((decimal)_settings.MaxBox.Width, 10);
|
||||
nudMaxH = MakeNUD((decimal)_settings.MaxBox.Height, 10);
|
||||
nudMaxD = MakeNUD((decimal)_settings.MaxBox.Depth, 10);
|
||||
|
||||
// Validate box dimensions when max changes
|
||||
nudMaxW.ValueChanged += (s, e) => {
|
||||
nudBoxW.Maximum = nudMaxW.Value;
|
||||
if (nudBoxW.Value > nudMaxW.Value) nudBoxW.Value = nudMaxW.Value;
|
||||
};
|
||||
nudMaxH.ValueChanged += (s, e) => {
|
||||
nudBoxH.Maximum = nudMaxH.Value;
|
||||
if (nudBoxH.Value > nudMaxH.Value) nudBoxH.Value = nudMaxH.Value;
|
||||
};
|
||||
nudMaxD.ValueChanged += (s, e) => {
|
||||
nudBoxD.Maximum = nudMaxD.Value;
|
||||
if (nudBoxD.Value > nudMaxD.Value) nudBoxD.Value = nudMaxD.Value;
|
||||
};
|
||||
|
||||
grpMax.Controls.Add(Make3ColNudRow("Max šírka:", nudMaxW, "Max dĺžka:", nudMaxD, "Max výška:", nudMaxH));
|
||||
PushRow(stack, grpMax, r++);
|
||||
|
||||
// ── Padding + wall thickness (mm) ────────────────────────────────
|
||||
var grpPad = MakeGroupBox("🧱 Izolácia (mm)");
|
||||
nudWallPad = MakeNUD((decimal)_settings.Padding.WallPadding, 0, 1000, 1, 5m);
|
||||
@@ -300,7 +296,7 @@ namespace BoxPacker3D
|
||||
padTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
padTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
padTbl.Controls.Add(MakeLabel("Stenová izolácia:"), 0, 0);
|
||||
padTbl.Controls.Add(MakeLabel("Medikusová:"), 1, 0);
|
||||
padTbl.Controls.Add(MakeLabel("Medzikusová:"), 1, 0);
|
||||
padTbl.Controls.Add(MakeLabel("Hrúbka steny:"), 2, 0);
|
||||
padTbl.Controls.Add(nudWallPad, 0, 1);
|
||||
padTbl.Controls.Add(nudItemPad, 1, 1);
|
||||
@@ -319,7 +315,8 @@ namespace BoxPacker3D
|
||||
|
||||
dgvItems = new DataGridView
|
||||
{
|
||||
Dock = DockStyle.Fill, Height = 160,
|
||||
// Height grown by ~the space the (now hidden) box-navigation group used to take.
|
||||
Dock = DockStyle.Fill, Height = 230,
|
||||
BackgroundColor = C(40, 40, 55), GridColor = C(60, 60, 80),
|
||||
BorderStyle = BorderStyle.None, RowHeadersVisible = false,
|
||||
AllowUserToAddRows = false,
|
||||
@@ -339,8 +336,32 @@ namespace BoxPacker3D
|
||||
dgvItems.Columns.Add("H", "V(mm)");
|
||||
dgvItems.Columns.Add("D", "D(mm)");
|
||||
dgvItems.Columns.Add("Count", "Počet");
|
||||
dgvItems.Columns["Name"].FillWeight = 150f; // 1.5× the default 100 fill weight
|
||||
dgvItems.Columns["W"].FillWeight = 85f;
|
||||
dgvItems.Columns["H"].FillWeight = 85f;
|
||||
dgvItems.Columns["D"].FillWeight = 85f;
|
||||
dgvItems.Columns["Count"].FillWeight = 85f;
|
||||
|
||||
// "Poradie" — a fixed-width column of up/down move buttons, hand-painted (no
|
||||
// bound value); manual re-ordering of packing priority. Volume-based ordering
|
||||
// for the packer itself is intentionally left untouched for now.
|
||||
var orderCol = new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "Order",
|
||||
HeaderText = "Poradie",
|
||||
ReadOnly = true,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable,
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
|
||||
Width = 50,
|
||||
Resizable = DataGridViewTriState.False
|
||||
};
|
||||
dgvItems.Columns.Add(orderCol);
|
||||
|
||||
dgvItems.CellPainting += DgvItems_OrderColumnPainting;
|
||||
dgvItems.CellMouseClick += DgvItems_OrderColumnClick;
|
||||
|
||||
int ir = 0;
|
||||
PushRow(itemsStack, dgvItems, ir++, SizeType.Absolute, 165);
|
||||
PushRow(itemsStack, dgvItems, ir++, SizeType.Absolute, 235);
|
||||
|
||||
// Input row 1: Názov (ComboBox s autocomplete) + Pridať + Zmazať
|
||||
var inputRow1 = new TableLayoutPanel
|
||||
@@ -446,56 +467,10 @@ namespace BoxPacker3D
|
||||
btnAddItem.Click += (s, e) =>
|
||||
{
|
||||
var nm = cmbItemName.Text.Trim();
|
||||
bool isCustomName = !string.IsNullOrEmpty(nm);
|
||||
if (!isCustomName) nm = $"Kus {dgvItems.Rows.Count + 1}";
|
||||
|
||||
if (string.IsNullOrEmpty(nm)) nm = $"Kus {dgvItems.Rows.Count + 1}";
|
||||
|
||||
dgvItems.Rows.Add(nm, (int)nudNewW.Value, (int)nudNewH.Value, (int)nudNewD.Value, (int)nudNewCnt.Value);
|
||||
|
||||
// Auto-save to DB if user provided a custom name (not auto-generated)
|
||||
if (isCustomName && _itemDb != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newTemplate = new ItemTemplate
|
||||
{
|
||||
Name = nm,
|
||||
Width = (double)nudNewW.Value,
|
||||
Height = (double)nudNewH.Value,
|
||||
Depth = (double)nudNewD.Value
|
||||
};
|
||||
|
||||
// Check for duplicates
|
||||
var duplicate = _itemDb.FindDuplicate(newTemplate);
|
||||
if (duplicate != null)
|
||||
{
|
||||
string msg;
|
||||
if (duplicate.Name.Equals(nm, StringComparison.OrdinalIgnoreCase))
|
||||
msg = $"Kus s názvom \"{duplicate.Name}\" už existuje v databáze.\n\nRozmery: {duplicate.Width}×{duplicate.Height}×{duplicate.Depth} mm";
|
||||
else
|
||||
msg = $"Kus s rovnakými rozmermi ({duplicate.Width}×{duplicate.Height}×{duplicate.Depth} mm) už existuje v databáze pod názvom \"{duplicate.Name}\".";
|
||||
|
||||
MessageBox.Show($"{msg}\n\nKus bol pridaný do zoznamu, ale neuložený do databázy.",
|
||||
"Duplikát", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
statusLabel.Text = $"Pridaný do zoznamu: {nm} (duplikát v DB)";
|
||||
}
|
||||
else
|
||||
{
|
||||
_itemDb.Upsert(newTemplate);
|
||||
RefreshItemNameCombo();
|
||||
statusLabel.Text = $"Pridaný do zoznamu a uložený do DB: {nm}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Kus pridaný do zoznamu, ale nepodarilo sa uložiť do DB:\n{ex.Message}",
|
||||
"Upozornenie", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
statusLabel.Text = $"Pridaný do zoznamu: {nm} (chyba DB)";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
statusLabel.Text = $"Pridaný do zoznamu: {nm}";
|
||||
}
|
||||
statusLabel.Text = $"Pridaný do zoznamu: {nm}";
|
||||
};
|
||||
btnRemoveItem.Click += (s, e) =>
|
||||
{
|
||||
@@ -589,6 +564,70 @@ namespace BoxPacker3D
|
||||
btnPack.Click += BtnPack_Click;
|
||||
PushRow(stack, btnPack, r++);
|
||||
|
||||
// ── Pack type ─────────────────────────────────────────────────────
|
||||
var grpPackType = MakeGroupBox("🧭 Typ balenia a prispôsobenie");
|
||||
var packTypeTbl = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill, AutoSize = true,
|
||||
ColumnCount = 2, Margin = new Padding(2)
|
||||
};
|
||||
packTypeTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50f));
|
||||
packTypeTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50f));
|
||||
|
||||
chkSkyline = new CheckBox
|
||||
{
|
||||
Text = "Skyline", Checked = true,
|
||||
ForeColor = C(180, 180, 200), AutoSize = true,
|
||||
Margin = new Padding(3, 6, 3, 6)
|
||||
};
|
||||
chkExtremePoints = new CheckBox
|
||||
{
|
||||
Text = "Extreme points", Checked = false,
|
||||
ForeColor = C(180, 180, 200), AutoSize = true,
|
||||
Margin = new Padding(3, 6, 3, 6)
|
||||
};
|
||||
bool suppressPackTypeEvents = false;
|
||||
chkSkyline.CheckedChanged += (s, e) =>
|
||||
{
|
||||
if (suppressPackTypeEvents) return;
|
||||
suppressPackTypeEvents = true;
|
||||
if (chkSkyline.Checked) chkExtremePoints.Checked = false;
|
||||
else if (!chkExtremePoints.Checked) chkSkyline.Checked = true; // keep exactly one checked
|
||||
suppressPackTypeEvents = false;
|
||||
RefreshFootprintControls();
|
||||
};
|
||||
chkExtremePoints.CheckedChanged += (s, e) =>
|
||||
{
|
||||
if (suppressPackTypeEvents) return;
|
||||
suppressPackTypeEvents = true;
|
||||
if (chkExtremePoints.Checked) chkSkyline.Checked = false;
|
||||
else if (!chkSkyline.Checked) chkExtremePoints.Checked = true; // keep exactly one checked
|
||||
suppressPackTypeEvents = false;
|
||||
RefreshFootprintControls();
|
||||
};
|
||||
|
||||
packTypeTbl.Controls.Add(chkSkyline, 0, 0);
|
||||
packTypeTbl.Controls.Add(chkExtremePoints, 1, 0);
|
||||
|
||||
// ── Manual footprint / box-size adjustment (first box only) ────────
|
||||
lblFootprintW = MakeLabel("-");
|
||||
lblFootprintD = MakeLabel("-");
|
||||
nudFootprintW = MakeNUD(1, 1, 100000, 0, 1);
|
||||
nudFootprintD = MakeNUD(1, 1, 100000, 0, 1);
|
||||
nudFootprintW.Enabled = false;
|
||||
nudFootprintD.Enabled = false;
|
||||
|
||||
nudFootprintW.ValueChanged += (s, e) => AdjustFirstBoxFootprint(isWidth: true);
|
||||
nudFootprintD.ValueChanged += (s, e) => AdjustFirstBoxFootprint(isWidth: false);
|
||||
|
||||
packTypeTbl.Controls.Add(lblFootprintW, 0, 1);
|
||||
packTypeTbl.Controls.Add(lblFootprintD, 1, 1);
|
||||
packTypeTbl.Controls.Add(nudFootprintW, 0, 2);
|
||||
packTypeTbl.Controls.Add(nudFootprintD, 1, 2);
|
||||
|
||||
grpPackType.Controls.Add(packTypeTbl);
|
||||
PushRow(stack, grpPackType, r++);
|
||||
|
||||
// ── Results ───────────────────────────────────────────────────────
|
||||
grpResult = MakeGroupBox("📊 Výsledky");
|
||||
var resTbl = new TableLayoutPanel
|
||||
@@ -607,8 +646,9 @@ namespace BoxPacker3D
|
||||
grpResult.Controls.Add(resTbl);
|
||||
PushRow(stack, grpResult, r++);
|
||||
|
||||
// ── Box navigation ────────────────────────────────────────────────
|
||||
// ── Box navigation (hidden — multi-box packing removed; only one box exists) ──
|
||||
var grpNav = MakeGroupBox("📦 Navigácia škatúľ");
|
||||
grpNav.Visible = false;
|
||||
var navTbl = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill, AutoSize = true,
|
||||
@@ -807,10 +847,244 @@ namespace BoxPacker3D
|
||||
lblEfficiency.Text = $"Využitie: {effOuter:F1}% (celok) | {effInner:F1}% (bez stien) " +
|
||||
$"({sol.Box.W:F0}×{sol.Box.D:F0}×{sol.Box.H:F0} mm)";
|
||||
Rerender();
|
||||
RefreshFootprintControls();
|
||||
}
|
||||
|
||||
private void UpdateResultLabels()
|
||||
{
|
||||
int totalPacked = _solutions.Sum(s => s.PackedItems.Count);
|
||||
int totalItems = totalPacked + _unpackedItems.Count;
|
||||
lblPacked.Text = $"Zabalené: {totalPacked} / {totalItems} kusov";
|
||||
lblTotal.Text = $"Počet škatúľ: {_solutions.Count}";
|
||||
|
||||
if (_unpackedItems.Count > 0)
|
||||
{
|
||||
lblPacked.Text += $" | Nezabalené: {_unpackedItems.Count}";
|
||||
lblPacked.ForeColor = Color.FromArgb(211, 47, 47);
|
||||
|
||||
var grouped = _unpackedItems
|
||||
.GroupBy(it => it.Name)
|
||||
.Select(g => new {
|
||||
Name = g.Key,
|
||||
W = g.First().W, H = g.First().H, D = g.First().D,
|
||||
Count = g.Count(),
|
||||
TotalVolume = g.Sum(it => it.Volume)
|
||||
})
|
||||
.OrderBy(x => x.Name);
|
||||
|
||||
var sb = new System.Text.StringBuilder("Nezabalené kusy:\n");
|
||||
foreach (var g in grouped)
|
||||
sb.AppendLine($"{g.Name} ({g.W:F0}×{g.H:F0}×{g.D:F0} mm) — {g.Count} ks, " +
|
||||
$"objem typu: {g.TotalVolume / 1000.0:F0} cm³");
|
||||
_unpackedTooltip.SetToolTip(lblPacked, sb.ToString().TrimEnd());
|
||||
}
|
||||
else
|
||||
{
|
||||
lblPacked.ForeColor = Color.FromArgb(56, 142, 60);
|
||||
_unpackedTooltip.SetToolTip(lblPacked, "");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the manual footprint (Skyline) / box-size (Extreme Points) adjustment
|
||||
/// controls for the FIRST box only — always based on _solutions[0], but only
|
||||
/// editable while that box is the one currently displayed.
|
||||
/// </summary>
|
||||
private void RefreshFootprintControls()
|
||||
{
|
||||
if (_solutions.Count == 0)
|
||||
{
|
||||
lblFootprintW.Text = "-"; lblFootprintD.Text = "-";
|
||||
nudFootprintW.Enabled = nudFootprintD.Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var sol = _solutions[0];
|
||||
bool enabled = _currentBoxIndex == 0;
|
||||
|
||||
double edge = sol.WallPad + sol.WallThickness;
|
||||
|
||||
_suppressFootprintEvents = true;
|
||||
try
|
||||
{
|
||||
if (sol.IsSkyline)
|
||||
{
|
||||
double stepW = sol.Orient0.iW0 + sol.ItemPad;
|
||||
double stepD = sol.Orient0.iD0 + sol.ItemPad;
|
||||
double extW = sol.NW * stepW - sol.ItemPad;
|
||||
double extD = sol.ND * stepD - sol.ItemPad;
|
||||
|
||||
// Footprint is the INNER usable area — only values on the nW/nD grid
|
||||
// (extW = nW*stepW - itemPad) are ever actually achievable, so Min/Max
|
||||
// must land exactly on that grid too, or the spinner can stop one notch
|
||||
// past what PackWithFixedFootprint would actually clamp down to.
|
||||
double maxIW = Math.Max(0, sol.HardMaxBox.W - 2 * edge);
|
||||
double maxID = Math.Max(0, sol.HardMaxBox.D - 2 * edge);
|
||||
int maxNW = Math.Max(1, (int)((maxIW + sol.ItemPad) / stepW));
|
||||
int maxND = Math.Max(1, (int)((maxID + sol.ItemPad) / stepD));
|
||||
double minExtW = stepW - sol.ItemPad; // nW = 1
|
||||
double minExtD = stepD - sol.ItemPad; // nD = 1
|
||||
double maxExtW = maxNW * stepW - sol.ItemPad;
|
||||
double maxExtD = maxND * stepD - sol.ItemPad;
|
||||
|
||||
lblFootprintW.Text = $"Krok šírky: {sol.Orient0.iW0:F0} mm";
|
||||
lblFootprintD.Text = $"Krok dĺžky: {sol.Orient0.iD0:F0} mm";
|
||||
|
||||
nudFootprintW.Increment = (decimal)stepW;
|
||||
nudFootprintD.Increment = (decimal)stepD;
|
||||
nudFootprintW.Minimum = (decimal)minExtW;
|
||||
nudFootprintD.Minimum = (decimal)minExtD;
|
||||
nudFootprintW.Maximum = (decimal)maxExtW;
|
||||
nudFootprintD.Maximum = (decimal)maxExtD;
|
||||
nudFootprintW.Value = Math.Clamp((decimal)extW, nudFootprintW.Minimum, nudFootprintW.Maximum);
|
||||
nudFootprintD.Value = Math.Clamp((decimal)extD, nudFootprintD.Minimum, nudFootprintD.Maximum);
|
||||
}
|
||||
else
|
||||
{
|
||||
// "Type 0" for Extreme Points too — whichever item comes first (manual
|
||||
// priority / table order), same convention LayerPacker now uses.
|
||||
var type0 = sol.SourceItems.FirstOrDefault();
|
||||
double stepW = (type0?.W ?? 1) + sol.ItemPad;
|
||||
double stepD = (type0?.D ?? 1) + sol.ItemPad;
|
||||
|
||||
// Box size is the OUTER dimension — capped directly at the configured max.
|
||||
double minW = Math.Max(1, sol.MinBox.W);
|
||||
double minD = Math.Max(1, sol.MinBox.D);
|
||||
double maxW = Math.Max(minW, sol.HardMaxBox.W);
|
||||
double maxD = Math.Max(minD, sol.HardMaxBox.D);
|
||||
|
||||
lblFootprintW.Text = "Šírka škatule:";
|
||||
lblFootprintD.Text = "Dĺžka škatule:";
|
||||
|
||||
nudFootprintW.Increment = (decimal)stepW;
|
||||
nudFootprintD.Increment = (decimal)stepD;
|
||||
nudFootprintW.Minimum = (decimal)minW;
|
||||
nudFootprintD.Minimum = (decimal)minD;
|
||||
nudFootprintW.Maximum = (decimal)maxW;
|
||||
nudFootprintD.Maximum = (decimal)maxD;
|
||||
nudFootprintW.Value = Math.Clamp((decimal)sol.Box.W, nudFootprintW.Minimum, nudFootprintW.Maximum);
|
||||
nudFootprintD.Value = Math.Clamp((decimal)sol.Box.D, nudFootprintD.Minimum, nudFootprintD.Maximum);
|
||||
}
|
||||
}
|
||||
finally { _suppressFootprintEvents = false; }
|
||||
|
||||
lblFootprintW.Enabled = lblFootprintD.Enabled = enabled;
|
||||
nudFootprintW.Enabled = nudFootprintD.Enabled = enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-packs ONLY the first box (_solutions[0]) after the user nudges the footprint
|
||||
/// (Skyline) or box-size (Extreme Points) numeric — camera rotation/zoom trackbars are
|
||||
/// left untouched, so Rerender() (called via ShowCurrentBox) keeps the current view.
|
||||
/// </summary>
|
||||
private void AdjustFirstBoxFootprint(bool isWidth)
|
||||
{
|
||||
if (_suppressFootprintEvents || _solutions.Count == 0 || _currentBoxIndex != 0) return;
|
||||
|
||||
var sol = _solutions[0];
|
||||
List<PackedItem> packed;
|
||||
Box box;
|
||||
List<Item> unpacked;
|
||||
|
||||
if (sol.IsSkyline)
|
||||
{
|
||||
double stepW = sol.Orient0.iW0 + sol.ItemPad;
|
||||
double stepD = sol.Orient0.iD0 + sol.ItemPad;
|
||||
double curExtW = sol.NW * stepW - sol.ItemPad;
|
||||
double curExtD = sol.ND * stepD - sol.ItemPad;
|
||||
|
||||
int nW = sol.NW, nD = sol.ND;
|
||||
if (isWidth)
|
||||
nW = Math.Max(1, sol.NW + (int)Math.Round(((double)nudFootprintW.Value - curExtW) / stepW));
|
||||
else
|
||||
nD = Math.Max(1, sol.ND + (int)Math.Round(((double)nudFootprintD.Value - curExtD) / stepD));
|
||||
|
||||
(packed, box, unpacked) = new LayerPacker().PackWithFixedFootprint(
|
||||
sol.SourceItems, sol.MinBox, sol.MaxBox, sol.WallPad, sol.ItemPad, sol.WallThickness,
|
||||
sol.Orient0, nW, nD);
|
||||
sol.NW = nW; sol.ND = nD;
|
||||
}
|
||||
else
|
||||
{
|
||||
double newW = isWidth ? (double)nudFootprintW.Value : sol.MaxBox.W;
|
||||
double newD = isWidth ? sol.MaxBox.D : (double)nudFootprintD.Value;
|
||||
sol.MaxBox = new Box(newW, sol.MaxBox.H, newD, sol.WallThickness);
|
||||
|
||||
(packed, box, unpacked) = new Packer3D().Pack(
|
||||
sol.SourceItems, sol.MinBox, sol.MaxBox, sol.WallPad, sol.ItemPad, sol.WallThickness);
|
||||
}
|
||||
|
||||
sol.Box = box;
|
||||
sol.PackedItems = packed;
|
||||
sol.Unpacked = unpacked;
|
||||
|
||||
_unpackedItems = _solutions.SelectMany(s => s.Unpacked).ToList();
|
||||
UpdateResultLabels();
|
||||
ShowCurrentBox();
|
||||
}
|
||||
|
||||
// ── "Poradie" (order) column: hand-painted up/down move buttons ────────
|
||||
|
||||
private static readonly string[] _itemDataColumns = { "Name", "W", "H", "D", "Count" };
|
||||
|
||||
private void SwapItemRows(int a, int b)
|
||||
{
|
||||
foreach (var colName in _itemDataColumns)
|
||||
{
|
||||
var tmp = dgvItems.Rows[a].Cells[colName].Value;
|
||||
dgvItems.Rows[a].Cells[colName].Value = dgvItems.Rows[b].Cells[colName].Value;
|
||||
dgvItems.Rows[b].Cells[colName].Value = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
private void DgvItems_OrderColumnPainting(object sender, DataGridViewCellPaintingEventArgs e)
|
||||
{
|
||||
if (e.RowIndex < 0 || dgvItems.Columns[e.ColumnIndex].Name != "Order") return;
|
||||
|
||||
e.PaintBackground(e.CellBounds, true);
|
||||
|
||||
bool canUp = e.RowIndex > 0;
|
||||
bool canDown = e.RowIndex < dgvItems.Rows.Count - 1;
|
||||
|
||||
var upRect = new Rectangle(e.CellBounds.X, e.CellBounds.Y, e.CellBounds.Width, e.CellBounds.Height / 2);
|
||||
var downRect = new Rectangle(e.CellBounds.X, e.CellBounds.Y + e.CellBounds.Height / 2,
|
||||
e.CellBounds.Width, e.CellBounds.Height - e.CellBounds.Height / 2);
|
||||
|
||||
DrawOrderArrow(e.Graphics, upRect, canUp ? C(200, 200, 220) : C(70, 70, 85), up: true);
|
||||
DrawOrderArrow(e.Graphics, downRect, canDown ? C(200, 200, 220) : C(70, 70, 85), up: false);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void DrawOrderArrow(Graphics g, Rectangle bounds, Color color, bool up)
|
||||
{
|
||||
int cx = bounds.X + bounds.Width / 2;
|
||||
int cy = bounds.Y + bounds.Height / 2;
|
||||
int size = Math.Max(3, Math.Min(bounds.Width, bounds.Height) / 3);
|
||||
Point[] pts = up
|
||||
? new[] { new Point(cx - size, cy + size / 2), new Point(cx + size, cy + size / 2), new Point(cx, cy - size / 2) }
|
||||
: new[] { new Point(cx - size, cy - size / 2), new Point(cx + size, cy - size / 2), new Point(cx, cy + size / 2) };
|
||||
using var brush = new SolidBrush(color);
|
||||
g.FillPolygon(brush, pts);
|
||||
}
|
||||
|
||||
private void DgvItems_OrderColumnClick(object sender, DataGridViewCellMouseEventArgs e)
|
||||
{
|
||||
if (e.RowIndex < 0 || dgvItems.Columns[e.ColumnIndex].Name != "Order") return;
|
||||
|
||||
int rowHeight = dgvItems.Rows[e.RowIndex].Height;
|
||||
bool clickedUp = e.Y < rowHeight / 2;
|
||||
|
||||
if (clickedUp && e.RowIndex > 0)
|
||||
SwapItemRows(e.RowIndex, e.RowIndex - 1);
|
||||
else if (!clickedUp && e.RowIndex < dgvItems.Rows.Count - 1)
|
||||
SwapItemRows(e.RowIndex, e.RowIndex + 1);
|
||||
}
|
||||
|
||||
// ── packing logic ─────────────────────────────────────────────────────
|
||||
|
||||
private IPacker CreatePacker() => chkExtremePoints.Checked ? new Packer3D() : new LayerPacker();
|
||||
|
||||
private void BtnPack_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (dgvItems.Rows.Count == 0)
|
||||
@@ -878,107 +1152,75 @@ namespace BoxPacker3D
|
||||
for (int j = 0; j < cnt; j++) allItems.Add(new Item(name, w, h, d, colors[i]));
|
||||
}
|
||||
|
||||
// ── Calculate number of boxes ─────────────────────────────────────
|
||||
// ── Volume check — heads-up only, packing still proceeds ──────────
|
||||
double totalItemVol = allItems.Sum(i => i.Volume);
|
||||
double maxInnerVol = maxUsableW * maxUsableH * maxUsableD;
|
||||
int boxCount = Math.Max(1, (int)Math.Ceiling(totalItemVol / maxInnerVol));
|
||||
|
||||
string confirmMsg =
|
||||
$"Celkový objem kusov: {totalItemVol / 1000.0:F0} cm³\n" +
|
||||
$"Max. objem škatule (bez iz.): {maxInnerVol / 1000.0:F0} cm³\n\n" +
|
||||
$"Odhadovaný počet škatúľ: {boxCount}\n" +
|
||||
$"(každý typ kusu zostane celý v jednej škatuli)\n\n" +
|
||||
$"Pokračovať s balením do {boxCount} škatúľ?";
|
||||
|
||||
if (MessageBox.Show(confirmMsg, "Potvrdenie balenia",
|
||||
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
// ── Partition types into bins (LPT) ───────────────────────────────
|
||||
var bins = PartitionTypesToBins(allItems, boxCount)
|
||||
.Where(b => b.Count > 0).ToList();
|
||||
if (totalItemVol > maxInnerVol)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Celkový objem kusov ({totalItemVol / 1000.0:F0} cm³) prekračuje objem maximálnej " +
|
||||
$"škatule ({maxInnerVol / 1000.0:F0} cm³).\n\n" +
|
||||
$"Kusy, ktoré sa nezmestia, skončia v zozname nezabalených — ten je po zabalení možné " +
|
||||
$"vyexportovať do PDF (tlačidlo PDF v Exporte).",
|
||||
"Kusy sa pravdepodobne nezmestia", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
_solutions.Clear();
|
||||
_unpackedItems.Clear();
|
||||
_currentBoxIndex = 0;
|
||||
progressBar.Visible = true;
|
||||
progressBar.Value = 0;
|
||||
statusLabel.Text = "Balím škatuľu...";
|
||||
Application.DoEvents();
|
||||
|
||||
for (int binIdx = 0; binIdx < bins.Count; binIdx++)
|
||||
double minW = (double)nudBoxW.Value;
|
||||
double minH = (double)nudBoxH.Value;
|
||||
double minD = (double)nudBoxD.Value;
|
||||
var minBox = new Box(minW, minH, minD, wallThickness);
|
||||
var maxBox2 = new Box(maxW, maxH, maxD, wallThickness);
|
||||
|
||||
BoxSolution sol;
|
||||
List<Item> unpacked;
|
||||
if (chkSkyline.Checked)
|
||||
{
|
||||
var binItems = bins[binIdx];
|
||||
int boxNum = binIdx + 1;
|
||||
|
||||
statusLabel.Text = $"Hľadám optimálnu škatuľu {boxNum}/{bins.Count}...";
|
||||
Application.DoEvents();
|
||||
|
||||
double minW = (double)nudBoxW.Value;
|
||||
double minH = (double)nudBoxH.Value;
|
||||
double minD = (double)nudBoxD.Value;
|
||||
|
||||
statusLabel.Text = $"Balím škatuľu {boxNum}/{bins.Count}...";
|
||||
Application.DoEvents();
|
||||
|
||||
var minBox = new Box(minW, minH, minD, wallThickness);
|
||||
var maxBox2 = new Box(maxW, maxH, maxD, wallThickness);
|
||||
var (packed, packBox, binUnpacked) = new LayerPacker().Pack(
|
||||
binItems, minBox, maxBox2, wallPad, itemPad, wallThickness);
|
||||
|
||||
_solutions.Add(new BoxSolution(packBox, packed));
|
||||
_unpackedItems.AddRange(binUnpacked);
|
||||
}
|
||||
|
||||
// ── Update result labels ──────────────────────────────────────────
|
||||
int totalPacked = _solutions.Sum(s => s.PackedItems.Count);
|
||||
lblPacked.Text = $"Zabalené: {totalPacked} / {allItems.Count} kusov";
|
||||
lblTotal.Text = $"Počet škatúľ: {_solutions.Count}";
|
||||
|
||||
if (_unpackedItems.Count > 0)
|
||||
{
|
||||
lblPacked.Text += $" | Nezabalené: {_unpackedItems.Count}";
|
||||
lblPacked.ForeColor = Color.FromArgb(211, 47, 47);
|
||||
MessageBox.Show(
|
||||
$"{_unpackedItems.Count} kusov sa nepodarilo zabaliť.\n" +
|
||||
$"Skúste zväčšiť maximálne rozmery škatule alebo znížiť izoláciu.",
|
||||
"Nezabalené kusy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
var (packed, packBox, unp, orient0, nW, nD) = new LayerPacker().PackWithInfo(
|
||||
allItems, minBox, maxBox2, wallPad, itemPad, wallThickness);
|
||||
sol = new BoxSolution(packBox, packed) { IsSkyline = true, Orient0 = orient0, NW = nW, ND = nD };
|
||||
unpacked = unp;
|
||||
}
|
||||
else
|
||||
{
|
||||
lblPacked.ForeColor = Color.FromArgb(56, 142, 60);
|
||||
var (packed, packBox, unp) = new Packer3D().Pack(
|
||||
allItems, minBox, maxBox2, wallPad, itemPad, wallThickness);
|
||||
sol = new BoxSolution(packBox, packed) { IsSkyline = false };
|
||||
unpacked = unp;
|
||||
}
|
||||
sol.SourceItems = allItems;
|
||||
sol.MinBox = minBox;
|
||||
sol.MaxBox = maxBox2;
|
||||
sol.HardMaxBox = maxBox2;
|
||||
sol.WallPad = wallPad; sol.ItemPad = itemPad; sol.WallThickness = wallThickness;
|
||||
sol.Unpacked = unpacked;
|
||||
|
||||
_solutions.Add(sol);
|
||||
_unpackedItems.AddRange(unpacked);
|
||||
|
||||
// ── Update result labels ──────────────────────────────────────────
|
||||
UpdateResultLabels();
|
||||
|
||||
if (_unpackedItems.Count > 0)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"{_unpackedItems.Count} kusov sa nepodarilo zabaliť.\n" +
|
||||
$"Zoznam nezabalených kusov môžete vyexportovať do PDF (tlačidlo PDF v Exporte), " +
|
||||
$"alebo si ho pozrieť pri prejdení myšou nad textom \"Nezabalené\" vo Výsledkoch.",
|
||||
"Nezabalené kusy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
if (_solutions.Count > 0) ShowCurrentBox();
|
||||
else lblEfficiency.Text = "Využitie: -";
|
||||
ShowCurrentBox();
|
||||
|
||||
progressBar.Visible = false;
|
||||
statusLabel.Text = $"Hotovo! {_solutions.Count} škatúľ, {totalPacked}/{allItems.Count} kusov";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LPT (Longest Processing Time First) partitioning — assigns item types to N bins
|
||||
/// so that bin volumes are as equal as possible. A type is never split across bins.
|
||||
/// </summary>
|
||||
private List<List<Item>> PartitionTypesToBins(List<Item> allItems, int N)
|
||||
{
|
||||
var typeGroups = allItems
|
||||
.GroupBy(i => i.Name)
|
||||
.Select(g => new { Items = g.ToList(), TotalVolume = g.Sum(i => i.Volume), MaxHeight = g.Max(i => i.H) })
|
||||
.OrderByDescending(g => g.MaxHeight)
|
||||
.ToList();
|
||||
|
||||
var bins = Enumerable.Range(0, N).Select(_ => new List<Item>()).ToList();
|
||||
var loads = new double[N];
|
||||
|
||||
foreach (var tg in typeGroups)
|
||||
{
|
||||
int minBin = 0;
|
||||
for (int i = 1; i < N; i++)
|
||||
if (loads[i] < loads[minBin]) minBin = i;
|
||||
bins[minBin].AddRange(tg.Items);
|
||||
loads[minBin] += tg.TotalVolume;
|
||||
}
|
||||
|
||||
return bins;
|
||||
statusLabel.Text = $"Hotovo! {sol.PackedItems.Count}/{allItems.Count} kusov";
|
||||
}
|
||||
|
||||
private (Box? box, List<PackedItem> packed, int totalCount) SuggestBoxSize(
|
||||
@@ -997,7 +1239,7 @@ namespace BoxPacker3D
|
||||
|
||||
var minBox = new Box(minW, minH, minD, wallThickness);
|
||||
var maxBox = new Box(maxW, maxH, maxD, wallThickness);
|
||||
var (packed, box, unpacked) = new LayerPacker().Pack(
|
||||
var (packed, box, unpacked) = CreatePacker().Pack(
|
||||
items, minBox, maxBox, wallPad, itemPad, wallThickness);
|
||||
|
||||
progressBar.Visible = false;
|
||||
@@ -1094,16 +1336,9 @@ namespace BoxPacker3D
|
||||
foreach (var item in allItems)
|
||||
if (!packedSet.Contains(item)) _unpackedItems.Add(item);
|
||||
|
||||
int totalPacked = sugPacked.Count;
|
||||
lblPacked.Text = $"Zabalené: {totalPacked} / {allItems.Count} kusov";
|
||||
lblTotal.Text = $"Počet škatúľ: 1";
|
||||
lblPacked.ForeColor = _unpackedItems.Count > 0
|
||||
? Color.FromArgb(211, 47, 47)
|
||||
: Color.FromArgb(56, 142, 60);
|
||||
if (_unpackedItems.Count > 0)
|
||||
lblPacked.Text += $" | Nezabalené: {_unpackedItems.Count}";
|
||||
UpdateResultLabels();
|
||||
ShowCurrentBox();
|
||||
statusLabel.Text = $"Hotovo! {totalPacked}/{allItems.Count} kusov v škatuli {suggestedBox.W:F0}×{suggestedBox.D:F0}×{suggestedBox.H:F0} mm";
|
||||
statusLabel.Text = $"Hotovo! {sugPacked.Count}/{allItems.Count} kusov v škatuli {suggestedBox.W:F0}×{suggestedBox.D:F0}×{suggestedBox.H:F0} mm";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user