Files
BoxPacker3D/Form1.cs
T
romanandClaude Sonnet 5 654e1b79d0 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>
2026-08-19 14:23:54 +02:00

1471 lines
69 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace BoxPacker3D
{
// Multi-box storage: each "solution" box contains its packed items
public class BoxSolution
{
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; }
}
public partial class Form1 : Form
{
private AppSettings _settings;
private ItemDatabase _itemDb;
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;
private GlViewer glViewer;
private Panel panelLeft, panelRight, panelTop;
private GroupBox grpBox, grpItems, grpResult;
private NumericUpDown nudBoxW, nudBoxH, nudBoxD, nudBoxWallThickness;
private NumericUpDown nudMaxW, nudMaxH, nudMaxD;
private NumericUpDown nudWallPad, nudItemPad;
private DataGridView dgvItems;
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;
private Panel panelViewer;
private TrackBar trkRotX, trkRotY, trkZoom;
private Label lblRotX, lblRotY, lblZoom;
private StatusStrip statusStrip;
private ToolStripStatusLabel statusLabel;
private ToolStripProgressBar progressBar;
public Form1()
{
// Load settings & open database BEFORE UI construction so defaults can be applied
_settings = AppSettings.Load();
try
{
_itemDb = new ItemDatabase(_settings.ResolvedDatabasePath);
}
catch (Exception ex)
{
MessageBox.Show(
$"Databázu kusov sa nepodarilo otvoriť:\n{_settings.ResolvedDatabasePath}\n\n{ex.Message}",
"Chyba databázy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_itemDb = null;
}
InitializeComponent();
BuildUI();
viewer = new Viewer3D(panelViewer);
try
{
glViewer = new GlViewer(panelViewer);
glViewer.ViewChanged += GlViewer_ViewChanged;
statusLabel.Text = $"GPU renderer aktívny | DB: {_settings.ResolvedDatabasePath}";
}
catch (Exception ex)
{
glViewer = null;
statusLabel.Text = $"GDI+ renderer (OpenGL nedostupný: {ex.Message})";
}
RefreshItemNameCombo();
}
private static Color C(int r, int g, int b) => Color.FromArgb(r, g, b);
private Label MakeLabel(string text, Color? color = null)
{
return new Label
{
Text = text,
ForeColor = color ?? C(180, 180, 200),
AutoSize = true,
Anchor = AnchorStyles.Left | AnchorStyles.Top,
Margin = new Padding(3, 7, 3, 0)
};
}
private NumericUpDown MakeNUD(decimal val, decimal min = 0, decimal max = 100000,
int decimalPlaces = 0, decimal increment = 1)
{
return new NumericUpDown
{
Minimum = min, Maximum = max, Value = val,
DecimalPlaces = decimalPlaces, Increment = increment,
BackColor = C(45, 45, 60), ForeColor = Color.White,
BorderStyle = BorderStyle.FixedSingle,
Dock = DockStyle.Fill, Margin = new Padding(3)
};
}
private Button MakeButton(string text, Color bg, Color fg)
{
var btn = new Button
{
Text = text, BackColor = bg, ForeColor = fg,
FlatStyle = FlatStyle.Flat, Cursor = Cursors.Hand,
Font = new Font("Segoe UI", 9f, FontStyle.Bold),
Dock = DockStyle.Fill, AutoSize = false,
Height = 32, Margin = new Padding(2)
};
btn.FlatAppearance.BorderColor = C(80, 80, 100);
btn.FlatAppearance.BorderSize = 1;
return btn;
}
private static GroupBox MakeGroupBox(string title)
{
return new GroupBox
{
Text = title, ForeColor = C(100, 200, 255),
BackColor = C(35, 35, 48),
Font = new Font("Segoe UI", 9f, FontStyle.Bold),
Dock = DockStyle.Fill, AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
Margin = new Padding(0, 0, 0, 6),
Padding = new Padding(6, 8, 6, 6)
};
}
private static void PushRow(TableLayoutPanel tbl, Control ctrl, int row,
SizeType st = SizeType.AutoSize, float size = 0)
{
tbl.RowCount = row + 1;
tbl.RowStyles.Add(new RowStyle(st, size));
tbl.Controls.Add(ctrl, 0, row);
}
private void BuildUI()
{
this.Text = "3D Box Packer";
this.Size = new Size(_settings.UI.WindowWidth, _settings.UI.WindowHeight);
this.MinimumSize = new Size(1050, 720);
this.BackColor = C(30, 30, 40);
this.AutoScaleMode = AutoScaleMode.Dpi;
this.AutoScaleDimensions = new SizeF(96f, 96f);
statusStrip = new StatusStrip { BackColor = C(20, 20, 30) };
statusLabel = new ToolStripStatusLabel("Pripravený") { ForeColor = C(100, 200, 255) };
progressBar = new ToolStripProgressBar
{
Minimum = 0, Maximum = 100, Value = 0, Visible = false,
Size = new Size(200, 16),
};
statusStrip.Items.Add(statusLabel);
statusStrip.Items.Add(progressBar);
this.Controls.Add(statusStrip);
panelTop = new Panel { Dock = DockStyle.Top, Height = 50, BackColor = C(20, 20, 30) };
panelTop.Controls.Add(new Label
{
Text = "⬛ 3D Box Packer",
Font = new Font("Segoe UI", 16f, FontStyle.Bold),
ForeColor = C(100, 200, 255),
AutoSize = true, Location = new Point(15, 10)
});
this.Controls.Add(panelTop);
panelLeft = new Panel
{
Dock = DockStyle.Left, Width = _settings.UI.LeftPanelWidth,
BackColor = C(35, 35, 48),
Padding = new Padding(10), AutoScroll = true
};
this.Controls.Add(panelLeft);
panelRight = new Panel { Dock = DockStyle.Fill, BackColor = C(25, 25, 35) };
this.Controls.Add(panelRight);
BuildLeftPanel();
BuildRightPanel();
this.Controls.SetChildIndex(statusStrip, 0);
this.Controls.SetChildIndex(panelRight, 1);
this.Controls.SetChildIndex(panelLeft, 2);
this.Controls.SetChildIndex(panelTop, 3);
}
private void BuildLeftPanel()
{
var stack = new TableLayoutPanel
{
Dock = DockStyle.Top, AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1, Padding = new Padding(0), Margin = new Padding(0)
};
stack.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
panelLeft.Controls.Add(stack);
int r = 0;
// ── 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);
// 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 = 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) ────────────────────────────────
var grpPad = MakeGroupBox("🧱 Izolácia (mm)");
nudWallPad = MakeNUD((decimal)_settings.Padding.WallPadding, 0, 1000, 1, 5m);
nudItemPad = MakeNUD((decimal)_settings.Padding.ItemPadding, 0, 1000, 1, 5m);
var padTbl = new TableLayoutPanel
{
Dock = DockStyle.Fill, AutoSize = true,
ColumnCount = 3, RowCount = 2, Margin = new Padding(2)
};
padTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 34f));
padTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
padTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
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("Medzikusová:"), 1, 0);
padTbl.Controls.Add(MakeLabel("Hrúbka steny:"), 2, 0);
padTbl.Controls.Add(nudWallPad, 0, 1);
padTbl.Controls.Add(nudItemPad, 1, 1);
padTbl.Controls.Add(nudBoxWallThickness, 2, 1);
grpPad.Controls.Add(padTbl);
PushRow(stack, grpPad, r++);
// ── Items (mm) ────────────────────────────────────────────────────
grpItems = MakeGroupBox("🧩 Kusy na zabalenie (mm)");
var itemsStack = new TableLayoutPanel
{
Dock = DockStyle.Fill, AutoSize = true,
ColumnCount = 1, Margin = new Padding(2)
};
itemsStack.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
dgvItems = new DataGridView
{
// 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,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
Margin = new Padding(0, 0, 0, 4)
};
dgvItems.DefaultCellStyle.BackColor = C(40, 40, 55);
dgvItems.DefaultCellStyle.ForeColor = Color.White;
dgvItems.DefaultCellStyle.SelectionBackColor = C(60, 100, 160);
dgvItems.ColumnHeadersDefaultCellStyle.BackColor = C(30, 30, 45);
dgvItems.ColumnHeadersDefaultCellStyle.ForeColor = C(100, 200, 255);
dgvItems.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 8.5f, FontStyle.Bold);
dgvItems.EnableHeadersVisualStyles = false;
dgvItems.Columns.Add("Name", "Názov");
dgvItems.Columns.Add("W", "Š(mm)");
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, 235);
// Input row 1: Názov (ComboBox s autocomplete) + Pridať + Zmazať
var inputRow1 = new TableLayoutPanel
{
Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 3,
Margin = new Padding(0, 4, 0, 2)
};
inputRow1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50f));
inputRow1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
inputRow1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
cmbItemName = new ComboBox
{
BackColor = C(45, 45, 60), ForeColor = Color.White,
FlatStyle = FlatStyle.Flat,
Dock = DockStyle.Fill, Margin = new Padding(3),
DropDownStyle = ComboBoxStyle.DropDown, // editable + dropdown
AutoCompleteMode = AutoCompleteMode.SuggestAppend,
AutoCompleteSource = AutoCompleteSource.ListItems
};
btnAddItem = MakeButton("➕ Pridať", C(40, 120, 60), Color.White);
btnRemoveItem = MakeButton("🗑 Zmazať", C(120, 40, 40), Color.White);
inputRow1.Controls.Add(cmbItemName, 0, 0);
inputRow1.Controls.Add(btnAddItem, 1, 0);
inputRow1.Controls.Add(btnRemoveItem, 2, 0);
PushRow(itemsStack, inputRow1, ir++);
// Input row 2: dimensions
nudNewW = MakeNUD(100, 1);
nudNewH = MakeNUD(100, 1);
nudNewD = MakeNUD(100, 1);
nudNewCnt = MakeNUD(1, 1);
var inputRow2 = new TableLayoutPanel
{
Dock = DockStyle.Fill, AutoSize = true,
ColumnCount = 4, RowCount = 2, Margin = new Padding(0, 2, 0, 4)
};
inputRow2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
inputRow2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
inputRow2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
inputRow2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
inputRow2.RowStyles.Add(new RowStyle(SizeType.AutoSize));
inputRow2.RowStyles.Add(new RowStyle(SizeType.AutoSize));
inputRow2.Controls.Add(MakeLabel("Šírka (mm):"), 0, 0);
inputRow2.Controls.Add(MakeLabel("Výška (mm):"), 1, 0);
inputRow2.Controls.Add(MakeLabel("Dĺžka (mm):"), 2, 0);
inputRow2.Controls.Add(MakeLabel("Počet ks:"), 3, 0);
inputRow2.Controls.Add(nudNewW, 0, 1);
inputRow2.Controls.Add(nudNewH, 1, 1);
inputRow2.Controls.Add(nudNewD, 2, 1);
inputRow2.Controls.Add(nudNewCnt, 3, 1);
PushRow(itemsStack, inputRow2, ir++);
// Input row 3: Save-to-DB + Edit-DB + Refresh-DB
var inputRow3 = new TableLayoutPanel
{
Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 3,
Margin = new Padding(0, 2, 0, 2)
};
inputRow3.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50f));
inputRow3.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
inputRow3.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
btnSaveTemplate = MakeButton("💾 Uložiť kus do databázy", C(60, 90, 150), Color.White);
btnEditDb = MakeButton("📝 Spravovať DB", C(80, 60, 120), Color.White);
btnRefreshDb = MakeButton("🔄 Obnoviť", C(50, 60, 90), Color.White);
inputRow3.Controls.Add(btnSaveTemplate, 0, 0);
inputRow3.Controls.Add(btnEditDb, 1, 0);
inputRow3.Controls.Add(btnRefreshDb, 2, 0);
PushRow(itemsStack, inputRow3, ir++);
// ── event handlers ────────────────────────────────────────────────
// When user picks a name from the dropdown, auto-fill dimensions
cmbItemName.SelectedIndexChanged += (s, e) =>
{
if (cmbItemName.SelectedItem is ItemTemplate t)
{
nudNewW.Value = (decimal)Math.Min((double)nudNewW.Maximum, Math.Max((double)nudNewW.Minimum, t.Width));
nudNewH.Value = (decimal)Math.Min((double)nudNewH.Maximum, Math.Max((double)nudNewH.Minimum, t.Height));
nudNewD.Value = (decimal)Math.Min((double)nudNewD.Maximum, Math.Max((double)nudNewD.Minimum, t.Depth));
// Show clean name in the textbox (instead of the full template.ToString())
cmbItemName.Text = t.Name;
statusLabel.Text = $"Načítaný kus: {t.Name} ({t.Width}×{t.Height}×{t.Depth} mm)";
}
};
// When user types a name, try to find a matching DB template (on Leave)
cmbItemName.Leave += (s, e) =>
{
if (_itemDb == null) return;
var typed = cmbItemName.Text.Trim();
if (string.IsNullOrEmpty(typed)) return;
var found = _itemDb.FindByName(typed);
if (found != null)
{
nudNewW.Value = (decimal)found.Width;
nudNewH.Value = (decimal)found.Height;
nudNewD.Value = (decimal)found.Depth;
}
};
btnAddItem.Click += (s, e) =>
{
var nm = cmbItemName.Text.Trim();
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);
statusLabel.Text = $"Pridaný do zoznamu: {nm}";
};
btnRemoveItem.Click += (s, e) =>
{
if (dgvItems.SelectedRows.Count > 0)
dgvItems.Rows.Remove(dgvItems.SelectedRows[0]);
};
btnSaveTemplate.Click += (s, e) =>
{
if (_itemDb == null)
{
MessageBox.Show("Databáza nie je dostupná.", "Chyba");
return;
}
var nm = cmbItemName.Text.Trim();
if (string.IsNullOrEmpty(nm))
{
MessageBox.Show("Zadajte názov kusu.", "Upozornenie");
return;
}
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, "Duplikát", MessageBoxButtons.OK, MessageBoxIcon.Information);
statusLabel.Text = $"Neuložené - duplikát v DB";
return;
}
_itemDb.Upsert(newTemplate);
RefreshItemNameCombo();
cmbItemName.Text = nm;
statusLabel.Text = $"Uložené do DB: {nm}";
}
catch (Exception ex)
{
MessageBox.Show($"Chyba pri ukladaní do databázy:\n{ex.Message}",
"Chyba", MessageBoxButtons.OK, MessageBoxIcon.Error);
statusLabel.Text = $"Chyba pri ukladaní: {nm}";
}
};
btnEditDb.Click += (s, e) =>
{
if (_itemDb == null)
{
MessageBox.Show("Databáza nie je dostupná.", "Chyba");
return;
}
using (var editor = new ItemDatabaseEditor(_itemDb))
{
editor.ShowDialog(this);
RefreshItemNameCombo(); // Refresh after closing editor
statusLabel.Text = "Editor databázy zatvorený.";
}
};
btnRefreshDb.Click += (s, e) =>
{
RefreshItemNameCombo();
statusLabel.Text = "Databáza obnovená.";
};
grpItems.Controls.Add(itemsStack);
PushRow(stack, grpItems, r++);
// ── Suggest box size button ───────────────────────────────────────
btnSuggest = MakeButton("💡 Vypočítať odporúčanú veľkosť", C(25, 118, 210), Color.White);
btnSuggest.Font = new Font("Segoe UI", 9.5f, FontStyle.Bold);
btnSuggest.Height = 38; btnSuggest.Margin = new Padding(0, 2, 0, 2);
btnSuggest.Click += BtnSuggest_Click;
PushRow(stack, btnSuggest, r++);
// ── Pack button ───────────────────────────────────────────────────
btnPack = MakeButton("🚀 SPUSTIŤ BALENIE", C(30, 100, 200), Color.White);
btnPack.Font = new Font("Segoe UI", 11f, FontStyle.Bold);
btnPack.Height = 46; btnPack.Margin = new Padding(0, 4, 0, 4);
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
{
Dock = DockStyle.Fill, AutoSize = true,
ColumnCount = 1, Margin = new Padding(2)
};
resTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
lblPacked = MakeLabel("Zabalené: -", C(120, 220, 120));
lblTotal = MakeLabel("Celkom kusov: -", C(220, 180, 80));
lblEfficiency= MakeLabel("Využitie: -", C(100, 200, 255));
lblPacked.Font = lblTotal.Font = lblEfficiency.Font = new Font("Segoe UI", 9f, FontStyle.Bold);
resTbl.Controls.Add(lblPacked, 0, 0); resTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
resTbl.Controls.Add(lblTotal, 0, 1); resTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
resTbl.Controls.Add(lblEfficiency, 0, 2); resTbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
grpResult.Controls.Add(resTbl);
PushRow(stack, grpResult, r++);
// ── 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,
ColumnCount = 3, Margin = new Padding(2)
};
navTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
navTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 34f));
navTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
btnPrevBox = MakeButton("◀ Predošlá", C(60, 80, 120), Color.White);
btnPrevBox.Click += (s, e) => { if (_currentBoxIndex > 0) { _currentBoxIndex--; ShowCurrentBox(); } };
lblBoxNav = MakeLabel("Škatuľa: -", C(220, 220, 240));
lblBoxNav.Font = new Font("Segoe UI", 9.5f, FontStyle.Bold);
lblBoxNav.TextAlign = ContentAlignment.MiddleCenter;
lblBoxNav.Dock = DockStyle.Fill;
lblBoxNav.Margin = new Padding(0);
btnNextBox = MakeButton("Ďalšia ▶", C(60, 80, 120), Color.White);
btnNextBox.Click += (s, e) => { if (_currentBoxIndex < _solutions.Count - 1) { _currentBoxIndex++; ShowCurrentBox(); } };
navTbl.Controls.Add(btnPrevBox, 0, 0);
navTbl.Controls.Add(lblBoxNav, 1, 0);
navTbl.Controls.Add(btnNextBox, 2, 0);
grpNav.Controls.Add(navTbl);
PushRow(stack, grpNav, r++);
// ── Export ────────────────────────────────────────────────────────
var grpExport = MakeGroupBox("💾 Export");
var expTbl = new TableLayoutPanel
{
Dock = DockStyle.Fill, AutoSize = true,
ColumnCount = 3, Margin = new Padding(2)
};
expTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33));
expTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33));
expTbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 34));
btnExportPng = MakeButton("PNG", C(40, 80, 140), Color.White);
btnExportJpg = MakeButton("JPG", C(40, 100, 80), Color.White);
btnExportPdf = MakeButton("PDF (vrstvy)", C(140, 50, 30), Color.White);
btnExportPng.Click += (s, e) => ExportImage(ImageFormat.Png, "png");
btnExportJpg.Click += (s, e) => ExportImage(ImageFormat.Jpeg, "jpg");
btnExportPdf.Click += (s, e) => ExportPdf();
expTbl.Controls.Add(btnExportPng, 0, 0);
expTbl.Controls.Add(btnExportJpg, 1, 0);
expTbl.Controls.Add(btnExportPdf, 2, 0);
grpExport.Controls.Add(expTbl);
PushRow(stack, grpExport, r++);
// ── Clear ─────────────────────────────────────────────────────────
btnClear = MakeButton("🔄 Vymazať všetko", C(60, 40, 80), C(200, 180, 220));
btnClear.Margin = new Padding(0, 4, 0, 4);
btnClear.Click += (s, e) =>
{
dgvItems.Rows.Clear();
_solutions.Clear();
_currentBoxIndex = 0;
viewer.Clear();
glViewer?.Clear();
lblPacked.Text = "Zabalené: -";
lblTotal.Text = "Celkom kusov: -";
lblEfficiency.Text = "Využitie: -";
lblBoxNav.Text = "Škatuľa: -";
statusLabel.Text = "Vymazané";
};
PushRow(stack, btnClear, r++);
}
private void BuildRightPanel()
{
var pnlControls = new TableLayoutPanel
{
Dock = DockStyle.Bottom,
Height = 70, // increased from 58 to make room for labels below sliders
BackColor = C(20, 20, 30),
ColumnCount = 7,
Padding = new Padding(6, 8, 6, 8) // more vertical padding
};
pnlControls.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
pnlControls.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 28));
pnlControls.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
pnlControls.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 28));
pnlControls.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
pnlControls.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 24));
pnlControls.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 16));
panelRight.Controls.Add(pnlControls);
TrackBar MakeTrk(int min, int max, int val, int tick)
=> new TrackBar
{
Minimum = min, Maximum = max, Value = val,
BackColor = C(20, 20, 30),
Dock = DockStyle.Fill,
TickFrequency = tick
};
int initRotX = Math.Clamp(_settings.View.InitialRotationX, -90, 90);
int initRotY = Math.Clamp(_settings.View.InitialRotationY, -90, 90);
int initZoom = Math.Clamp(_settings.View.InitialZoomPercent, 10, 300);
lblRotX = new Label
{
Text = $"Rot X: {initRotX}°",
ForeColor = C(100, 200, 255),
AutoSize = true,
Anchor = AnchorStyles.Left | AnchorStyles.Top,
TextAlign = ContentAlignment.TopLeft,
Margin = new Padding(0, 0, 4, 0)
};
trkRotX = MakeTrk(-90, 90, initRotX, 15);
trkRotX.Scroll += (s, e) => { lblRotX.Text = $"Rot X: {trkRotX.Value}°"; Rerender(); };
lblRotY = new Label
{
Text = $"Rot Y: {initRotY}°",
ForeColor = C(100, 200, 255),
AutoSize = true,
Anchor = AnchorStyles.Left | AnchorStyles.Top,
TextAlign = ContentAlignment.TopLeft,
Margin = new Padding(4, 0, 4, 0)
};
trkRotY = MakeTrk(-90, 90, initRotY, 15);
trkRotY.Scroll += (s, e) => { lblRotY.Text = $"Rot Y: {trkRotY.Value}°"; Rerender(); };
lblZoom = new Label
{
Text = $"Zoom: {initZoom}%",
ForeColor = C(100, 255, 180),
AutoSize = true,
Anchor = AnchorStyles.Left | AnchorStyles.Top,
TextAlign = ContentAlignment.TopLeft,
Margin = new Padding(4, 0, 4, 0)
};
trkZoom = MakeTrk(10, 300, initZoom, 10);
trkZoom.Scroll += (s, e) => { lblZoom.Text = $"Zoom: {trkZoom.Value}%"; Rerender(); };
var lblHint = new Label
{
Text = "💡 Ťahaj myšou / koliesko zoom",
ForeColor = C(80, 80, 100),
AutoSize = false,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
Margin = new Padding(6, 0, 0, 0)
};
pnlControls.Controls.Add(lblRotX, 0, 0);
pnlControls.Controls.Add(trkRotX, 1, 0);
pnlControls.Controls.Add(lblRotY, 2, 0);
pnlControls.Controls.Add(trkRotY, 3, 0);
pnlControls.Controls.Add(lblZoom, 4, 0);
pnlControls.Controls.Add(trkZoom, 5, 0);
pnlControls.Controls.Add(lblHint, 6, 0);
panelViewer = new Panel { Dock = DockStyle.Fill, BackColor = C(18, 18, 28) };
panelRight.Controls.Add(panelViewer);
}
private void GlViewer_ViewChanged(object sender, GlViewer.ViewArgs e)
{
int rx = Math.Clamp((int)e.RotX, trkRotX.Minimum, trkRotX.Maximum);
int ry = Math.Clamp((int)e.RotY, trkRotY.Minimum, trkRotY.Maximum);
int zm = Math.Clamp((int)(e.Zoom * 100), trkZoom.Minimum, trkZoom.Maximum);
trkRotX.Value = rx; lblRotX.Text = $"Rot X: {rx}°";
trkRotY.Value = ry; lblRotY.Text = $"Rot Y: {ry}°";
trkZoom.Value = zm; lblZoom.Text = $"Zoom: {zm}%";
}
private void Rerender()
{
if (_solutions.Count == 0) return;
var sol = _solutions[_currentBoxIndex];
double wallPad = (double)nudWallPad.Value;
double itemPad = (double)nudItemPad.Value;
double rotX = trkRotX.Value, rotY = trkRotY.Value, zoom = trkZoom.Value / 100.0;
if (glViewer != null)
glViewer.Render(sol.PackedItems, sol.Box, rotX, rotY, zoom, wallPad, itemPad);
else
viewer?.Render(sol.PackedItems, sol.Box, rotX, rotY, zoom, wallPad, itemPad);
}
private void ShowCurrentBox()
{
if (_solutions.Count == 0) return;
lblBoxNav.Text = $"Škatuľa {_currentBoxIndex + 1} / {_solutions.Count}";
var sol = _solutions[_currentBoxIndex];
double outerVol = sol.Box.W * sol.Box.H * sol.Box.D;
double wt = sol.Box.WallThickness;
double innerVol = Math.Max(0, sol.Box.W - 2 * wt)
* Math.Max(0, sol.Box.H - 2 * wt)
* Math.Max(0, sol.Box.D - 2 * wt);
double usedVol = sol.PackedItems.Sum(p => p.RW * p.RH * p.RD);
double effOuter = outerVol > 0 ? usedVol / outerVol * 100.0 : 0;
double effInner = innerVol > 0 ? usedVol / innerVol * 100.0 : 0;
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)
{
MessageBox.Show("Pridajte aspoň jeden kus!", "Upozornenie",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
double wallPad = (double)nudWallPad.Value;
double itemPad = (double)nudItemPad.Value;
double wallThickness = (double)nudBoxWallThickness.Value;
double maxW = (double)nudMaxW.Value;
double maxH = (double)nudMaxH.Value;
double maxD = (double)nudMaxD.Value;
var colors = GenerateColors(dgvItems.Rows.Count);
double edge = wallPad + wallThickness;
double maxUsableW = maxW - 2 * edge;
double maxUsableH = maxH - 2 * edge;
double maxUsableD = maxD - 2 * edge;
if (maxUsableW <= 0 || maxUsableH <= 0 || maxUsableD <= 0)
{
MessageBox.Show(
$"Kombinácia izolácie ({wallPad} mm) a hrúbky steny ({wallThickness} mm) je príliš veľká.\n" +
$"Použiteľný priestor v maximálnej škatuli je záporný!\n\n" +
$"Použiteľný priestor: {maxUsableW:F0}×{maxUsableH:F0}×{maxUsableD:F0} mm\n" +
$"Znížte izoláciu alebo hrúbku steny.",
"Chyba konfigurácie", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// ── Parse and validate items ──────────────────────────────────────
var allItems = new List<Item>();
for (int i = 0; i < dgvItems.Rows.Count; i++)
{
var row = dgvItems.Rows[i];
string name = row.Cells["Name"].Value?.ToString() ?? $"Kus {i + 1}";
double w = Convert.ToDouble(row.Cells["W"].Value);
double h = Convert.ToDouble(row.Cells["H"].Value);
double d = Convert.ToDouble(row.Cells["D"].Value);
int cnt = Convert.ToInt32(row.Cells["Count"].Value);
if (w <= 0 || h <= 0 || d <= 0)
{
MessageBox.Show($"Kus '{name}' má neplatné rozmery ({w}×{h}×{d} mm).\nVšetky rozmery musia byť väčšie ako 0.",
"Chyba", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
bool fitsNormal = (w <= maxUsableW && h <= maxUsableH && d <= maxUsableD);
bool fitsRotated = (d <= maxUsableW && h <= maxUsableH && w <= maxUsableD);
if (!fitsNormal && !fitsRotated)
{
MessageBox.Show(
$"Kus '{name}' ({w}×{h}×{d} mm) sa nezmestí do maximálnej škatule.\n\n" +
$"Max použiteľný priestor: {maxUsableW:F0}×{maxUsableH:F0}×{maxUsableD:F0} mm\n" +
$"(po odpočítaní izolácie {wallPad} mm a steny {wallThickness} mm)\n\n" +
$"Zväčšite maximálne rozmery alebo zmenšite kus.",
"Kus sa nezmestí", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
for (int j = 0; j < cnt; j++) allItems.Add(new Item(name, w, h, d, colors[i]));
}
// ── Volume check — heads-up only, packing still proceeds ──────────
double totalItemVol = allItems.Sum(i => i.Volume);
double maxInnerVol = maxUsableW * maxUsableH * maxUsableD;
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();
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 (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
{
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);
}
ShowCurrentBox();
progressBar.Visible = false;
statusLabel.Text = $"Hotovo! {sol.PackedItems.Count}/{allItems.Count} kusov";
}
private (Box? box, List<PackedItem> packed, int totalCount) SuggestBoxSize(
List<Item> items, double maxW, double maxH, double maxD,
double wallPad, double itemPad, double wallThickness)
{
int totalCount = items.Count;
double minW = (double)nudBoxW.Value;
double minH = (double)nudBoxH.Value;
double minD = (double)nudBoxD.Value;
progressBar.Visible = true;
progressBar.Value = 0;
statusLabel.Text = "Hľadám optimálnu škatuľu...";
Application.DoEvents();
var minBox = new Box(minW, minH, minD, wallThickness);
var maxBox = new Box(maxW, maxH, maxD, wallThickness);
var (packed, box, unpacked) = CreatePacker().Pack(
items, minBox, maxBox, wallPad, itemPad, wallThickness);
progressBar.Visible = false;
if (packed.Count == 0)
return (null, new List<PackedItem>(), totalCount);
return (box, packed, totalCount);
}
private void BtnSuggest_Click(object? sender, EventArgs e)
{
if (dgvItems.Rows.Count == 0)
{
MessageBox.Show("Pridajte najprv kusy do zoznamu!", "Upozornenie",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var allItems = new List<Item>();
var colors = GenerateColors(dgvItems.Rows.Count);
for (int i = 0; i < dgvItems.Rows.Count; i++)
{
var row = dgvItems.Rows[i];
if (row.IsNewRow) continue;
var name = row.Cells[0].Value?.ToString() ?? "";
var w = Convert.ToDouble(row.Cells[1].Value ?? 0.0);
var h = Convert.ToDouble(row.Cells[2].Value ?? 0.0);
var d = Convert.ToDouble(row.Cells[3].Value ?? 0.0);
var count = Convert.ToInt32(row.Cells[4].Value ?? 1);
for (int j = 0; j < count; j++)
allItems.Add(new Item(name, w, h, d, colors[i]));
}
if (allItems.Count == 0)
{
MessageBox.Show("Pridajte najprv kusy do zoznamu!", "Upozornenie",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var maxW = (double)nudMaxW.Value;
var maxH = (double)nudMaxH.Value;
var maxD = (double)nudMaxD.Value;
var wallPad = (double)nudWallPad.Value;
var itemPad = (double)nudItemPad.Value;
var wallThickness = (double)nudBoxWallThickness.Value;
statusLabel.Text = "Počítam optimálnu veľkosť škatule...";
Application.DoEvents();
var (suggestedBox, sugPacked, sugTotalCount) = SuggestBoxSize(allItems, maxW, maxH, maxD, wallPad, itemPad, wallThickness);
if (suggestedBox == null)
{
MessageBox.Show(
$"Kusy sa nezmestia do maximálnej škatule ({maxW:F0}×{maxD:F0}×{maxH:F0} mm).\n\n" +
$"Skúste:\n" +
$"• Zväčšiť maximálne rozmery (📏 Max rozmery škatule)\n" +
$"• Zmenšiť hrúbku steny alebo stenovú/medzi-kusovú izoláciu\n" +
$"• Použiť viacero škatúľ (multi-box balenie)\n" +
$"• Odstrániť niektoré kusy",
"Nezmestí sa do maximálnej škatule", MessageBoxButtons.OK, MessageBoxIcon.Warning);
statusLabel.Text = "Nezmestí sa do maximálnej škatule";
return;
}
int sugPackedCount = sugPacked.Count;
string fitInfo = sugPackedCount >= sugTotalCount
? $"📦 {suggestedBox.W:F0} × {suggestedBox.D:F0} × {suggestedBox.H:F0} mm\n" +
$"(zmestí všetkých {sugTotalCount} kusov, minimálna veľkosť)"
: $"📦 {suggestedBox.W:F0} × {suggestedBox.D:F0} × {suggestedBox.H:F0} mm\n" +
$"(zmestí {sugPackedCount} z {sugTotalCount} kusov — maximum v daných rozmeroch)\n" +
$"Zvyšných {sugTotalCount - sugPackedCount} kusov bude potrebovať ďalšiu škatuľu.";
var dr = MessageBox.Show(
$"Pre {allItems.Count} kusov odporúčam škatuľu:\n\n" +
fitInfo + "\n\n" +
$"Min škatuľa: {nudBoxW.Value} × {nudBoxD.Value} × {nudBoxH.Value} mm\n" +
$"Maximum: {maxW:F0} × {maxD:F0} × {maxH:F0} mm\n\n" +
$"Chcete použiť odporúčanú veľkosť?",
"Odporúčaná veľkosť škatule",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (dr == DialogResult.Yes)
{
_solutions.Clear();
_unpackedItems.Clear();
_currentBoxIndex = 0;
_solutions.Add(new BoxSolution(suggestedBox, sugPacked));
var packedSet = new HashSet<Item>(sugPacked.Select(p => p.Item));
foreach (var item in allItems)
if (!packedSet.Contains(item)) _unpackedItems.Add(item);
UpdateResultLabels();
ShowCurrentBox();
statusLabel.Text = $"Hotovo! {sugPacked.Count}/{allItems.Count} kusov v škatuli {suggestedBox.W:F0}×{suggestedBox.D:F0}×{suggestedBox.H:F0} mm";
}
else
{
statusLabel.Text = "Odporúčaná veľkosť nezmenená";
}
}
/// <summary>
/// Reloads the item name dropdown from the database and configures autocomplete.
/// </summary>
private void RefreshItemNameCombo()
{
if (cmbItemName == null) return;
string current = cmbItemName.Text;
cmbItemName.Items.Clear();
if (_itemDb == null) return;
try
{
var templates = _itemDb.GetAll();
foreach (var t in templates) cmbItemName.Items.Add(t);
// Populate auto-complete source with bare names
var src = new AutoCompleteStringCollection();
foreach (var t in templates) src.Add(t.Name);
cmbItemName.AutoCompleteCustomSource = src;
cmbItemName.AutoCompleteSource = AutoCompleteSource.CustomSource;
cmbItemName.Text = current; // preserve user input
}
catch (Exception ex)
{
MessageBox.Show($"Chyba pri čítaní z databázy:\n{ex.Message}",
"Chyba", MessageBoxButtons.OK, MessageBoxIcon.Error);
statusLabel.Text = $"Chyba čítania DB: {ex.Message}";
}
}
private Color[] GenerateColors(int count)
{
var palette = new[] {
C(200,80,80), C(80,180,80), C(80,120,220), C(220,180,50), C(180,80,180),
C(80,200,200), C(230,130,50), C(100,200,150), C(200,100,150), C(150,150,220)
};
var r = new Color[count];
for (int i = 0; i < count; i++) r[i] = palette[i % palette.Length];
return r;
}
// ── export ────────────────────────────────────────────────────────────
private void ExportImage(ImageFormat fmt, string ext)
{
if (_solutions.Count == 0)
{ MessageBox.Show("Najprv spustite balenie!"); return; }
var sol = _solutions[_currentBoxIndex];
var dlg = new SaveFileDialog { Filter = $"{ext.ToUpper()}|*.{ext}", FileName = $"box{_currentBoxIndex + 1}.{ext}" };
if (dlg.ShowDialog() == DialogResult.OK)
{
viewer.RenderToBitmap(sol.PackedItems, sol.Box,
trkRotX.Value, trkRotY.Value, trkZoom.Value / 100.0, 1200, 800,
(double)nudWallPad.Value, (double)nudItemPad.Value).Save(dlg.FileName, fmt);
statusLabel.Text = $"Exportované: {dlg.FileName}";
MessageBox.Show($"Uložené:\n{dlg.FileName}", "Export úspešný");
}
}
private void ExportPdf()
{
if (_solutions.Count == 0)
{ MessageBox.Show("Najprv spustite balenie!"); return; }
var dlg = new SaveFileDialog { Filter = "PDF|*.pdf", FileName = "balenie_3d.pdf" };
if (dlg.ShowDialog() != DialogResult.OK) return;
statusLabel.Text = "Generujem PDF...";
Application.DoEvents();
double rotX = trkRotX.Value, rotY = trkRotY.Value, zoom = trkZoom.Value / 100.0;
double wallPad = (double)nudWallPad.Value, itemPad = (double)nudItemPad.Value;
var allLayerBitmaps = new List<Bitmap>();
var allLayers = new List<(double, double)>();
var allOverviews = new List<Bitmap>();
// For each box, generate layer + overview
foreach (var sol in _solutions)
{
var layers = Viewer3D.GetLayers(sol.PackedItems);
for (int layerIdx = 0; layerIdx < layers.Count; layerIdx++)
{
var layer = layers[layerIdx];
var bmp = viewer.RenderLayerToBitmap(
sol.PackedItems, sol.Box, rotX, rotY, zoom,
layer.YMin, layer.YMax,
layerIdx, layers.Count, 1000, 700, wallPad, itemPad, whiteBg: true);
allLayerBitmaps.Add(bmp);
allLayers.Add(layer);
}
var overview = viewer.RenderToBitmap(
sol.PackedItems, sol.Box, rotX, rotY, zoom, 1000, 700, wallPad, itemPad, whiteBg: true);
allOverviews.Add(overview);
}
PdfExporter.Export(dlg.FileName, _solutions, allOverviews, allLayerBitmaps, allLayers, wallPad, itemPad, _unpackedItems);
foreach (var b in allLayerBitmaps) b.Dispose();
foreach (var b in allOverviews) b.Dispose();
statusLabel.Text = $"PDF ({allLayerBitmaps.Count} vrstiev + {_solutions.Count} škatúľ)";
MessageBox.Show($"PDF uložené:\n{dlg.FileName}", "Export úspešný");
}
private void InitializeComponent()
{
this.SuspendLayout();
this.AutoScaleDimensions = new SizeF(96F, 96F);
this.AutoScaleMode = AutoScaleMode.Dpi;
this.ResumeLayout(false);
}
protected override void Dispose(bool disposing)
{
if (disposing) glViewer?.Dispose();
base.Dispose(disposing);
}
}
}