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 PackedItems; public BoxSolution(Box box, List packed) { Box = box; PackedItems = packed; } } public partial class Form1 : Form { private AppSettings _settings; private ItemDatabase _itemDb; private List _solutions = new List(); private List _unpackedItems = new List(); // Track items that didn't fit private int _currentBoxIndex = 0; private List itemTypes = new List(); 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(); 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"; } /// /// 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. /// private List> PartitionTypesToBins(List 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()).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 packed, int totalCount) SuggestBoxSize( List 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(), 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(); 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(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á"; } } /// /// Reloads the item name dropdown from the database and configures autocomplete. /// 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(); var allLayers = new List<(double, double)>(); var allOverviews = new List(); // 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); } } }