Files
2026-06-05 22:02:32 +02:00

1236 lines
58 KiB
C#
Raw Permalink 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;
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 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 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 TableLayoutPanel Make3ColNudRow(
string lbl1, NumericUpDown nud1,
string lbl2, NumericUpDown nud2,
string lbl3, NumericUpDown nud3)
{
var tbl = new TableLayoutPanel
{
Dock = DockStyle.Fill, AutoSize = true,
ColumnCount = 3, RowCount = 2, Margin = new Padding(2)
};
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33f));
tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 34f));
tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
tbl.Controls.Add(MakeLabel(lbl1), 0, 0);
tbl.Controls.Add(MakeLabel(lbl2), 1, 0);
tbl.Controls.Add(MakeLabel(lbl3), 2, 0);
tbl.Controls.Add(nud1, 0, 1);
tbl.Controls.Add(nud2, 1, 1);
tbl.Controls.Add(nud3, 2, 1);
return tbl;
}
private void BuildUI()
{
this.Text = "3D Box Packer";
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 box dimensions (mm) ───────────────────────────────────────
grpBox = MakeGroupBox("📦 Min 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);
// 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);
nudMaxH = MakeNUD((decimal)_settings.MaxBox.Height, 10);
nudMaxD = MakeNUD((decimal)_settings.MaxBox.Depth, 10);
// Validate box dimensions when max changes
nudMaxW.ValueChanged += (s, e) => {
nudBoxW.Maximum = nudMaxW.Value;
if (nudBoxW.Value > nudMaxW.Value) nudBoxW.Value = nudMaxW.Value;
};
nudMaxH.ValueChanged += (s, e) => {
nudBoxH.Maximum = nudMaxH.Value;
if (nudBoxH.Value > nudMaxH.Value) nudBoxH.Value = nudMaxH.Value;
};
nudMaxD.ValueChanged += (s, e) => {
nudBoxD.Maximum = nudMaxD.Value;
if (nudBoxD.Value > nudMaxD.Value) nudBoxD.Value = nudMaxD.Value;
};
grpMax.Controls.Add(Make3ColNudRow("Max šírka:", nudMaxW, "Max dĺžka:", nudMaxD, "Max výška:", nudMaxH));
PushRow(stack, grpMax, r++);
// ── Padding + wall thickness (mm) ────────────────────────────────
var grpPad = MakeGroupBox("🧱 Izolácia (mm)");
nudWallPad = MakeNUD((decimal)_settings.Padding.WallPadding, 0, 1000, 1, 5m);
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("Medikusová:"), 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
{
Dock = DockStyle.Fill, Height = 160,
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");
int ir = 0;
PushRow(itemsStack, dgvItems, ir++, SizeType.Absolute, 165);
// 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();
bool isCustomName = !string.IsNullOrEmpty(nm);
if (!isCustomName) nm = $"Kus {dgvItems.Rows.Count + 1}";
dgvItems.Rows.Add(nm, (int)nudNewW.Value, (int)nudNewH.Value, (int)nudNewD.Value, (int)nudNewCnt.Value);
// Auto-save to DB if user provided a custom name (not auto-generated)
if (isCustomName && _itemDb != null)
{
try
{
var newTemplate = new ItemTemplate
{
Name = nm,
Width = (double)nudNewW.Value,
Height = (double)nudNewH.Value,
Depth = (double)nudNewD.Value
};
// Check for duplicates
var duplicate = _itemDb.FindDuplicate(newTemplate);
if (duplicate != null)
{
string msg;
if (duplicate.Name.Equals(nm, StringComparison.OrdinalIgnoreCase))
msg = $"Kus s názvom \"{duplicate.Name}\" už existuje v databáze.\n\nRozmery: {duplicate.Width}×{duplicate.Height}×{duplicate.Depth} mm";
else
msg = $"Kus s rovnakými rozmermi ({duplicate.Width}×{duplicate.Height}×{duplicate.Depth} mm) už existuje v databáze pod názvom \"{duplicate.Name}\".";
MessageBox.Show($"{msg}\n\nKus bol pridaný do zoznamu, ale neuložený do databázy.",
"Duplikát", MessageBoxButtons.OK, MessageBoxIcon.Information);
statusLabel.Text = $"Pridaný do zoznamu: {nm} (duplikát v DB)";
}
else
{
_itemDb.Upsert(newTemplate);
RefreshItemNameCombo();
statusLabel.Text = $"Pridaný do zoznamu a uložený do DB: {nm}";
}
}
catch (Exception ex)
{
MessageBox.Show($"Kus pridaný do zoznamu, ale nepodarilo sa uložiť do DB:\n{ex.Message}",
"Upozornenie", MessageBoxButtons.OK, MessageBoxIcon.Warning);
statusLabel.Text = $"Pridaný do zoznamu: {nm} (chyba DB)";
}
}
else
{
statusLabel.Text = $"Pridaný do zoznamu: {nm}";
}
};
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++);
// ── 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 ────────────────────────────────────────────────
var grpNav = MakeGroupBox("📦 Navigácia škatúľ");
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();
}
// ── packing logic ─────────────────────────────────────────────────────
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]));
}
// ── Calculate number of boxes ─────────────────────────────────────
double totalItemVol = allItems.Sum(i => i.Volume);
double maxInnerVol = maxUsableW * maxUsableH * maxUsableD;
int boxCount = Math.Max(1, (int)Math.Ceiling(totalItemVol / maxInnerVol));
string confirmMsg =
$"Celkový objem kusov: {totalItemVol / 1000.0:F0} cm³\n" +
$"Max. objem škatule (bez iz.): {maxInnerVol / 1000.0:F0} cm³\n\n" +
$"Odhadovaný počet škatúľ: {boxCount}\n" +
$"(každý typ kusu zostane celý v jednej škatuli)\n\n" +
$"Pokračovať s balením do {boxCount} škatúľ?";
if (MessageBox.Show(confirmMsg, "Potvrdenie balenia",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
// ── Partition types into bins (LPT) ───────────────────────────────
var bins = PartitionTypesToBins(allItems, boxCount)
.Where(b => b.Count > 0).ToList();
_solutions.Clear();
_unpackedItems.Clear();
_currentBoxIndex = 0;
progressBar.Visible = true;
progressBar.Value = 0;
for (int binIdx = 0; binIdx < bins.Count; binIdx++)
{
var binItems = bins[binIdx];
int boxNum = binIdx + 1;
statusLabel.Text = $"Hľadám optimálnu škatuľu {boxNum}/{bins.Count}...";
Application.DoEvents();
double minW = (double)nudBoxW.Value;
double minH = (double)nudBoxH.Value;
double minD = (double)nudBoxD.Value;
statusLabel.Text = $"Balím škatuľu {boxNum}/{bins.Count}...";
Application.DoEvents();
var minBox = new Box(minW, minH, minD, wallThickness);
var maxBox2 = new Box(maxW, maxH, maxD, wallThickness);
var (packed, packBox, binUnpacked) = new LayerPacker().Pack(
binItems, minBox, maxBox2, wallPad, itemPad, wallThickness);
_solutions.Add(new BoxSolution(packBox, packed));
_unpackedItems.AddRange(binUnpacked);
}
// ── Update result labels ──────────────────────────────────────────
int totalPacked = _solutions.Sum(s => s.PackedItems.Count);
lblPacked.Text = $"Zabalené: {totalPacked} / {allItems.Count} kusov";
lblTotal.Text = $"Počet škatúľ: {_solutions.Count}";
if (_unpackedItems.Count > 0)
{
lblPacked.Text += $" | Nezabalené: {_unpackedItems.Count}";
lblPacked.ForeColor = Color.FromArgb(211, 47, 47);
MessageBox.Show(
$"{_unpackedItems.Count} kusov sa nepodarilo zabaliť.\n" +
$"Skúste zväčšiť maximálne rozmery škatule alebo znížiť izoláciu.",
"Nezabalené kusy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
lblPacked.ForeColor = Color.FromArgb(56, 142, 60);
}
if (_solutions.Count > 0) ShowCurrentBox();
else lblEfficiency.Text = "Využitie: -";
progressBar.Visible = false;
statusLabel.Text = $"Hotovo! {_solutions.Count} škatúľ, {totalPacked}/{allItems.Count} kusov";
}
/// <summary>
/// LPT (Longest Processing Time First) partitioning — assigns item types to N bins
/// so that bin volumes are as equal as possible. A type is never split across bins.
/// </summary>
private List<List<Item>> PartitionTypesToBins(List<Item> allItems, int N)
{
var typeGroups = allItems
.GroupBy(i => i.Name)
.Select(g => new { Items = g.ToList(), TotalVolume = g.Sum(i => i.Volume), MaxHeight = g.Max(i => i.H) })
.OrderByDescending(g => g.MaxHeight)
.ToList();
var bins = Enumerable.Range(0, N).Select(_ => new List<Item>()).ToList();
var loads = new double[N];
foreach (var tg in typeGroups)
{
int minBin = 0;
for (int i = 1; i < N; i++)
if (loads[i] < loads[minBin]) minBin = i;
bins[minBin].AddRange(tg.Items);
loads[minBin] += tg.TotalVolume;
}
return bins;
}
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) = new LayerPacker().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);
int totalPacked = sugPacked.Count;
lblPacked.Text = $"Zabalené: {totalPacked} / {allItems.Count} kusov";
lblTotal.Text = $"Počet škatúľ: 1";
lblPacked.ForeColor = _unpackedItems.Count > 0
? Color.FromArgb(211, 47, 47)
: Color.FromArgb(56, 142, 60);
if (_unpackedItems.Count > 0)
lblPacked.Text += $" | Nezabalené: {_unpackedItems.Count}";
ShowCurrentBox();
statusLabel.Text = $"Hotovo! {totalPacked}/{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);
}
}
}