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:
@@ -361,3 +361,4 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/binCheck 2
|
||||||
|
|||||||
@@ -44,6 +44,14 @@
|
|||||||
"PrerequisitesLocation" = "2:1"
|
"PrerequisitesLocation" = "2:1"
|
||||||
"Url" = "8:"
|
"Url" = "8:"
|
||||||
"ComponentsUrl" = "8:"
|
"ComponentsUrl" = "8:"
|
||||||
|
"Items"
|
||||||
|
{
|
||||||
|
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:.NETFramework,Version=v4.7.2"
|
||||||
|
{
|
||||||
|
"Name" = "8:Microsoft .NET Framework 4.7.2 (x86 and x64)"
|
||||||
|
"ProductCode" = "8:.NETFramework,Version=v4.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Release"
|
"Release"
|
||||||
@@ -68,6 +76,14 @@
|
|||||||
"PrerequisitesLocation" = "2:1"
|
"PrerequisitesLocation" = "2:1"
|
||||||
"Url" = "8:"
|
"Url" = "8:"
|
||||||
"ComponentsUrl" = "8:"
|
"ComponentsUrl" = "8:"
|
||||||
|
"Items"
|
||||||
|
{
|
||||||
|
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:.NETFramework,Version=v4.7.2"
|
||||||
|
{
|
||||||
|
"Name" = "8:Microsoft .NET Framework 4.7.2 (x86 and x64)"
|
||||||
|
"ProductCode" = "8:.NETFramework,Version=v4.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,15 +181,15 @@
|
|||||||
{
|
{
|
||||||
"Name" = "8:Microsoft Visual Studio"
|
"Name" = "8:Microsoft Visual Studio"
|
||||||
"ProductName" = "8:3D Box Packer"
|
"ProductName" = "8:3D Box Packer"
|
||||||
"ProductCode" = "8:{AD850FE7-45D6-4A75-BA48-14975E8E19D4}"
|
"ProductCode" = "8:{FE12B7F9-9B56-4E84-87AB-DC02C8A15113}"
|
||||||
"PackageCode" = "8:{0DB6CFE3-A763-4935-B9A2-8FF88E8EAE76}"
|
"PackageCode" = "8:{784F648E-B453-46B7-A4EB-028B03475A69}"
|
||||||
"UpgradeCode" = "8:{73799267-8F2E-4994-AE60-A0CFA8390BEE}"
|
"UpgradeCode" = "8:{73799267-8F2E-4994-AE60-A0CFA8390BEE}"
|
||||||
"AspNetVersion" = "8:"
|
"AspNetVersion" = "8:"
|
||||||
"RestartWWWService" = "11:FALSE"
|
"RestartWWWService" = "11:FALSE"
|
||||||
"RemovePreviousVersions" = "11:TRUE"
|
"RemovePreviousVersions" = "11:TRUE"
|
||||||
"DetectNewerInstalledVersion" = "11:TRUE"
|
"DetectNewerInstalledVersion" = "11:TRUE"
|
||||||
"InstallAllUsers" = "11:FALSE"
|
"InstallAllUsers" = "11:FALSE"
|
||||||
"ProductVersion" = "8:1.0.1"
|
"ProductVersion" = "8:1.0.3"
|
||||||
"Manufacturer" = "8:Your Company"
|
"Manufacturer" = "8:Your Company"
|
||||||
"ARPHELPTELEPHONE" = "8:"
|
"ARPHELPTELEPHONE" = "8:"
|
||||||
"ARPHELPLINK" = "8:"
|
"ARPHELPLINK" = "8:"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<ApplicationIcon></ApplicationIcon>
|
<ApplicationIcon></ApplicationIcon>
|
||||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<AssemblyVersion>1.0.3</AssemblyVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ namespace BoxPacker3D
|
|||||||
{
|
{
|
||||||
public Box Box;
|
public Box Box;
|
||||||
public List<PackedItem> PackedItems;
|
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)
|
public BoxSolution(Box box, List<PackedItem> packed)
|
||||||
{ Box = box; PackedItems = packed; }
|
{ Box = box; PackedItems = packed; }
|
||||||
}
|
}
|
||||||
@@ -26,6 +39,7 @@ namespace BoxPacker3D
|
|||||||
private List<BoxSolution> _solutions = new List<BoxSolution>();
|
private List<BoxSolution> _solutions = new List<BoxSolution>();
|
||||||
private List<Item> _unpackedItems = new List<Item>(); // Track items that didn't fit
|
private List<Item> _unpackedItems = new List<Item>(); // Track items that didn't fit
|
||||||
private int _currentBoxIndex = 0;
|
private int _currentBoxIndex = 0;
|
||||||
|
private ToolTip _unpackedTooltip = new ToolTip { AutoPopDelay = 30000, InitialDelay = 300, ReshowDelay = 100 };
|
||||||
|
|
||||||
private List<ItemType> itemTypes = new List<ItemType>();
|
private List<ItemType> itemTypes = new List<ItemType>();
|
||||||
private Viewer3D viewer;
|
private Viewer3D viewer;
|
||||||
@@ -40,6 +54,10 @@ namespace BoxPacker3D
|
|||||||
private ComboBox cmbItemName;
|
private ComboBox cmbItemName;
|
||||||
private NumericUpDown nudNewW, nudNewH, nudNewD, nudNewCnt;
|
private NumericUpDown nudNewW, nudNewH, nudNewD, nudNewCnt;
|
||||||
private Button btnAddItem, btnRemoveItem, btnPack, btnSuggest, btnClear, btnSaveTemplate, btnEditDb, btnRefreshDb;
|
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 btnExportJpg, btnExportPng, btnExportPdf;
|
||||||
private Button btnPrevBox, btnNextBox;
|
private Button btnPrevBox, btnNextBox;
|
||||||
private Label lblEfficiency, lblPacked, lblTotal, lblBoxNav;
|
private Label lblEfficiency, lblPacked, lblTotal, lblBoxNav;
|
||||||
@@ -149,30 +167,6 @@ namespace BoxPacker3D
|
|||||||
tbl.Controls.Add(ctrl, 0, row);
|
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()
|
private void BuildUI()
|
||||||
{
|
{
|
||||||
this.Text = "3D Box Packer";
|
this.Text = "3D Box Packer";
|
||||||
@@ -235,35 +229,12 @@ namespace BoxPacker3D
|
|||||||
panelLeft.Controls.Add(stack);
|
panelLeft.Controls.Add(stack);
|
||||||
int r = 0;
|
int r = 0;
|
||||||
|
|
||||||
// ── Min box dimensions (mm) ───────────────────────────────────────
|
// ── Min & Max box dimensions (mm) ─────────────────────────────────
|
||||||
grpBox = MakeGroupBox("📦 Min rozmery škatule (mm)");
|
grpBox = MakeGroupBox("📦 Min a Max rozmery škatule (mm)");
|
||||||
nudBoxW = MakeNUD((decimal)_settings.DefaultBox.Width, 10);
|
nudBoxW = MakeNUD((decimal)_settings.DefaultBox.Width, 10);
|
||||||
nudBoxH = MakeNUD((decimal)_settings.DefaultBox.Height, 10);
|
nudBoxH = MakeNUD((decimal)_settings.DefaultBox.Height, 10);
|
||||||
nudBoxD = MakeNUD((decimal)_settings.DefaultBox.Depth, 10);
|
nudBoxD = MakeNUD((decimal)_settings.DefaultBox.Depth, 10);
|
||||||
nudBoxWallThickness = MakeNUD((decimal)_settings.DefaultBox.WallThickness, 0, 500, 1, 0.5m);
|
nudBoxWallThickness = MakeNUD((decimal)_settings.DefaultBox.WallThickness, 0, 500, 1, 0.5m);
|
||||||
|
|
||||||
// 3-column label+NUD row (wall thickness is in Izolácia group)
|
|
||||||
var boxTbl = new TableLayoutPanel
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Fill, AutoSize = true,
|
|
||||||
ColumnCount = 3, RowCount = 2, Margin = new Padding(2)
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
grpBox.Controls.Add(boxTbl);
|
|
||||||
PushRow(stack, grpBox, r++);
|
|
||||||
|
|
||||||
// ── Max dimensions ────────────────────────────────────────────────
|
|
||||||
var grpMax = MakeGroupBox("📏 Max rozmery škatule (mm)");
|
|
||||||
nudMaxW = MakeNUD((decimal)_settings.MaxBox.Width, 10);
|
nudMaxW = MakeNUD((decimal)_settings.MaxBox.Width, 10);
|
||||||
nudMaxH = MakeNUD((decimal)_settings.MaxBox.Height, 10);
|
nudMaxH = MakeNUD((decimal)_settings.MaxBox.Height, 10);
|
||||||
nudMaxD = MakeNUD((decimal)_settings.MaxBox.Depth, 10);
|
nudMaxD = MakeNUD((decimal)_settings.MaxBox.Depth, 10);
|
||||||
@@ -282,8 +253,33 @@ namespace BoxPacker3D
|
|||||||
if (nudBoxD.Value > nudMaxD.Value) nudBoxD.Value = 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));
|
// 4-column table: label column + Šírka/Dĺžka/Výška, one row for Min, one for Max
|
||||||
PushRow(stack, grpMax, r++);
|
var boxTbl = new TableLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Fill, AutoSize = true,
|
||||||
|
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.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++);
|
||||||
|
|
||||||
// ── Padding + wall thickness (mm) ────────────────────────────────
|
// ── Padding + wall thickness (mm) ────────────────────────────────
|
||||||
var grpPad = MakeGroupBox("🧱 Izolácia (mm)");
|
var grpPad = MakeGroupBox("🧱 Izolácia (mm)");
|
||||||
@@ -300,7 +296,7 @@ namespace BoxPacker3D
|
|||||||
padTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
padTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||||
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("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(MakeLabel("Hrúbka steny:"), 2, 0);
|
||||||
padTbl.Controls.Add(nudWallPad, 0, 1);
|
padTbl.Controls.Add(nudWallPad, 0, 1);
|
||||||
padTbl.Controls.Add(nudItemPad, 1, 1);
|
padTbl.Controls.Add(nudItemPad, 1, 1);
|
||||||
@@ -319,7 +315,8 @@ namespace BoxPacker3D
|
|||||||
|
|
||||||
dgvItems = new DataGridView
|
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),
|
BackgroundColor = C(40, 40, 55), GridColor = C(60, 60, 80),
|
||||||
BorderStyle = BorderStyle.None, RowHeadersVisible = false,
|
BorderStyle = BorderStyle.None, RowHeadersVisible = false,
|
||||||
AllowUserToAddRows = false,
|
AllowUserToAddRows = false,
|
||||||
@@ -339,8 +336,32 @@ namespace BoxPacker3D
|
|||||||
dgvItems.Columns.Add("H", "V(mm)");
|
dgvItems.Columns.Add("H", "V(mm)");
|
||||||
dgvItems.Columns.Add("D", "D(mm)");
|
dgvItems.Columns.Add("D", "D(mm)");
|
||||||
dgvItems.Columns.Add("Count", "Počet");
|
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;
|
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ť
|
// Input row 1: Názov (ComboBox s autocomplete) + Pridať + Zmazať
|
||||||
var inputRow1 = new TableLayoutPanel
|
var inputRow1 = new TableLayoutPanel
|
||||||
@@ -446,56 +467,10 @@ namespace BoxPacker3D
|
|||||||
btnAddItem.Click += (s, e) =>
|
btnAddItem.Click += (s, e) =>
|
||||||
{
|
{
|
||||||
var nm = cmbItemName.Text.Trim();
|
var nm = cmbItemName.Text.Trim();
|
||||||
bool isCustomName = !string.IsNullOrEmpty(nm);
|
if (string.IsNullOrEmpty(nm)) nm = $"Kus {dgvItems.Rows.Count + 1}";
|
||||||
if (!isCustomName) nm = $"Kus {dgvItems.Rows.Count + 1}";
|
|
||||||
|
|
||||||
dgvItems.Rows.Add(nm, (int)nudNewW.Value, (int)nudNewH.Value, (int)nudNewD.Value, (int)nudNewCnt.Value);
|
dgvItems.Rows.Add(nm, (int)nudNewW.Value, (int)nudNewH.Value, (int)nudNewD.Value, (int)nudNewCnt.Value);
|
||||||
|
statusLabel.Text = $"Pridaný do zoznamu: {nm}";
|
||||||
// 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}";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
btnRemoveItem.Click += (s, e) =>
|
btnRemoveItem.Click += (s, e) =>
|
||||||
{
|
{
|
||||||
@@ -589,6 +564,70 @@ namespace BoxPacker3D
|
|||||||
btnPack.Click += BtnPack_Click;
|
btnPack.Click += BtnPack_Click;
|
||||||
PushRow(stack, btnPack, r++);
|
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 ───────────────────────────────────────────────────────
|
// ── Results ───────────────────────────────────────────────────────
|
||||||
grpResult = MakeGroupBox("📊 Výsledky");
|
grpResult = MakeGroupBox("📊 Výsledky");
|
||||||
var resTbl = new TableLayoutPanel
|
var resTbl = new TableLayoutPanel
|
||||||
@@ -607,8 +646,9 @@ namespace BoxPacker3D
|
|||||||
grpResult.Controls.Add(resTbl);
|
grpResult.Controls.Add(resTbl);
|
||||||
PushRow(stack, grpResult, r++);
|
PushRow(stack, grpResult, r++);
|
||||||
|
|
||||||
// ── Box navigation ────────────────────────────────────────────────
|
// ── Box navigation (hidden — multi-box packing removed; only one box exists) ──
|
||||||
var grpNav = MakeGroupBox("📦 Navigácia škatúľ");
|
var grpNav = MakeGroupBox("📦 Navigácia škatúľ");
|
||||||
|
grpNav.Visible = false;
|
||||||
var navTbl = new TableLayoutPanel
|
var navTbl = new TableLayoutPanel
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Fill, AutoSize = true,
|
Dock = DockStyle.Fill, AutoSize = true,
|
||||||
@@ -807,10 +847,244 @@ namespace BoxPacker3D
|
|||||||
lblEfficiency.Text = $"Využitie: {effOuter:F1}% (celok) | {effInner:F1}% (bez stien) " +
|
lblEfficiency.Text = $"Využitie: {effOuter:F1}% (celok) | {effInner:F1}% (bez stien) " +
|
||||||
$"({sol.Box.W:F0}×{sol.Box.D:F0}×{sol.Box.H:F0} mm)";
|
$"({sol.Box.W:F0}×{sol.Box.D:F0}×{sol.Box.H:F0} mm)";
|
||||||
Rerender();
|
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 ─────────────────────────────────────────────────────
|
// ── packing logic ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private IPacker CreatePacker() => chkExtremePoints.Checked ? new Packer3D() : new LayerPacker();
|
||||||
|
|
||||||
private void BtnPack_Click(object sender, EventArgs e)
|
private void BtnPack_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (dgvItems.Rows.Count == 0)
|
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]));
|
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 totalItemVol = allItems.Sum(i => i.Volume);
|
||||||
double maxInnerVol = maxUsableW * maxUsableH * maxUsableD;
|
double maxInnerVol = maxUsableW * maxUsableH * maxUsableD;
|
||||||
int boxCount = Math.Max(1, (int)Math.Ceiling(totalItemVol / maxInnerVol));
|
if (totalItemVol > maxInnerVol)
|
||||||
|
{
|
||||||
string confirmMsg =
|
MessageBox.Show(
|
||||||
$"Celkový objem kusov: {totalItemVol / 1000.0:F0} cm³\n" +
|
$"Celkový objem kusov ({totalItemVol / 1000.0:F0} cm³) prekračuje objem maximálnej " +
|
||||||
$"Max. objem škatule (bez iz.): {maxInnerVol / 1000.0:F0} cm³\n\n" +
|
$"škatule ({maxInnerVol / 1000.0:F0} cm³).\n\n" +
|
||||||
$"Odhadovaný počet škatúľ: {boxCount}\n" +
|
$"Kusy, ktoré sa nezmestia, skončia v zozname nezabalených — ten je po zabalení možné " +
|
||||||
$"(každý typ kusu zostane celý v jednej škatuli)\n\n" +
|
$"vyexportovať do PDF (tlačidlo PDF v Exporte).",
|
||||||
$"Pokračovať s balením do {boxCount} škatúľ?";
|
"Kusy sa pravdepodobne nezmestia", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
}
|
||||||
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();
|
|
||||||
|
|
||||||
_solutions.Clear();
|
_solutions.Clear();
|
||||||
_unpackedItems.Clear();
|
_unpackedItems.Clear();
|
||||||
_currentBoxIndex = 0;
|
_currentBoxIndex = 0;
|
||||||
progressBar.Visible = true;
|
progressBar.Visible = true;
|
||||||
progressBar.Value = 0;
|
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];
|
var (packed, packBox, unp, orient0, nW, nD) = new LayerPacker().PackWithInfo(
|
||||||
int boxNum = binIdx + 1;
|
allItems, minBox, maxBox2, wallPad, itemPad, wallThickness);
|
||||||
|
sol = new BoxSolution(packBox, packed) { IsSkyline = true, Orient0 = orient0, NW = nW, ND = nD };
|
||||||
statusLabel.Text = $"Hľadám optimálnu škatuľu {boxNum}/{bins.Count}...";
|
unpacked = unp;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
else
|
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();
|
ShowCurrentBox();
|
||||||
else lblEfficiency.Text = "Využitie: -";
|
|
||||||
|
|
||||||
progressBar.Visible = false;
|
progressBar.Visible = false;
|
||||||
statusLabel.Text = $"Hotovo! {_solutions.Count} škatúľ, {totalPacked}/{allItems.Count} kusov";
|
statusLabel.Text = $"Hotovo! {sol.PackedItems.Count}/{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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private (Box? box, List<PackedItem> packed, int totalCount) SuggestBoxSize(
|
private (Box? box, List<PackedItem> packed, int totalCount) SuggestBoxSize(
|
||||||
@@ -997,7 +1239,7 @@ namespace BoxPacker3D
|
|||||||
|
|
||||||
var minBox = new Box(minW, minH, minD, wallThickness);
|
var minBox = new Box(minW, minH, minD, wallThickness);
|
||||||
var maxBox = new Box(maxW, maxH, maxD, 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);
|
items, minBox, maxBox, wallPad, itemPad, wallThickness);
|
||||||
|
|
||||||
progressBar.Visible = false;
|
progressBar.Visible = false;
|
||||||
@@ -1094,16 +1336,9 @@ namespace BoxPacker3D
|
|||||||
foreach (var item in allItems)
|
foreach (var item in allItems)
|
||||||
if (!packedSet.Contains(item)) _unpackedItems.Add(item);
|
if (!packedSet.Contains(item)) _unpackedItems.Add(item);
|
||||||
|
|
||||||
int totalPacked = sugPacked.Count;
|
UpdateResultLabels();
|
||||||
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}";
|
|
||||||
ShowCurrentBox();
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BoxPacker3D
|
||||||
|
{
|
||||||
|
public interface IPacker
|
||||||
|
{
|
||||||
|
(List<PackedItem> packed, Box box, List<Item> unpacked) Pack(
|
||||||
|
List<Item> allItems, Box minBox, Box maxBox,
|
||||||
|
double wallPad, double itemPad, double wallThickness);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-8
@@ -107,18 +107,29 @@ namespace BoxPacker3D
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Inserts or replaces a template (upsert by Name).</summary>
|
/// <summary>
|
||||||
|
/// Inserts a new template (Id == 0) or updates the existing row by Id (Id > 0).
|
||||||
|
/// Using Id rather than Name for the update key means renaming an existing item
|
||||||
|
/// during edit correctly updates that same row instead of leaving the old-named
|
||||||
|
/// row behind and inserting a new one.
|
||||||
|
/// </summary>
|
||||||
public void Upsert(ItemTemplate item)
|
public void Upsert(ItemTemplate item)
|
||||||
{
|
{
|
||||||
using var conn = Open();
|
using var conn = Open();
|
||||||
using var cmd = conn.CreateCommand();
|
using var cmd = conn.CreateCommand();
|
||||||
cmd.CommandText = @"
|
if (item.Id > 0)
|
||||||
INSERT INTO items (Name, Width, Height, Depth)
|
{
|
||||||
VALUES ($n, $w, $h, $d)
|
cmd.CommandText = @"
|
||||||
ON CONFLICT(Name) DO UPDATE SET
|
UPDATE items SET Name = $n, Width = $w, Height = $h, Depth = $d
|
||||||
Width = excluded.Width,
|
WHERE Id = $id;";
|
||||||
Height = excluded.Height,
|
cmd.Parameters.AddWithValue("$id", item.Id);
|
||||||
Depth = excluded.Depth;";
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cmd.CommandText = @"
|
||||||
|
INSERT INTO items (Name, Width, Height, Depth)
|
||||||
|
VALUES ($n, $w, $h, $d);";
|
||||||
|
}
|
||||||
cmd.Parameters.AddWithValue("$n", item.Name);
|
cmd.Parameters.AddWithValue("$n", item.Name);
|
||||||
cmd.Parameters.AddWithValue("$w", item.Width);
|
cmd.Parameters.AddWithValue("$w", item.Width);
|
||||||
cmd.Parameters.AddWithValue("$h", item.Height);
|
cmd.Parameters.AddWithValue("$h", item.Height);
|
||||||
|
|||||||
@@ -456,9 +456,9 @@ namespace BoxPacker3D
|
|||||||
{
|
{
|
||||||
return new NumericUpDown
|
return new NumericUpDown
|
||||||
{
|
{
|
||||||
Value = (decimal)Math.Max(1, val),
|
|
||||||
Minimum = 1,
|
Minimum = 1,
|
||||||
Maximum = 100000,
|
Maximum = 100000,
|
||||||
|
Value = (decimal)Math.Max(1, val),
|
||||||
DecimalPlaces = 0,
|
DecimalPlaces = 0,
|
||||||
Increment = 10,
|
Increment = 10,
|
||||||
BackColor = Color.FromArgb(45, 45, 60),
|
BackColor = Color.FromArgb(45, 45, 60),
|
||||||
|
|||||||
+392
-203
@@ -11,19 +11,29 @@ namespace BoxPacker3D
|
|||||||
/// 1. Compute total padded volume: each item slot = (W+p)×(H+p)×(D+p), sum all types.
|
/// 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
|
/// 2. t = paddedVol / maxInnerVol → estimate box via linear interpolation
|
||||||
/// dim(t) = dim_min + t*(dim_max – dim_min) for W, H, D
|
/// 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.
|
/// 3. Types keep whatever order they first appear in the input (manual priority —
|
||||||
/// Items may only rotate around the vertical axis (W↔D), H is always preserved.
|
/// e.g. table row order in the UI — not sorted by volume). Items may only rotate
|
||||||
/// 4. Snap D: fit a whole number of items into estimated D.
|
/// around the vertical axis (W↔D); H is always preserved.
|
||||||
/// 5. Snap W: fit a whole number of items into estimated W.
|
/// 4. Type 0 (the first type) defines the box footprint: BestGrid picks (nW × nD)
|
||||||
/// 6. Stack H: add as many layers upward as needed for all items of this type.
|
/// close to the Step-2 estimate. Type 0 is placed in a plain raster grid, layer by layer.
|
||||||
/// Cap at maxBox.H; excess items → unpacked.
|
/// 5. Every subsequent type reuses the SAME W×D footprint. A type is always fully
|
||||||
/// 7. Subsequent types: add full horizontal layers on top (same W×D footprint).
|
/// consumed (or the box height runs out) before the next type is considered.
|
||||||
/// If the type does not fill a complete layer in original orientation, try W↔D rotation.
|
/// If a type's own last layer is left with unused floor space (not enough items
|
||||||
/// 8. After every placement step, verify that no dimension exceeds maxBox.
|
/// of that type to fill it), that space is offered to the NEXT type in line —
|
||||||
/// 9. Tighten box so wall insulation touches items on all six sides.
|
/// and if that type still leaves space, to the type after that, and so on — until
|
||||||
/// 10. Items that do not fit → unpacked list (shown in PDF export).
|
/// the area is filled or every type has been exhausted (only then is what remains
|
||||||
|
/// insulation). This cascading fill is implemented once, uniformly, by FillArea,
|
||||||
|
/// and applies both to a freshly opened layer and to left-over space inside one.
|
||||||
|
/// 6. Nothing may end up unpacked unless it truly does not fit even at maxBox: if
|
||||||
|
/// step 4/5 cannot place everything within the current footprint before the box
|
||||||
|
/// height (maxBox.H) runs out, the footprint (nW or nD) is grown — within
|
||||||
|
/// maxBox's W/D limits — and the WHOLE pack is retried from scratch. Only once
|
||||||
|
/// the footprint is already at its maximum size and items still don't fit do
|
||||||
|
/// they end up in the unpacked list (shown in the UI/PDF export).
|
||||||
|
/// 7. TightenBox shrinks the final box so wall insulation touches items on all six
|
||||||
|
/// sides.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LayerPacker
|
public class LayerPacker : IPacker
|
||||||
{
|
{
|
||||||
private const double EPS = 0.001;
|
private const double EPS = 0.001;
|
||||||
|
|
||||||
@@ -31,22 +41,36 @@ namespace BoxPacker3D
|
|||||||
List<Item> allItems,
|
List<Item> allItems,
|
||||||
Box minBox, Box maxBox,
|
Box minBox, Box maxBox,
|
||||||
double wallPad, double itemPad, double wallThickness)
|
double wallPad, double itemPad, double wallThickness)
|
||||||
|
{
|
||||||
|
var (packed, box, unpacked, _, _, _) = PackWithInfo(allItems, minBox, maxBox, wallPad, itemPad, wallThickness);
|
||||||
|
return (packed, box, unpacked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Same as <see cref="Pack"/>, but also reports the footprint that was actually used:
|
||||||
|
/// type 0's chosen orientation and the (nW, nD) grid — needed by callers that want to
|
||||||
|
/// let the user manually resize the footprint afterwards (see
|
||||||
|
/// <see cref="PackWithFixedFootprint"/>). Returns <c>nW = nD = 0</c> when nothing could
|
||||||
|
/// be placed at all (no type fits inside maxBox).
|
||||||
|
/// </summary>
|
||||||
|
public (List<PackedItem> packed, Box box, List<Item> unpacked,
|
||||||
|
(double iW0, double iH0, double iD0) orient0, int nW, int nD) PackWithInfo(
|
||||||
|
List<Item> allItems,
|
||||||
|
Box minBox, Box maxBox,
|
||||||
|
double wallPad, double itemPad, double wallThickness)
|
||||||
{
|
{
|
||||||
var packed = new List<PackedItem>();
|
var packed = new List<PackedItem>();
|
||||||
var unpacked = new List<Item>();
|
var unpacked = new List<Item>();
|
||||||
double edge = wallPad + wallThickness;
|
double edge = wallPad + wallThickness;
|
||||||
|
|
||||||
if (allItems.Count == 0)
|
if (allItems.Count == 0)
|
||||||
return (packed, minBox, unpacked);
|
return (packed, minBox, unpacked, default, 0, 0);
|
||||||
|
|
||||||
// ── Step 1: total padded volume ───────────────────────────────────────
|
// ── 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 =>
|
double totalPaddedVol = allItems.Sum(i =>
|
||||||
(i.W + itemPad) * (i.H + itemPad) * (i.D + itemPad));
|
(i.W + itemPad) * (i.H + itemPad) * (i.D + itemPad));
|
||||||
|
|
||||||
// ── Step 2: estimated box size via linear interpolation ───────────────
|
// ── 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 maxIW = Math.Max(EPS, maxBox.W - 2 * edge);
|
||||||
double maxIH = Math.Max(EPS, maxBox.H - 2 * edge);
|
double maxIH = Math.Max(EPS, maxBox.H - 2 * edge);
|
||||||
double maxID = Math.Max(EPS, maxBox.D - 2 * edge);
|
double maxID = Math.Max(EPS, maxBox.D - 2 * edge);
|
||||||
@@ -54,236 +78,401 @@ namespace BoxPacker3D
|
|||||||
|
|
||||||
double t = Math.Min(1.0, Math.Max(0.0, totalPaddedVol / maxInnerVol));
|
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 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);
|
double estD = minBox.D + t * (maxBox.D - minBox.D);
|
||||||
|
|
||||||
// Inner (usable) dimensions from the estimate
|
|
||||||
double estIW = Math.Max(0, estW);
|
double estIW = Math.Max(0, estW);
|
||||||
double estIH = Math.Max(0, estH);
|
|
||||||
double estID = Math.Max(0, estD);
|
double estID = Math.Max(0, estD);
|
||||||
|
|
||||||
// ── Step 3: sort types by single-item volume descending ───────────────
|
// ── Step 3: type order — whatever order they first appear in allItems ──
|
||||||
var types = allItems
|
// (volume-based ordering was replaced by manual priority: the caller controls
|
||||||
|
// this via the order items are listed in, e.g. the "Poradie" column in the UI —
|
||||||
|
// GroupBy preserves first-occurrence order, so it just falls out of allItems).
|
||||||
|
var typeGroups = allItems
|
||||||
.GroupBy(i => i.Name)
|
.GroupBy(i => i.Name)
|
||||||
.Select(g => (Proto: g.First(), Items: g.ToList()))
|
.Select(g => (Proto: g.First(), Items: g.ToList()))
|
||||||
.OrderByDescending(g => g.Proto.Volume)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// ── Steps 4–6: place first type ───────────────────────────────────────
|
// A type can only define the footprint if it fits inside maxBox at all (even a
|
||||||
var (proto0, items0) = types[0];
|
// single unit). Types that don't (e.g. one item physically bigger than maxBox)
|
||||||
|
// are permanently unpacked and skipped — the next-largest type takes over as
|
||||||
|
// the footprint-defining "type 0" instead.
|
||||||
|
var permanentlyUnpacked = new List<Item>();
|
||||||
|
int startIdx = 0;
|
||||||
|
|
||||||
bool firstPlaced = false;
|
List<PackedItem>? bestPacked = null;
|
||||||
double extW = 0, extH = 0, extD = 0;
|
List<Item>? bestUnpacked = null;
|
||||||
|
(double iW0, double iH0, double iD0) bestOrient0 = default;
|
||||||
|
int bestNW = 0, bestND = 0;
|
||||||
|
|
||||||
// Open (incomplete) last layer state
|
while (startIdx < typeGroups.Count)
|
||||||
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)
|
var proto0 = typeGroups[startIdx].Proto;
|
||||||
int nD = Math.Max(1, (int)((estID + itemPad) / (iD + itemPad)));
|
int count0 = typeGroups[startIdx].Items.Count;
|
||||||
nD = Math.Min(nD, (int)((maxID + itemPad) / (iD + itemPad)));
|
var remainingTypeGroups = typeGroups.Skip(startIdx).ToList();
|
||||||
if (nD < 1) continue;
|
|
||||||
|
|
||||||
// Step 5: snap W to whole number of items (based on estimated W)
|
// Try both orientations of this candidate type 0; keep whichever manages to
|
||||||
int nW = Math.Max(1, (int)((estIW + itemPad) / (iW + itemPad)));
|
// place everything, or — if neither can — whichever leaves fewer unpacked.
|
||||||
nW = Math.Min(nW, (int)((maxIW + itemPad) / (iW + itemPad)));
|
foreach (var (iW0, iH0, iD0) in Orientations(proto0))
|
||||||
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++],
|
int maxNW = Math.Max(1, (int)((maxIW + itemPad) / (iW0 + itemPad)));
|
||||||
edge + ix * (iW + itemPad),
|
int maxND = Math.Max(1, (int)((maxID + itemPad) / (iD0 + itemPad)));
|
||||||
edge + iy * (iH + itemPad),
|
|
||||||
edge + iz * (iD + itemPad),
|
var grid = BestGrid(iW0, iD0, estIW, estID, maxIW, maxID, count0, maxIH, iH0, itemPad);
|
||||||
iW, iH, iD));
|
if (grid == null) continue; // this orientation can't fit even a single item0
|
||||||
|
int nW = grid.Value.nW, nD = grid.Value.nD;
|
||||||
|
|
||||||
|
List<PackedItem> triedPacked;
|
||||||
|
List<Item> triedUnpacked;
|
||||||
|
int guard = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var (p, unp) = TryPackAll((iW0, iH0, iD0), nW, nD, remainingTypeGroups, edge, itemPad, maxBox);
|
||||||
|
triedPacked = p;
|
||||||
|
triedUnpacked = unp;
|
||||||
|
|
||||||
|
if (triedUnpacked.Count == 0) break;
|
||||||
|
|
||||||
|
bool canGrowW = nW < maxNW;
|
||||||
|
bool canGrowD = nD < maxND;
|
||||||
|
if (!canGrowW && !canGrowD) break;
|
||||||
|
if (++guard > 2000) break; // safety cap
|
||||||
|
|
||||||
|
double wRoom = canGrowW ? (maxNW - nW) / (double)maxNW : -1;
|
||||||
|
double dRoom = canGrowD ? (maxND - nD) / (double)maxND : -1;
|
||||||
|
if (wRoom >= dRoom) nW++; else nD++;
|
||||||
|
|
||||||
|
Debug.WriteLine($"[LP] Grow footprint: nW={nW} nD={nD} (unpacked so far={triedUnpacked.Count})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestUnpacked == null || triedUnpacked.Count < bestUnpacked.Count)
|
||||||
|
{
|
||||||
|
bestPacked = triedPacked;
|
||||||
|
bestUnpacked = triedUnpacked;
|
||||||
|
bestOrient0 = (iW0, iH0, iD0);
|
||||||
|
bestNW = nW; bestND = nD;
|
||||||
|
}
|
||||||
|
if (triedUnpacked.Count == 0) break; // fully successful — no need to try the other orientation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Items that didn't fit (nH was capped) → unpacked
|
if (bestPacked != null) break; // found a workable footprint — done
|
||||||
for (int i = idx; i < items0.Count; i++)
|
|
||||||
unpacked.Add(items0[i]);
|
|
||||||
|
|
||||||
extW = nW * (iW + itemPad) - itemPad;
|
// This type doesn't fit inside maxBox in either orientation at all —
|
||||||
extH = nH * (iH + itemPad) - itemPad;
|
// it can never be placed, regardless of footprint. Skip it permanently
|
||||||
extD = nD * (iD + itemPad) - itemPad;
|
// and try the next-largest type as the footprint definer instead.
|
||||||
|
permanentlyUnpacked.AddRange(typeGroups[startIdx].Items);
|
||||||
// Track open last layer if it was not completely filled.
|
startIdx++;
|
||||||
// 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)
|
if (bestPacked == null)
|
||||||
{
|
{
|
||||||
unpacked.AddRange(allItems);
|
// No type at all fits inside maxBox.
|
||||||
|
unpacked.AddRange(permanentlyUnpacked);
|
||||||
|
return (packed, minBox, unpacked, default, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
unpacked.AddRange(permanentlyUnpacked);
|
||||||
|
unpacked.AddRange(bestUnpacked!);
|
||||||
|
|
||||||
|
var box = TightenBox(bestPacked, edge, minBox, maxBox, wallThickness);
|
||||||
|
return (bestPacked, box, unpacked, bestOrient0, bestNW, bestND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Packs into a MANUALLY specified type-0 footprint (orientation + nW×nD grid),
|
||||||
|
/// bypassing the automatic estimate/growth logic entirely — used when the user wants
|
||||||
|
/// to hand-tune the footprint that <see cref="PackWithInfo"/> originally computed.
|
||||||
|
/// nW/nD are clamped to what maxBox can physically hold. Types are the same
|
||||||
|
/// "largest volume first" groups Pack/PackWithInfo would derive from allItems;
|
||||||
|
/// <paramref name="orient0"/> must belong to whichever type ends up first after that
|
||||||
|
/// sort (i.e. the same type 0 the original packing chose) or the footprint won't mean
|
||||||
|
/// what the caller expects.
|
||||||
|
/// </summary>
|
||||||
|
public (List<PackedItem> packed, Box box, List<Item> unpacked) PackWithFixedFootprint(
|
||||||
|
List<Item> allItems, Box minBox, Box maxBox,
|
||||||
|
double wallPad, double itemPad, double wallThickness,
|
||||||
|
(double iW0, double iH0, double iD0) orient0, int nW, int nD)
|
||||||
|
{
|
||||||
|
var packed = new List<PackedItem>();
|
||||||
|
var unpacked = new List<Item>();
|
||||||
|
double edge = wallPad + wallThickness;
|
||||||
|
|
||||||
|
if (allItems.Count == 0)
|
||||||
return (packed, minBox, unpacked);
|
return (packed, minBox, unpacked);
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 7: subsequent types — fill open layer then full layers on top ─
|
double maxIW = Math.Max(EPS, maxBox.W - 2 * edge);
|
||||||
for (int ti = 1; ti < types.Count; ti++)
|
double maxID = Math.Max(EPS, maxBox.D - 2 * edge);
|
||||||
{
|
int maxNW = Math.Max(1, (int)((maxIW + itemPad) / (orient0.iW0 + itemPad)));
|
||||||
var (proto, typeItems) = types[ti];
|
int maxND = Math.Max(1, (int)((maxID + itemPad) / (orient0.iD0 + itemPad)));
|
||||||
var queue = new Queue<Item>(typeItems);
|
nW = Math.Clamp(nW, 1, maxNW);
|
||||||
|
nD = Math.Clamp(nD, 1, maxND);
|
||||||
|
|
||||||
// ── 7a: fill the open (incomplete) last layer of the previous type ─
|
var typeGroups = allItems
|
||||||
Debug.WriteLine(
|
.GroupBy(i => i.Name)
|
||||||
$"[LP] Type{ti} '{proto.Name}': openLayerY={openLayerY:F1} " +
|
.Select(g => (Proto: g.First(), Items: g.ToList()))
|
||||||
$"openFilled={openFilled} openCap={openCapacity} " +
|
.ToList();
|
||||||
$"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.
|
var (p, unp) = TryPackAll(orient0, nW, nD, typeGroups, edge, itemPad, maxBox);
|
||||||
// Items must fit within openID depth to avoid overlapping Region B.
|
packed = p;
|
||||||
if (ixPartial > 0)
|
unpacked = unp;
|
||||||
{
|
|
||||||
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).
|
if (packed.Count == 0)
|
||||||
int izFree = izPartial + (ixPartial > 0 ? 1 : 0);
|
return (packed, minBox, unpacked);
|
||||||
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);
|
var box = TightenBox(packed, edge, minBox, maxBox, wallThickness);
|
||||||
return (packed, box, unpacked);
|
return (packed, box, unpacked);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
/// <summary>
|
||||||
|
/// A free rectangular floor area whose current "height" (the Y an item placed here
|
||||||
|
/// would start at) may differ from every other slot's — this is what allows different
|
||||||
|
/// parts of the footprint to reach different heights (a "staircase") instead of every
|
||||||
|
/// part waiting for a single shared layer height.
|
||||||
|
/// </summary>
|
||||||
|
private readonly struct Slot
|
||||||
|
{
|
||||||
|
public readonly double X, Z, W, D, Y;
|
||||||
|
public Slot(double x, double z, double w, double d, double y) { X = x; Z = z; W = w; D = d; Y = y; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fills a rectangular horizontal region (xBase,zBase)+(rectW×rectD) at height y
|
/// Packs every type (type 0 included) into a fixed nW×nD footprint (derived from
|
||||||
/// with items from queue, trying both W/D orientations. After placing the primary
|
/// type 0's chosen orientation). Starts from one slot covering the whole footprint
|
||||||
/// grid the remaining two strips (Z-remainder and X-remainder) are filled recursively,
|
/// and repeatedly places ONE layer of the current type into whichever free slot is
|
||||||
/// so rotated items can be used to fill gaps left by the primary orientation.
|
/// currently lowest (ties broken by largest area) — see <see cref="PlaceIntoSlot"/> —
|
||||||
/// layerH caps the maximum item height that may be placed.
|
/// re-evaluating after every single layer so a type spreads breadth-first across
|
||||||
|
/// same-height areas instead of being stacked entirely into one narrow column. Once
|
||||||
|
/// no equally-low area is left for it, later layers of the same type naturally climb
|
||||||
|
/// higher (a "staircase") rather than everything waiting for the tallest neighbour.
|
||||||
|
/// A type is only skipped once it no longer fits into ANY current slot. Whatever
|
||||||
|
/// cannot be placed because the box height (maxBox.H) is exhausted is returned as
|
||||||
|
/// unpacked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void FillRectangle(
|
private static (List<PackedItem> packed, List<Item> unpacked) TryPackAll(
|
||||||
Queue<Item> queue, List<PackedItem> packed,
|
(double iW0, double iH0, double iD0) orient0, int nW, int nD,
|
||||||
double xBase, double zBase, double rectW, double rectD,
|
List<(Item Proto, List<Item> Items)> typeGroups,
|
||||||
double y, double layerH, double itemPad, Item proto)
|
double edge, double itemPad, Box maxBox)
|
||||||
{
|
{
|
||||||
if (queue.Count == 0 || rectW < EPS || rectD < EPS) return;
|
double extW = nW * (orient0.iW0 + itemPad) - itemPad;
|
||||||
|
double extD = nD * (orient0.iD0 + itemPad) - itemPad;
|
||||||
|
double maxTopY = edge + Math.Max(EPS, maxBox.H - 2 * edge);
|
||||||
|
|
||||||
|
var types = typeGroups.Select(g => (Proto: g.Proto, Queue: new Queue<Item>(g.Items))).ToList();
|
||||||
|
var packed = new List<PackedItem>();
|
||||||
|
var slots = new List<Slot> { new Slot(edge, edge, extW, extD, edge) };
|
||||||
|
|
||||||
|
int typeIdx = 0;
|
||||||
|
while (typeIdx < types.Count)
|
||||||
|
{
|
||||||
|
if (types[typeIdx].Queue.Count == 0) { typeIdx++; continue; }
|
||||||
|
|
||||||
|
var proto = types[typeIdx].Proto;
|
||||||
|
var queue = types[typeIdx].Queue;
|
||||||
|
(double, double, double)? forced = typeIdx == 0 ? orient0 : ((double, double, double)?)null;
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
int bestIdx = -1;
|
||||||
|
double bestY = double.MaxValue, bestArea = -1;
|
||||||
|
(double iW, double iH, double iD) bestOrient = default;
|
||||||
|
|
||||||
|
for (int i = 0; i < slots.Count; i++)
|
||||||
|
{
|
||||||
|
var s = slots[i];
|
||||||
|
double availH = maxTopY - s.Y;
|
||||||
|
if (availH <= EPS) continue;
|
||||||
|
var ch = PickOrientation(proto, s.W, s.D, availH, itemPad, forced);
|
||||||
|
if (ch == null) continue;
|
||||||
|
double area = s.W * s.D;
|
||||||
|
if (s.Y < bestY - EPS || (Math.Abs(s.Y - bestY) <= EPS && area > bestArea))
|
||||||
|
{
|
||||||
|
bestY = s.Y; bestArea = area; bestIdx = i; bestOrient = ch.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestIdx < 0) break; // this type doesn't fit into any current slot right now
|
||||||
|
|
||||||
|
var slot = slots[bestIdx];
|
||||||
|
slots.RemoveAt(bestIdx);
|
||||||
|
slots.AddRange(PlaceIntoSlot(slot, queue, bestOrient, itemPad, packed));
|
||||||
|
}
|
||||||
|
|
||||||
|
typeIdx++; // exhausted, or doesn't fit anywhere currently — move on to the next type
|
||||||
|
}
|
||||||
|
|
||||||
|
var unpacked = new List<Item>();
|
||||||
|
foreach (var t in types)
|
||||||
|
unpacked.AddRange(t.Queue);
|
||||||
|
|
||||||
|
return (packed, unpacked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places ONE sub-layer of <paramref name="orient"/> into <paramref name="slot"/>,
|
||||||
|
/// then splits whatever's left of the slot into new, geometrically disjoint slots —
|
||||||
|
/// the used footprint at its new (taller) height, and whatever wasn't touched (a
|
||||||
|
/// partially filled row, or the boundary strip left when the footprint isn't an exact
|
||||||
|
/// multiple of the item) still at the slot's original height. Only one layer is placed
|
||||||
|
/// per call — the caller re-evaluates which slot is lowest before every placement —
|
||||||
|
/// so a type spreads across same-height areas breadth-first instead of being stacked
|
||||||
|
/// entirely into one narrow column before its neighbours are considered. Different
|
||||||
|
/// areas of the footprint can still end up at different heights (a staircase) once
|
||||||
|
/// their available slots genuinely differ.
|
||||||
|
/// </summary>
|
||||||
|
private static List<Slot> PlaceIntoSlot(
|
||||||
|
Slot slot, Queue<Item> queue, (double iW, double iH, double iD) orient,
|
||||||
|
double itemPad, List<PackedItem> packed)
|
||||||
|
{
|
||||||
|
var (iW, iH, iD) = orient;
|
||||||
|
int nW = (int)((slot.W + itemPad) / (iW + itemPad));
|
||||||
|
int nD = (int)((slot.D + itemPad) / (iD + itemPad));
|
||||||
|
int cap = nW * nD;
|
||||||
|
|
||||||
|
double gridW = nW * (iW + itemPad) - itemPad;
|
||||||
|
double gridD = nD * (iD + itemPad) - itemPad;
|
||||||
|
double zRemain = slot.D - nD * (iD + itemPad);
|
||||||
|
double xRemain = slot.W - nW * (iW + itemPad);
|
||||||
|
|
||||||
|
int filled = 0;
|
||||||
|
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(),
|
||||||
|
slot.X + ix * (iW + itemPad), slot.Y, slot.Z + iz * (iD + itemPad), iW, iH, iD));
|
||||||
|
filled++;
|
||||||
|
}
|
||||||
|
|
||||||
|
double newY = slot.Y + (iH + itemPad);
|
||||||
|
var result = new List<Slot>();
|
||||||
|
|
||||||
|
if (filled < cap)
|
||||||
|
{
|
||||||
|
// Ran out mid-row: the cells that DID get this layer end up taller (newY)
|
||||||
|
// than the ones that didn't (still slot.Y) — split accordingly.
|
||||||
|
int filledRows = filled / nW;
|
||||||
|
int filledInRow = filled % nW;
|
||||||
|
|
||||||
|
if (filledInRow > 0)
|
||||||
|
{
|
||||||
|
result.Add(new Slot(slot.X, slot.Z + filledRows * (iD + itemPad),
|
||||||
|
filledInRow * (iW + itemPad) - itemPad, iD, newY));
|
||||||
|
result.Add(new Slot(slot.X + filledInRow * (iW + itemPad), slot.Z + filledRows * (iD + itemPad),
|
||||||
|
gridW - filledInRow * (iW + itemPad), iD, slot.Y));
|
||||||
|
}
|
||||||
|
if (filledRows > 0)
|
||||||
|
result.Add(new Slot(slot.X, slot.Z, gridW, filledRows * (iD + itemPad) - itemPad, newY));
|
||||||
|
|
||||||
|
// Rows of the grid beyond what we attempted (still within the grid's own
|
||||||
|
// width — the boundary strip beyond gridW is a SEPARATE slot, added below;
|
||||||
|
// using the full slot width here would double-claim that strip).
|
||||||
|
int fullRowsUsed = filledRows + (filledInRow > 0 ? 1 : 0);
|
||||||
|
double zOff = fullRowsUsed * (iD + itemPad);
|
||||||
|
double remGridD = gridD - zOff;
|
||||||
|
if (remGridD > EPS)
|
||||||
|
result.Add(new Slot(slot.X, slot.Z + zOff, gridW, remGridD, slot.Y));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Layer fully placed — one slot for the used footprint at its new (taller) height.
|
||||||
|
result.Add(new Slot(slot.X, slot.Z, gridW, gridD, newY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boundary strips (footprint not an exact multiple of this orientation) never got
|
||||||
|
// touched — they stay at the slot's ORIGINAL height, as fresh slots of their own,
|
||||||
|
// so the main loop can offer them (with full remaining height, not capped) to this
|
||||||
|
// same type again or to the next type in line.
|
||||||
|
if (zRemain > EPS)
|
||||||
|
result.Add(new Slot(slot.X, slot.Z + nD * (iD + itemPad), slot.W, zRemain, slot.Y));
|
||||||
|
if (xRemain > EPS)
|
||||||
|
result.Add(new Slot(slot.X + nW * (iW + itemPad), slot.Z, xRemain, gridD, slot.Y));
|
||||||
|
|
||||||
|
return result.Where(s => s.W > EPS && s.D > EPS).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Picks the orientation of <paramref name="proto"/> (from <see cref="Orientations"/>)
|
||||||
|
/// that fits the most items into rectW×rectD while respecting the layerH height cap.
|
||||||
|
/// If <paramref name="forced"/> is supplied, that exact orientation is used instead
|
||||||
|
/// (used for type 0, whose footprint is tailored to its own dimensions).
|
||||||
|
/// </summary>
|
||||||
|
private static (double iW, double iH, double iD)? PickOrientation(
|
||||||
|
Item proto, double rectW, double rectD, double layerH, double itemPad,
|
||||||
|
(double, double, double)? forced)
|
||||||
|
{
|
||||||
|
if (forced.HasValue)
|
||||||
|
{
|
||||||
|
var (fw, fh, fd) = forced.Value;
|
||||||
|
return fh <= layerH + EPS ? forced : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
(double iW, double iH, double iD)? best = null;
|
||||||
|
int bestCap = 0;
|
||||||
foreach (var (iW, iH, iD) in Orientations(proto))
|
foreach (var (iW, iH, iD) in Orientations(proto))
|
||||||
{
|
{
|
||||||
if (iH > layerH + EPS) continue;
|
if (iH > layerH + EPS) continue;
|
||||||
|
|
||||||
int nW = (int)((rectW + itemPad) / (iW + itemPad));
|
int nW = (int)((rectW + itemPad) / (iW + itemPad));
|
||||||
int nD = (int)((rectD + itemPad) / (iD + itemPad));
|
int nD = (int)((rectD + itemPad) / (iD + itemPad));
|
||||||
if (nW < 1 || nD < 1) continue;
|
if (nW < 1 || nD < 1) continue;
|
||||||
|
int cap = nW * nD;
|
||||||
for (int iz = 0; iz < nD && queue.Count > 0; iz++)
|
if (cap > bestCap)
|
||||||
for (int ix = 0; ix < nW && queue.Count > 0; ix++)
|
{
|
||||||
packed.Add(new PackedItem(queue.Dequeue(),
|
bestCap = cap;
|
||||||
xBase + ix * (iW + itemPad),
|
best = (iW, iH, iD);
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches a small neighbourhood around the estimated inner dimensions for the
|
||||||
|
/// (nW, nD) grid that needs the fewest stacked layers to hold <paramref name="count"/>
|
||||||
|
/// items, instead of naively flooring the estimate (which systematically
|
||||||
|
/// under-sizes the footprint). Ties are broken by whichever footprint area is
|
||||||
|
/// closest to the estimated footprint area.
|
||||||
|
/// </summary>
|
||||||
|
private static (int nW, int nD)? BestGrid(
|
||||||
|
double iW, double iD, double estIW, double estID,
|
||||||
|
double maxIW, double maxID, int count, double maxIH, double iH, double itemPad)
|
||||||
|
{
|
||||||
|
int maxNW = Math.Max(1, (int)((maxIW + itemPad) / (iW + itemPad)));
|
||||||
|
int maxND = Math.Max(1, (int)((maxID + itemPad) / (iD + itemPad)));
|
||||||
|
|
||||||
|
int roundW = (int)Math.Round((estIW + itemPad) / (iW + itemPad), MidpointRounding.AwayFromZero);
|
||||||
|
int roundD = (int)Math.Round((estID + itemPad) / (iD + itemPad), MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
(int nW, int nD)? bestGrid = null;
|
||||||
|
int bestNH = int.MaxValue;
|
||||||
|
double bestAreaDiff = double.MaxValue;
|
||||||
|
double estArea = estIW * estID;
|
||||||
|
|
||||||
|
for (int dOff = -1; dOff <= 1; dOff++)
|
||||||
|
{
|
||||||
|
int nD = Math.Clamp(roundD + dOff, 1, maxND);
|
||||||
|
for (int wOff = -1; wOff <= 1; wOff++)
|
||||||
|
{
|
||||||
|
int nW = Math.Clamp(roundW + wOff, 1, maxNW);
|
||||||
|
|
||||||
|
int nH = (int)Math.Ceiling((double)count / (nW * nD));
|
||||||
|
int capNH = (int)((maxIH + itemPad) / (iH + itemPad));
|
||||||
|
if (capNH < 1) continue;
|
||||||
|
nH = Math.Min(nH, capNH);
|
||||||
|
if (nH < 1) continue;
|
||||||
|
|
||||||
|
double area = (nW * (iW + itemPad)) * (nD * (iD + itemPad));
|
||||||
|
double areaDiff = Math.Abs(area - estArea);
|
||||||
|
|
||||||
|
bool better = nH < bestNH || (nH == bestNH && areaDiff < bestAreaDiff);
|
||||||
|
if (better)
|
||||||
|
{
|
||||||
|
bestNH = nH;
|
||||||
|
bestAreaDiff = areaDiff;
|
||||||
|
bestGrid = (nW, nD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestGrid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+114
-10
@@ -1,23 +1,127 @@
|
|||||||
/*
|
|
||||||
* PÔVODNÝ ALGORITMUS – zakomentovaný, nahradený LayerPacker
|
|
||||||
*
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace BoxPacker3D
|
namespace BoxPacker3D
|
||||||
{
|
{
|
||||||
public class Packer3D
|
/// <summary>
|
||||||
|
/// Extreme Points packing algorithm — an alternative to <see cref="LayerPacker"/> (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.
|
||||||
|
/// </summary>
|
||||||
|
public class Packer3D : IPacker
|
||||||
{
|
{
|
||||||
private const double EPS = 0.001;
|
private const double EPS = 0.001;
|
||||||
|
|
||||||
public List<PackedItem> Pack(List<Item> items, Box box,
|
public (List<PackedItem> packed, Box box, List<Item> unpacked) Pack(
|
||||||
double wallPad = 0, double itemPad = 0, Action<int, int>? onProgress = null,
|
List<Item> allItems, Box minBox, Box maxBox,
|
||||||
bool allowRotation = true, bool preferRotated = false)
|
double wallPad, double itemPad, double wallThickness)
|
||||||
{
|
{
|
||||||
// ... (Extreme Points algorithm)
|
var packed = new List<PackedItem>();
|
||||||
|
var unpacked = new List<Item>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Two allowed orientations: original and rotated 90° around the 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Overlaps(
|
||||||
|
List<PackedItem> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
// ... helper methods
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
+8
-5
@@ -175,7 +175,8 @@ namespace BoxPacker3D
|
|||||||
Count = g.Count(),
|
Count = g.Count(),
|
||||||
W = g.First().W,
|
W = g.First().W,
|
||||||
H = g.First().H,
|
H = g.First().H,
|
||||||
D = g.First().D
|
D = g.First().D,
|
||||||
|
TotalVolume = g.Sum(it => it.Volume)
|
||||||
})
|
})
|
||||||
.OrderBy(x => x.Name)
|
.OrderBy(x => x.Name)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -212,8 +213,9 @@ namespace BoxPacker3D
|
|||||||
cs.AppendLine("0.25 0.25 0.35 rg BT /F1 9 Tf");
|
cs.AppendLine("0.25 0.25 0.35 rg BT /F1 9 Tf");
|
||||||
cs.AppendLine($"{Margin + 10:F2} {y + 6:F2} Td");
|
cs.AppendLine($"{Margin + 10:F2} {y + 6:F2} Td");
|
||||||
cs.AppendLine("(Nazov) Tj");
|
cs.AppendLine("(Nazov) Tj");
|
||||||
cs.AppendLine($"{250:F2} {0:F2} Td (Rozmery \\(mm\\)) Tj");
|
cs.AppendLine($"{190:F2} {0:F2} Td (Rozmery \\(mm\\)) Tj");
|
||||||
cs.AppendLine($"{160:F2} {0:F2} Td (Pocet) Tj ET");
|
cs.AppendLine($"{140:F2} {0:F2} Td (Pocet) Tj");
|
||||||
|
cs.AppendLine($"{140:F2} {0:F2} Td (Objem typu \\(cm3\\)) Tj ET");
|
||||||
|
|
||||||
y -= 25;
|
y -= 25;
|
||||||
|
|
||||||
@@ -233,8 +235,9 @@ namespace BoxPacker3D
|
|||||||
cs.AppendLine("0.20 0.20 0.30 rg BT /F2 8 Tf");
|
cs.AppendLine("0.20 0.20 0.30 rg BT /F2 8 Tf");
|
||||||
cs.AppendLine($"{Margin + 10:F2} {y + 5:F2} Td");
|
cs.AppendLine($"{Margin + 10:F2} {y + 5:F2} Td");
|
||||||
cs.AppendLine($"({EscPdf(item.Name)}) Tj");
|
cs.AppendLine($"({EscPdf(item.Name)}) Tj");
|
||||||
cs.AppendLine($"{250:F2} {0:F2} Td ({item.W:F0} x {item.H:F0} x {item.D:F0}) Tj");
|
cs.AppendLine($"{190:F2} {0:F2} Td ({item.W:F0} x {item.H:F0} x {item.D:F0}) Tj");
|
||||||
cs.AppendLine($"{160:F2} {0:F2} Td ({item.Count}) Tj ET");
|
cs.AppendLine($"{140:F2} {0:F2} Td ({item.Count}) Tj");
|
||||||
|
cs.AppendLine($"{140:F2} {0:F2} Td ({item.TotalVolume / 1000.0:F0}) Tj ET");
|
||||||
|
|
||||||
y -= 18;
|
y -= 18;
|
||||||
rowNum++;
|
rowNum++;
|
||||||
|
|||||||
Reference in New Issue
Block a user