1236 lines
58 KiB
C#
1236 lines
58 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|