diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..ad03d0a
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,36 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(dotnet build *)",
+ "Bash(Get-ChildItem -Path \"C:\\\\Users\\\\roman\\\\source\\\\repos\\\\BoxPacker3D\" -Recurse -File)",
+ "Bash(Select-Object -ExpandProperty FullName)",
+ "Bash(taskkill /PID 35736 /F)",
+ "Bash(del \"C:\\\\Users\\\\roman\\\\source\\\\repos\\\\BoxPacker3D\\\\LayerPacker.cs\")",
+ "Bash(find /mnt/c/Users/roman/source/repos/BoxPacker3D -name \"*.vdproj\" -o -name \"*.wixproj\" -o -name \"*.wxs\" -o -name \"*setup*\" -o -name \"*Setup*\" 2>/dev/null | head -40)",
+ "Read(//c/Users/roman/source/repos/**)",
+ "Bash(dir \"C:/Users/roman/source/repos/\")",
+ "Bash(where candle.exe)",
+ "Bash(where wix.exe)",
+ "Bash(dotnet tool *)",
+ "Read(//c/Program Files \\(x86\\)/**)",
+ "Read(//c/Program Files/**)",
+ "Bash(cmd /c \"build-wix.bat\")",
+ "Bash(wix extension *)",
+ "Bash(dotnet publish *)",
+ "Bash(grep -v \"^runtimes$\")",
+ "Bash(wix build *)",
+ "Bash(dotnet new *)",
+ "Bash(dotnet add *)",
+ "Bash(Remove-Item \"C:\\\\Users\\\\roman\\\\source\\\\repos\\\\DX\\\\DxViewer3D\\\\DxViewport3D.xaml\")",
+ "Bash(ls /mnt/c/Users/roman/source/repos)",
+ "Bash(ls ~/source/repos 2>/dev/null || ls ~/repos 2>/dev/null || echo \"not found\")",
+ "Bash(mkdir /c/Users/roman/source/repos/CalendarReminder)",
+ "Bash(mv /c/Users/roman/source/repos/CalendarReminder.sln /c/Users/roman/source/repos/CalendarReminder/)",
+ "Bash(dotnet sln *)",
+ "Bash(mkdir -p /c/Users/roman/source/repos/CalendarReminder/CalendarReminder.Domain/Entities)",
+ "Bash(mkdir -p /c/Users/roman/source/repos/CalendarReminder/CalendarReminder.Api/Data)",
+ "Bash(mkdir -p /c/Users/roman/source/repos/CalendarReminder/CalendarReminder.Api/Controllers)",
+ "Bash(taskkill /F /IM \"CalendarReminder.Api.exe\")"
+ ]
+ }
+}
diff --git a/AppSettings.cs b/AppSettings.cs
new file mode 100644
index 0000000..52ca931
--- /dev/null
+++ b/AppSettings.cs
@@ -0,0 +1,125 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace BoxPacker3D
+{
+ ///
+ /// Strongly-typed representation of appsettings.json.
+ /// Loaded once at startup via ; falls back to sensible defaults
+ /// if the file is missing or malformed.
+ ///
+ public class AppSettings
+ {
+ public DatabaseSettings Database { get; set; } = new();
+ public DefaultBoxSettings DefaultBox { get; set; } = new();
+ public MaxBoxSettings MaxBox { get; set; } = new();
+ public PaddingSettings Padding { get; set; } = new();
+ public ViewSettings View { get; set; } = new();
+ public UISettings UI { get; set; } = new();
+
+ public class DatabaseSettings
+ {
+ public string FilePath { get; set; } = "items.db";
+ }
+ public class DefaultBoxSettings
+ {
+ public double Width { get; set; } = 600;
+ public double Height { get; set; } = 400;
+ public double Depth { get; set; } = 500;
+ public double WallThickness { get; set; } = 20;
+ }
+ public class MaxBoxSettings
+ {
+ public double Width { get; set; } = 1200;
+ public double Height { get; set; } = 800;
+ public double Depth { get; set; } = 1000;
+ }
+ public class PaddingSettings
+ {
+ public double WallPadding { get; set; } = 10;
+ public double ItemPadding { get; set; } = 3;
+ }
+ public class ViewSettings
+ {
+ public int InitialRotationX { get; set; } = 30;
+ public int InitialRotationY { get; set; } = 30;
+ public int InitialZoomPercent { get; set; } = 100;
+ }
+ public class UISettings
+ {
+ public int WindowWidth { get; set; } = 1340;
+ public int WindowHeight { get; set; } = 840;
+ public int LeftPanelWidth { get; set; } = 500;
+ }
+
+ ///
+ /// Loads appsettings.json from the application directory.
+ /// If the file is missing or invalid, returns defaults and writes a warning to stderr.
+ ///
+ public static AppSettings Load()
+ {
+ string path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
+ if (!File.Exists(path))
+ {
+ Console.Error.WriteLine($"[AppSettings] {path} not found – using defaults.");
+ return new AppSettings();
+ }
+ try
+ {
+ var json = File.ReadAllText(path);
+ var opts = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true
+ };
+ return JsonSerializer.Deserialize(json, opts) ?? new AppSettings();
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[AppSettings] Failed to parse: {ex.Message} – using defaults.");
+ return new AppSettings();
+ }
+ }
+
+ ///
+ /// Resolves the writable database path.
+ ///
+ /// Priority:
+ /// 1. If FilePath is absolute → use it as-is (developer override).
+ /// 2. Otherwise → store in %AppData%\BoxPacker3D\<filename> so the
+ /// file is always user-writable, even when the app is installed in
+ /// Program Files (which is read-only for normal users).
+ ///
+ /// On first run the seed database is copied from the application directory
+ /// to AppData so the user starts with the pre-built item catalogue.
+ ///
+ public string ResolvedDatabasePath
+ {
+ get
+ {
+ if (Path.IsPathRooted(Database.FilePath))
+ return Database.FilePath;
+
+ // User-writable location: %AppData%\BoxPacker3D\items.db
+ string appDataDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "BoxPacker3D");
+ Directory.CreateDirectory(appDataDir);
+
+ string userDbPath = Path.Combine(appDataDir, Path.GetFileName(Database.FilePath));
+
+ // Copy seed database on first run (do not overwrite user's existing data)
+ if (!File.Exists(userDbPath))
+ {
+ string seedPath = Path.Combine(AppContext.BaseDirectory, Database.FilePath);
+ if (File.Exists(seedPath))
+ File.Copy(seedPath, userDbPath);
+ }
+
+ return userDbPath;
+ }
+ }
+ }
+}
diff --git a/BoxPacker3D.Setup.vdproj b/BoxPacker3D.Setup.vdproj
new file mode 100644
index 0000000..a3602ed
--- /dev/null
+++ b/BoxPacker3D.Setup.vdproj
@@ -0,0 +1,439 @@
+"DeployProject"
+{
+"VSVersion" = "3:800"
+"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}"
+"IsWebType" = "8:FALSE"
+"ProjectName" = "8:BoxPacker3D.Setup"
+"LanguageId" = "3:1033"
+"CodePage" = "3:1252"
+"UILanguageId" = "3:1033"
+"SccProjectName" = "8:"
+"SccLocalPath" = "8:"
+"SccAuxPath" = "8:"
+"SccProvider" = "8:"
+ "Hierarchy"
+ {
+ "Entry"
+ {
+ "MsmKey" = "8:_UNDEFINED"
+ "OwnerKey" = "8:_UNDEFINED"
+ "MsmSig" = "8:_UNDEFINED"
+ }
+ }
+ "Configurations"
+ {
+ "Debug"
+ {
+ "DisplayName" = "8:Debug"
+ "IsDebugOnly" = "11:TRUE"
+ "IsReleaseOnly" = "11:FALSE"
+ "OutputFilename" = "8:Debug\\BoxPacker3D.Setup.msi"
+ "PackageFilesAs" = "3:2"
+ "PackageFileSize" = "3:-2147483648"
+ "CabType" = "3:1"
+ "Compression" = "3:2"
+ "SignOutput" = "11:FALSE"
+ "CertificateFile" = "8:"
+ "PrivateKeyFile" = "8:"
+ "TimeStampServer" = "8:"
+ "InstallerBootstrapper" = "3:2"
+ "BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
+ {
+ "Enabled" = "11:TRUE"
+ "PromptEnabled" = "11:TRUE"
+ "PrerequisitesLocation" = "2:1"
+ "Url" = "8:"
+ "ComponentsUrl" = "8:"
+ }
+ }
+ "Release"
+ {
+ "DisplayName" = "8:Release"
+ "IsDebugOnly" = "11:FALSE"
+ "IsReleaseOnly" = "11:TRUE"
+ "OutputFilename" = "8:Release\\BoxPacker3D.Setup.msi"
+ "PackageFilesAs" = "3:2"
+ "PackageFileSize" = "3:-2147483648"
+ "CabType" = "3:1"
+ "Compression" = "3:2"
+ "SignOutput" = "11:FALSE"
+ "CertificateFile" = "8:"
+ "PrivateKeyFile" = "8:"
+ "TimeStampServer" = "8:"
+ "InstallerBootstrapper" = "3:2"
+ "BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
+ {
+ "Enabled" = "11:TRUE"
+ "PromptEnabled" = "11:TRUE"
+ "PrerequisitesLocation" = "2:1"
+ "Url" = "8:"
+ "ComponentsUrl" = "8:"
+ }
+ }
+ }
+ "Deployable"
+ {
+ "CustomAction"
+ {
+ }
+ "DefaultFeature"
+ {
+ "Name" = "8:DefaultFeature"
+ "Title" = "8:"
+ "Description" = "8:"
+ }
+ "ExternalPersistence"
+ {
+ "LaunchCondition"
+ {
+ "{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_71B7E62E9D384D1AAB5BFBF0E5E5E5E5"
+ {
+ "Name" = "8:.NET Framework"
+ "Message" = "8:[VSDNETMSG]"
+ "FrameworkVersion" = "8:.NETFramework,Version=v4.7.2"
+ "AllowLaterVersions" = "11:FALSE"
+ "InstallUrl" = "8:https://dotnet.microsoft.com/download/dotnet-framework"
+ }
+ }
+ }
+ "File"
+ {
+ }
+ "FileType"
+ {
+ }
+ "Folder"
+ {
+ "{1525181F-901A-416C-8A58-119130FE478E}:_0A5A98EC74D14C92BC1EC5F53FD93F6B"
+ {
+ "Name" = "8:#1916"
+ "AlwaysCreate" = "11:FALSE"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Property" = "8:DesktopFolder"
+ "Folders"
+ {
+ }
+ }
+ "{1525181F-901A-416C-8A58-119130FE478E}:_3FAD3295FBD0487AAC3DC0ECCED54A0E"
+ {
+ "Name" = "8:#1919"
+ "AlwaysCreate" = "11:FALSE"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Property" = "8:ProgramMenuFolder"
+ "Folders"
+ {
+ "{9EF0B969-E518-4E46-987F-47570745A589}:_F7B9BCD3D5154E8BA8F0E0DD8D3A1B2C"
+ {
+ "Name" = "8:3D Box Packer"
+ "AlwaysCreate" = "11:FALSE"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Property" = "8:_B7D3B1E0F7E14E8BA8F0E0DD8D3A1B2C"
+ "Folders"
+ {
+ }
+ }
+ }
+ }
+ "{3C67513D-01DD-4637-8A68-80971EB9504F}:_7E5CA5E1A9E84D3AAA48E5A2E5C5E5E5"
+ {
+ "DefaultLocation" = "8:[ProgramFilesFolder][Manufacturer]\\[ProductName]"
+ "Name" = "8:#1925"
+ "AlwaysCreate" = "11:FALSE"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Property" = "8:TARGETDIR"
+ "Folders"
+ {
+ }
+ }
+ }
+ "LaunchCondition"
+ {
+ }
+ "Locator"
+ {
+ }
+ "MsiBootstrapper"
+ {
+ "LangId" = "3:1033"
+ "RequiresElevation" = "11:FALSE"
+ }
+ "Product"
+ {
+ "Name" = "8:Microsoft Visual Studio"
+ "ProductName" = "8:3D Box Packer"
+ "ProductCode" = "8:{AD850FE7-45D6-4A75-BA48-14975E8E19D4}"
+ "PackageCode" = "8:{0DB6CFE3-A763-4935-B9A2-8FF88E8EAE76}"
+ "UpgradeCode" = "8:{73799267-8F2E-4994-AE60-A0CFA8390BEE}"
+ "AspNetVersion" = "8:"
+ "RestartWWWService" = "11:FALSE"
+ "RemovePreviousVersions" = "11:TRUE"
+ "DetectNewerInstalledVersion" = "11:TRUE"
+ "InstallAllUsers" = "11:FALSE"
+ "ProductVersion" = "8:1.0.1"
+ "Manufacturer" = "8:Your Company"
+ "ARPHELPTELEPHONE" = "8:"
+ "ARPHELPLINK" = "8:"
+ "Title" = "8:3D Box Packer"
+ "Subject" = "8:"
+ "ARPCONTACT" = "8:Your Company"
+ "Keywords" = "8:"
+ "ARPCOMMENTS" = "8:3D Box Packer - Aplikacia pre 3D balenie kusov do skatul"
+ "ARPURLINFOABOUT" = "8:"
+ "ARPPRODUCTICON" = "8:"
+ "ARPIconIndex" = "3:0"
+ "SearchPath" = "8:"
+ "UseSystemSearchPath" = "11:TRUE"
+ "TargetPlatform" = "3:1"
+ "PreBuildEvent" = "8:"
+ "PostBuildEvent" = "8:"
+ "RunPostBuildEvent" = "3:0"
+ }
+ "Registry"
+ {
+ "HKLM"
+ {
+ "Keys"
+ {
+ }
+ }
+ "HKCU"
+ {
+ "Keys"
+ {
+ }
+ }
+ "HKCR"
+ {
+ "Keys"
+ {
+ }
+ }
+ "HKU"
+ {
+ "Keys"
+ {
+ }
+ }
+ "HKPU"
+ {
+ "Keys"
+ {
+ }
+ }
+ }
+ "Sequences"
+ {
+ }
+ "Shortcut"
+ {
+ }
+ "UserInterface"
+ {
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_3E3A7D6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Name" = "8:#1902"
+ "Sequence" = "3:2"
+ "Attributes" = "3:3"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_1D8E5F6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Finished"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdAdminFinishedDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_4E3A7D6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "UseDynamicProperties" = "11:FALSE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdBasicDialogs.wim"
+ }
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_5E3A7D6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Name" = "8:#1901"
+ "Sequence" = "3:1"
+ "Attributes" = "3:2"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_2D8E5F6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Progress"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdAdminProgressDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "ShowProgress"
+ {
+ "Name" = "8:ShowProgress"
+ "DisplayName" = "8:#1009"
+ "Description" = "8:#1109"
+ "Type" = "3:5"
+ "ContextData" = "8:1;True=1;False=0"
+ "Attributes" = "3:0"
+ "Setting" = "3:0"
+ "Value" = "3:1"
+ "DefaultValue" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_6E3A7D6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Name" = "8:#1900"
+ "Sequence" = "3:1"
+ "Attributes" = "3:1"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_3D8E5F6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Welcome"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdWelcomeDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "CopyrightWarning"
+ {
+ "Name" = "8:CopyrightWarning"
+ "DisplayName" = "8:#1002"
+ "Description" = "8:#1102"
+ "Type" = "3:3"
+ "ContextData" = "8:"
+ "Attributes" = "3:0"
+ "Setting" = "3:1"
+ "Value" = "8:#1202"
+ "DefaultValue" = "8:#1202"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "Welcome"
+ {
+ "Name" = "8:Welcome"
+ "DisplayName" = "8:#1003"
+ "Description" = "8:#1103"
+ "Type" = "3:3"
+ "ContextData" = "8:"
+ "Attributes" = "3:0"
+ "Setting" = "3:1"
+ "Value" = "8:#1203"
+ "DefaultValue" = "8:#1203"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_4D8E5F6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Sequence" = "3:200"
+ "DisplayName" = "8:Installation Folder"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdFolderDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "InstallAllUsersVisible"
+ {
+ "Name" = "8:InstallAllUsersVisible"
+ "DisplayName" = "8:#1059"
+ "Description" = "8:#1159"
+ "Type" = "3:5"
+ "ContextData" = "8:1;True=1;False=0"
+ "Attributes" = "3:0"
+ "Setting" = "3:0"
+ "Value" = "3:1"
+ "DefaultValue" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_5D8E5F6E9C3F4D3AAA48E5A2E5C5E5E5"
+ {
+ "Sequence" = "3:300"
+ "DisplayName" = "8:Confirm Installation"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdConfirmDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ }
+ "MergeModule"
+ {
+ }
+ "ProjectOutput"
+ {
+ }
+ }
+}
diff --git a/BoxPacker3D.csproj b/BoxPacker3D.csproj
new file mode 100644
index 0000000..8bf6ba7
--- /dev/null
+++ b/BoxPacker3D.csproj
@@ -0,0 +1,35 @@
+
+
+
+ WinExe
+ net9.0-windows
+ enable
+ true
+ disable
+ BoxPacker3D
+ BoxPacker3D
+
+ false
+ app.manifest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/BoxPacker3D.wxs b/BoxPacker3D.wxs
new file mode 100644
index 0000000..f3dd609
--- /dev/null
+++ b/BoxPacker3D.wxs
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BoxPacker3D_WiX.wixproj b/BoxPacker3D_WiX.wixproj
new file mode 100644
index 0000000..9f89f35
--- /dev/null
+++ b/BoxPacker3D_WiX.wixproj
@@ -0,0 +1,41 @@
+
+
+
+ Debug
+ x86
+ 3.10
+ a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6
+ 2.0
+ BoxPacker3D.Setup
+ Package
+
+
+
+ bin\$(Configuration)\
+ obj\$(Configuration)\
+ Debug
+
+
+
+ bin\$(Configuration)\
+ obj\$(Configuration)\
+
+
+
+
+
+
+
+
+ $(WixExtDir)\WixUIExtension.dll
+ WixUIExtension
+
+
+
+
+
+
+
+
+
+
diff --git a/BoxPacker3D_WithSetup.sln b/BoxPacker3D_WithSetup.sln
new file mode 100644
index 0000000..72fe24a
--- /dev/null
+++ b/BoxPacker3D_WithSetup.sln
@@ -0,0 +1,27 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.3
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "BoxPacker3D.Setup", "BoxPacker3D.Setup.vdproj", "{4026F4D4-D1CE-4A3E-9B39-0083A159D2EC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BoxPacker3D", "BoxPacker3D.csproj", "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {4026F4D4-D1CE-4A3E-9B39-0083A159D2EC}.Debug|Any CPU.ActiveCfg = Debug
+ {4026F4D4-D1CE-4A3E-9B39-0083A159D2EC}.Debug|Any CPU.Build.0 = Debug
+ {4026F4D4-D1CE-4A3E-9B39-0083A159D2EC}.Release|Any CPU.ActiveCfg = Release
+ {4026F4D4-D1CE-4A3E-9B39-0083A159D2EC}.Release|Any CPU.Build.0 = Release
+ {8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Form1.cs b/Form1.cs
new file mode 100644
index 0000000..a10bef4
--- /dev/null
+++ b/Form1.cs
@@ -0,0 +1,1235 @@
+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);
+ }
+ }
+}
diff --git a/GlViewer.cs b/GlViewer.cs
new file mode 100644
index 0000000..17e1028
--- /dev/null
+++ b/GlViewer.cs
@@ -0,0 +1,576 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Windows.Forms;
+using OpenTK.Graphics.OpenGL;
+using OpenTK.Mathematics;
+using OpenTK.WinForms;
+
+namespace BoxPacker3D
+{
+ public class GlViewer : IDisposable
+ {
+ private GLControl _gl;
+ private bool _glReady = false;
+
+ private List _items = new();
+ private Box? _box = null;
+ private float _rotX = 30f, _rotY = 30f, _zoom = 1f;
+ private double _wallPad = 0, _itemPad = 0;
+
+ private bool _dragging;
+ private Point _lastMouse;
+
+ public GlViewer(Panel container)
+ {
+ var settings = new GLControlSettings
+ {
+ API = OpenTK.Windowing.Common.ContextAPI.OpenGL,
+ APIVersion = new Version(2, 1),
+ Profile = OpenTK.Windowing.Common.ContextProfile.Any,
+ Flags = OpenTK.Windowing.Common.ContextFlags.Default,
+ NumberOfSamples = 4
+ };
+
+ _gl = new GLControl(settings)
+ {
+ Dock = DockStyle.Fill,
+ BackColor = Color.FromArgb(18, 18, 28)
+ };
+
+ _gl.Load += OnLoad;
+ _gl.Paint += OnPaint;
+ _gl.Resize += OnResize;
+ _gl.MouseDown += OnMouseDown;
+ _gl.MouseMove += OnMouseMove;
+ _gl.MouseUp += OnMouseUp;
+ _gl.MouseWheel += OnMouseWheel;
+
+ container.Controls.Add(_gl);
+ }
+
+ public Control Control => _gl;
+
+ public void Render(List items, Box box,
+ double rotX, double rotY, double zoom,
+ double wallPad = 0, double itemPad = 0)
+ {
+ _items = items ?? new List();
+ _box = box;
+ _rotX = (float)rotX;
+ _rotY = (float)rotY;
+ _zoom = (float)Math.Max(0.1, zoom);
+ _wallPad = wallPad;
+ _itemPad = itemPad;
+ if (_glReady) _gl.Invalidate();
+ }
+
+ public void Clear()
+ {
+ _items = new List();
+ _box = null;
+ if (_glReady) _gl.Invalidate();
+ }
+
+ public void SetView(float rotX, float rotY, float zoom)
+ {
+ _rotX = rotX; _rotY = rotY; _zoom = zoom;
+ if (_glReady) _gl.Invalidate();
+ }
+
+ private void OnLoad(object? sender, EventArgs e)
+ {
+ _gl.MakeCurrent();
+
+ GL.ClearColor(0.071f, 0.071f, 0.110f, 1f);
+ GL.Enable(EnableCap.DepthTest);
+ GL.Enable(EnableCap.CullFace);
+ GL.CullFace(CullFaceMode.Back);
+ GL.ShadeModel(ShadingModel.Smooth);
+
+ GL.Enable(EnableCap.Lighting);
+ GL.Enable(EnableCap.Light0);
+ GL.Enable(EnableCap.ColorMaterial);
+ GL.ColorMaterial(MaterialFace.FrontAndBack, ColorMaterialParameter.AmbientAndDiffuse);
+
+ GL.Light(LightName.Light0, LightParameter.Position, new float[] { 1f, 2f, 1.5f, 0f });
+ GL.Light(LightName.Light0, LightParameter.Diffuse, new float[] { 1f, 1f, 1f, 1f });
+ GL.Light(LightName.Light0, LightParameter.Ambient, new float[] { 0.25f, 0.25f, 0.25f, 1f });
+ GL.Light(LightName.Light0, LightParameter.Specular, new float[] { 0.4f, 0.4f, 0.4f, 1f });
+
+ GL.Enable(EnableCap.Multisample);
+ GL.Enable(EnableCap.LineSmooth);
+ GL.Hint(HintTarget.LineSmoothHint, HintMode.Nicest);
+
+ // Set initial viewport immediately so first render is centered
+ GL.Viewport(0, 0, _gl.Width, _gl.Height);
+
+ _glReady = true;
+ _gl.Invalidate(); // Force first paint
+ }
+
+ private void OnResize(object? sender, EventArgs e)
+ {
+ if (!_glReady) return;
+ _gl.MakeCurrent();
+ GL.Viewport(0, 0, _gl.Width, _gl.Height);
+ _gl.Invalidate();
+ }
+
+ private void OnPaint(object? sender, PaintEventArgs e)
+ {
+ if (!_glReady) return;
+ _gl.MakeCurrent();
+
+ GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+
+ if (_box == null) { DrawWelcome(); _gl.SwapBuffers(); return; }
+
+ SetupProjection();
+ SetupModelView();
+
+ DrawBoxOutline(_box, alpha: 0.3f);
+ if (_box.WallThickness > 0.001) DrawWallThickness(_box);
+ if (_wallPad > 0.001) DrawPaddingOutline(_box, _wallPad);
+ DrawItems();
+ DrawBoxOutline(_box, alpha: 0.9f, frontOnly: true);
+ DrawAxesLabels();
+ if (_box != null) DrawDimensionLabelsGL();
+
+ _gl.SwapBuffers();
+ }
+
+ private void SetupProjection()
+ {
+ GL.MatrixMode(MatrixMode.Projection);
+ GL.LoadIdentity();
+ float aspect = _gl.Width > 0 && _gl.Height > 0
+ ? (float)_gl.Width / _gl.Height : 1f;
+
+ // Increase far plane to 50000 to handle large boxes at low zoom
+ // Near plane 1.0 (instead of 0.1) for better depth precision
+ var proj = Matrix4.CreatePerspectiveFieldOfView(
+ MathHelper.DegreesToRadians(45f), aspect, 1f, 50000f);
+ GL.LoadMatrix(ref proj);
+ }
+
+ private void SetupModelView()
+ {
+ GL.MatrixMode(MatrixMode.Modelview);
+ GL.LoadIdentity();
+
+ double maxDim = _box == null ? 100
+ : Math.Max(Math.Max(_box.W, _box.H), _box.D);
+ float dist = (float)(maxDim * 2.2 / _zoom);
+
+ GL.Translate(0f, 0f, -dist);
+ GL.Rotate(_rotX, 1f, 0f, 0f);
+ GL.Rotate(_rotY, 0f, 1f, 0f);
+ if (_box != null)
+ GL.Translate(-(float)(_box.W / 2), -(float)(_box.H / 2), -(float)(_box.D / 2));
+ }
+
+ private void DrawItems()
+ {
+ foreach (var pi in _items)
+ {
+ float r = pi.Item.Color.R / 255f;
+ float g = pi.Item.Color.G / 255f;
+ float b = pi.Item.Color.B / 255f;
+ DrawSolidBox(
+ (float)pi.X, (float)pi.Y, (float)pi.Z,
+ (float)pi.RW, (float)pi.RH, (float)pi.RD,
+ r, g, b, 1f);
+ }
+ }
+
+ private void DrawSolidBox(float x, float y, float z,
+ float w, float h, float d,
+ float r, float g, float b, float a)
+ {
+ GL.Color4(r, g, b, a);
+ GL.Begin(PrimitiveType.Quads);
+
+ GL.Normal3(0f, 0f, 1f);
+ GL.Vertex3(x, y, z + d);
+ GL.Vertex3(x + w, y, z + d);
+ GL.Vertex3(x + w, y + h, z + d);
+ GL.Vertex3(x, y + h, z + d);
+
+ GL.Normal3(0f, 0f, -1f);
+ GL.Vertex3(x + w, y, z);
+ GL.Vertex3(x, y, z);
+ GL.Vertex3(x, y + h, z);
+ GL.Vertex3(x + w, y + h, z);
+
+ GL.Normal3(-1f, 0f, 0f);
+ GL.Vertex3(x, y, z);
+ GL.Vertex3(x, y, z + d);
+ GL.Vertex3(x, y + h, z + d);
+ GL.Vertex3(x, y + h, z);
+
+ GL.Normal3(1f, 0f, 0f);
+ GL.Vertex3(x + w, y, z + d);
+ GL.Vertex3(x + w, y, z);
+ GL.Vertex3(x + w, y + h, z);
+ GL.Vertex3(x + w, y + h, z + d);
+
+ GL.Normal3(0f, -1f, 0f);
+ GL.Vertex3(x, y, z);
+ GL.Vertex3(x + w, y, z);
+ GL.Vertex3(x + w, y, z + d);
+ GL.Vertex3(x, y, z + d);
+
+ GL.Normal3(0f, 1f, 0f);
+ GL.Vertex3(x, y + h, z + d);
+ GL.Vertex3(x + w, y + h, z + d);
+ GL.Vertex3(x + w, y + h, z);
+ GL.Vertex3(x, y + h, z);
+
+ GL.End();
+
+ GL.Disable(EnableCap.Lighting);
+ GL.LineWidth(1.2f);
+ GL.Color4(0f, 0f, 0f, 0.55f);
+ DrawWireBox(x, y, z, w, h, d);
+ GL.Enable(EnableCap.Lighting);
+ }
+
+ private void DrawWireBox(float x, float y, float z, float w, float h, float d)
+ {
+ GL.Begin(PrimitiveType.LineLoop);
+ GL.Vertex3(x, y, z); GL.Vertex3(x + w, y, z);
+ GL.Vertex3(x + w, y + h, z); GL.Vertex3(x, y + h, z);
+ GL.End();
+ GL.Begin(PrimitiveType.LineLoop);
+ GL.Vertex3(x, y, z + d); GL.Vertex3(x + w, y, z + d);
+ GL.Vertex3(x + w, y + h, z + d); GL.Vertex3(x, y + h, z + d);
+ GL.End();
+ GL.Begin(PrimitiveType.Lines);
+ GL.Vertex3(x, y, z); GL.Vertex3(x, y, z + d);
+ GL.Vertex3(x + w, y, z); GL.Vertex3(x + w, y, z + d);
+ GL.Vertex3(x + w, y + h, z); GL.Vertex3(x + w, y + h, z + d);
+ GL.Vertex3(x, y + h, z); GL.Vertex3(x, y + h, z + d);
+ GL.End();
+ }
+
+ private void DrawBoxOutline(Box box, float alpha, bool frontOnly = false)
+ {
+ GL.Disable(EnableCap.Lighting);
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ GL.LineWidth(frontOnly ? 2f : 1f);
+ GL.LineStipple(1, 0x0F0F);
+ GL.Enable(EnableCap.LineStipple);
+ GL.Color4(0.39f, 0.78f, 1f, alpha);
+
+ DrawWireBox(0, 0, 0, (float)box.W, (float)box.H, (float)box.D);
+
+ GL.Disable(EnableCap.LineStipple);
+ GL.Disable(EnableCap.Blend);
+ GL.Enable(EnableCap.Lighting);
+ }
+
+ /// Draws the inner cardboard wall surface and corner connectors between outer/inner cubes.
+ /// Visually represents the thickness of the box material.
+ private void DrawWallThickness(Box box)
+ {
+ float t = (float)box.WallThickness;
+ float w = (float)box.W, h = (float)box.H, d = (float)box.D;
+ float iw = w - 2 * t, ih = h - 2 * t, id = d - 2 * t;
+ if (iw <= 0 || ih <= 0 || id <= 0) return;
+
+ GL.Disable(EnableCap.Lighting);
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+
+ // 1) Inner wire cube (solid line, brownish cardboard tone)
+ GL.LineWidth(1.5f);
+ GL.Color4(0.72f, 0.55f, 0.35f, 0.85f);
+ DrawWireBox(t, t, t, iw, ih, id);
+
+ // 2) Corner connectors – 8 lines from outer corner to inner corner
+ // (visualises the wall as a prism at each corner)
+ GL.LineWidth(1.0f);
+ GL.Color4(0.72f, 0.55f, 0.35f, 0.55f);
+ GL.Begin(PrimitiveType.Lines);
+ void Link(float ox, float oy, float oz, float ix, float iy, float iz)
+ { GL.Vertex3(ox, oy, oz); GL.Vertex3(ix, iy, iz); }
+
+ Link(0, 0, 0, t, t, t);
+ Link(w, 0, 0, w - t, t, t);
+ Link(w, h, 0, w - t, h - t, t);
+ Link(0, h, 0, t, h - t, t);
+ Link(0, 0, d, t, t, d - t);
+ Link(w, 0, d, w - t, t, d - t);
+ Link(w, h, d, w - t, h - t, d - t);
+ Link(0, h, d, t, h - t, d - t);
+ GL.End();
+
+ // 3) Subtle translucent shaded floor – shows where inner base begins
+ GL.Color4(0.72f, 0.55f, 0.35f, 0.08f);
+ GL.Begin(PrimitiveType.Quads);
+ GL.Vertex3(t, t, t);
+ GL.Vertex3(w - t, t, t);
+ GL.Vertex3(w - t, t, d - t);
+ GL.Vertex3(t, t, d - t);
+ GL.End();
+
+ GL.Disable(EnableCap.Blend);
+ GL.Enable(EnableCap.Lighting);
+ }
+
+ private void DrawPaddingOutline(Box box, double pad)
+ {
+ float edge = (float)(pad + box.WallThickness);
+ float w = (float)box.W - 2 * edge;
+ float h = (float)box.H - 2 * edge;
+ float d = (float)box.D - 2 * edge;
+ if (w <= 0 || h <= 0 || d <= 0) return;
+
+ GL.Disable(EnableCap.Lighting);
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ GL.LineWidth(1.2f);
+ GL.LineStipple(2, 0xAAAA);
+ GL.Enable(EnableCap.LineStipple);
+ GL.Color4(1f, 0.86f, 0.31f, 0.5f);
+
+ DrawWireBox(edge, edge, edge, w, h, d);
+
+ GL.Disable(EnableCap.LineStipple);
+ GL.Disable(EnableCap.Blend);
+ GL.Enable(EnableCap.Lighting);
+ }
+
+ private void DrawAxesLabels()
+ {
+ GL.Disable(EnableCap.Lighting);
+ GL.LineWidth(2f);
+
+ float aw = _box == null ? 10f : (float)(_box.W * 0.12f);
+
+ GL.Begin(PrimitiveType.Lines);
+ GL.Color3(1f, 0.3f, 0.3f); GL.Vertex3(0, 0, 0); GL.Vertex3(aw, 0, 0);
+ GL.Color3(0.3f, 1f, 0.3f); GL.Vertex3(0, 0, 0); GL.Vertex3(0, aw, 0);
+ GL.Color3(0.3f, 0.5f, 1f); GL.Vertex3(0, 0, 0); GL.Vertex3(0, 0, aw);
+ GL.End();
+
+ GL.Enable(EnableCap.Lighting);
+ }
+
+ private void DrawDimensionLabelsGL()
+ {
+ if (_box == null) return;
+ int sw = _gl.Width, sh = _gl.Height;
+
+ var labels = new (float wx, float wy, float wz, string text, float r, float g, float b, float ox, float oy)[]
+ {
+ ((float)(_box.W / 2), 0f, 0f, $"šírka {_box.W:F0}", 1f, 0.43f, 0.43f, 0f, 26f),
+ ((float)_box.W, 0f, (float)(_box.D/2), $"dĺžka {_box.D:F0}", 0.43f,0.51f, 1f, 26f, 8f),
+ ((float)_box.W, (float)(_box.H/2), 0f, $"výška {_box.H:F0}", 0.43f,0.9f, 0.43f, 30f, 0f),
+ };
+
+ foreach (var lbl in labels)
+ {
+ var pt = Project3DToScreen(lbl.wx, lbl.wy, lbl.wz);
+ if (pt == null) continue;
+ float sx = pt.Value.X + lbl.ox;
+ float sy = pt.Value.Y + lbl.oy;
+ DrawTextGL(lbl.text, sx, sy, sw, sh, lbl.r, lbl.g, lbl.b);
+ }
+ }
+
+ private void DrawTextGL(string text, float sx, float sy, int sw, int sh, float r, float g, float b)
+ {
+ using var bmp = new Bitmap(1, 1);
+ using var measure = Graphics.FromImage(bmp);
+ using var font = new Font("Segoe UI", 11f, FontStyle.Bold);
+ var sz = measure.MeasureString(text, font);
+ int tw = (int)sz.Width + 6, th = (int)sz.Height + 4;
+ if (tw <= 0 || th <= 0) return;
+
+ using var textBmp = new Bitmap(tw, th, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
+ using (var tg = Graphics.FromImage(textBmp))
+ {
+ tg.Clear(Color.FromArgb(0, 0, 0, 0));
+ using var bgBr = new SolidBrush(Color.FromArgb(180, 15, 15, 25));
+ tg.FillRectangle(bgBr, 0, 0, tw, th);
+ using var br = new SolidBrush(Color.FromArgb(255, (int)(r*255), (int)(g*255), (int)(b*255)));
+ tg.DrawString(text, font, br, 3, 2);
+ }
+
+ // flip vertically (OpenGL vs GDI+ Y axis)
+ textBmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
+
+ int tex = GL.GenTexture();
+ GL.BindTexture(TextureTarget.Texture2D, tex);
+ var data = textBmp.LockBits(
+ new Rectangle(0, 0, tw, th),
+ System.Drawing.Imaging.ImageLockMode.ReadOnly,
+ System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
+ GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
+ tw, th, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
+ textBmp.UnlockBits(data);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
+
+ // switch to 2D ortho
+ GL.Disable(EnableCap.DepthTest);
+ GL.Disable(EnableCap.Lighting);
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ GL.Enable(EnableCap.Texture2D);
+
+ GL.MatrixMode(MatrixMode.Projection);
+ GL.PushMatrix();
+ GL.LoadIdentity();
+ GL.Ortho(0, sw, 0, sh, -1, 1);
+ GL.MatrixMode(MatrixMode.Modelview);
+ GL.PushMatrix();
+ GL.LoadIdentity();
+
+ // screen Y is flipped: sy is from top, GL ortho is from bottom
+ float glY = sh - sy - th;
+ GL.Color4(1f, 1f, 1f, 1f);
+ GL.Begin(PrimitiveType.Quads);
+ GL.TexCoord2(0f, 0f); GL.Vertex2(sx, glY);
+ GL.TexCoord2(1f, 0f); GL.Vertex2(sx + tw, glY);
+ GL.TexCoord2(1f, 1f); GL.Vertex2(sx + tw, glY + th);
+ GL.TexCoord2(0f, 1f); GL.Vertex2(sx, glY + th);
+ GL.End();
+
+ GL.PopMatrix();
+ GL.MatrixMode(MatrixMode.Projection);
+ GL.PopMatrix();
+ GL.MatrixMode(MatrixMode.Modelview);
+
+ GL.Disable(EnableCap.Texture2D);
+ GL.Disable(EnableCap.Blend);
+ GL.Enable(EnableCap.DepthTest);
+ GL.Enable(EnableCap.Lighting);
+ GL.DeleteTexture(tex);
+ }
+
+ private PointF? Project3DToScreen(float wx, float wy, float wz)
+ {
+ if (_box == null) return null;
+ int sw = _gl.Width, sh = _gl.Height;
+ if (sw <= 0 || sh <= 0) return null;
+
+ double maxDim = Math.Max(Math.Max(_box.W, _box.H), _box.D);
+ float dist = (float)(maxDim * 2.2 / _zoom);
+ float aspect = (float)sw / sh;
+ float fovY = (float)(Math.PI / 4); // 45°
+
+ // translate to box center
+ float tx = wx - (float)(_box.W / 2);
+ float ty = wy - (float)(_box.H / 2);
+ float tz = wz - (float)(_box.D / 2);
+
+ // rotate Y
+ float radY = _rotY * (float)(Math.PI / 180);
+ float cosY = (float)Math.Cos(radY), sinY = (float)Math.Sin(radY);
+ float rx = tx * cosY + tz * sinY;
+ float ry = ty;
+ float rz = -tx * sinY + tz * cosY;
+
+ // rotate X
+ float radX = _rotX * (float)(Math.PI / 180);
+ float cosX = (float)Math.Cos(radX), sinX = (float)Math.Sin(radX);
+ float fx = rx;
+ float fy = ry * cosX - rz * sinX;
+ float fz = ry * sinX + rz * cosX - dist;
+
+ if (fz >= -0.1f) return null;
+
+ float f = 1f / (float)Math.Tan(fovY / 2);
+ float ndcX = f / aspect * fx / (-fz);
+ float ndcY = f * fy / (-fz);
+
+ return new PointF(
+ (ndcX + 1f) / 2f * sw,
+ (1f - ndcY) / 2f * sh);
+ }
+
+ private void DrawDimensionLabels(Graphics g)
+ {
+ if (_box == null) return;
+ using var font = new Font("Segoe UI", 11f, FontStyle.Bold);
+
+ var colW = Color.FromArgb(255, 255, 110, 110);
+ var colD = Color.FromArgb(255, 110, 140, 255);
+ var colH = Color.FromArgb(255, 110, 230, 110);
+
+ void Label(PointF? pt, string text, Color col, float ox, float oy)
+ {
+ if (pt == null) return;
+ var sz = g.MeasureString(text, font);
+ float tx = pt.Value.X + ox - sz.Width / 2;
+ float ty = pt.Value.Y + oy - sz.Height / 2;
+ using var bgBr = new SolidBrush(Color.FromArgb(170, 15, 15, 25));
+ g.FillRectangle(bgBr, tx - 3, ty - 1, sz.Width + 6, sz.Height + 2);
+ using var br = new SolidBrush(col);
+ g.DrawString(text, font, br, tx, ty);
+ }
+
+ // midpoints of three outer edges
+ Label(Project3DToScreen((float)(_box.W / 2), 0f, 0f),
+ $"šírka {_box.W:F0}", colW, 0, 22);
+ Label(Project3DToScreen((float)_box.W, 0f, (float)(_box.D / 2)),
+ $"dĺžka {_box.D:F0}", colD, 22, 8);
+ Label(Project3DToScreen((float)_box.W, (float)(_box.H / 2), 0f),
+ $"výška {_box.H:F0}", colH, 28, 0);
+ }
+
+ private void DrawWelcome()
+ {
+ GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+ }
+
+ private void OnMouseDown(object? sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left)
+ { _dragging = true; _lastMouse = e.Location; }
+ }
+
+ private void OnMouseMove(object? sender, MouseEventArgs e)
+ {
+ if (!_dragging) return;
+ float dx = e.X - _lastMouse.X;
+ float dy = e.Y - _lastMouse.Y;
+ _rotY += dx * 0.5f;
+ _rotX += dy * 0.5f;
+ _lastMouse = e.Location;
+ _gl.Invalidate();
+ ViewChanged?.Invoke(this, new ViewArgs(_rotX, _rotY, _zoom));
+ }
+
+ private void OnMouseUp(object? sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left) _dragging = false;
+ }
+
+ private void OnMouseWheel(object? sender, MouseEventArgs e)
+ {
+ _zoom *= e.Delta > 0 ? 1.1f : 0.9f;
+ _zoom = Math.Clamp(_zoom, 0.1f, 10f);
+ _gl.Invalidate();
+ ViewChanged?.Invoke(this, new ViewArgs(_rotX, _rotY, _zoom));
+ }
+
+ public event EventHandler? ViewChanged;
+
+ public class ViewArgs : EventArgs
+ {
+ public float RotX, RotY, Zoom;
+ public ViewArgs(float rx, float ry, float z) { RotX = rx; RotY = ry; Zoom = z; }
+ }
+
+ public void Dispose()
+ {
+ _gl.Dispose();
+ }
+ }
+}
diff --git a/INSTALLER_OPTIONS_COMPARISON.md b/INSTALLER_OPTIONS_COMPARISON.md
new file mode 100644
index 0000000..9610ba6
--- /dev/null
+++ b/INSTALLER_OPTIONS_COMPARISON.md
@@ -0,0 +1,168 @@
+# BoxPacker3D - Porovnanie Installer riešení
+
+V projekte máte **3 možnosti** ako vytvoriť inštalátor. Každá má svoje výhody a nevýhody.
+
+---
+
+## 📦 Možnosť 1: Visual Studio Installer Projects (.vdproj)
+
+### ✅ **Výhody**
+- **GUI Editor** - Všetko cez drag & drop vo Visual Studio
+- **Jednoduchosť** - Žiadne učenie syntax, len klikanie
+- **Integrácia** - Súčasť VS 2017 (po inštalácii rozšírenia)
+- **MSI výstup** - Windows natívny formát
+- **Automatizácia** - Upgrade management zabudovaný
+
+### ❌ **Nevýhody**
+- **Binárny súbor** - `.vdproj` nie je dobre verzovatný v Git
+- **Menej flexibilné** - Obmedzené možnosti customizácie
+- **Potrebuje VS** - Nedá sa buildnuť bez Visual Studio
+- **Zastarávajúce** - Microsoft ho aktívne nerozvíja
+
+### 📂 **Súbory v projekte**
+- `BoxPacker3D.Setup.vdproj` - Setup projekt (kostra - treba dotvoriť v VS)
+- `BoxPacker3D_WithSetup.sln` - Solution s Setup projektom
+- `VISUAL_STUDIO_INSTALLER_GUIDE.md` - Podrobný návod
+
+### 🚀 **Ako použiť**
+1. Otvorte `BoxPacker3D_WithSetup.sln` vo VS 2017
+2. Nainštalujte rozšírenie "Microsoft Visual Studio Installer Projects"
+3. V Setup projekte pridajte súbory cez GUI (Project Output)
+4. Build → MSI súbor v `BoxPacker3D.Setup\Release\`
+
+**Najlepšie pre:** Vývojárov, ktorí nechcú riešiť syntax a preferujú GUI
+
+---
+
+## 🔧 Možnosť 2: WiX Toolset (.wxs)
+
+### ✅ **Výhody**
+- **Úplná kontrola** - XML súbor, plne scriptovateľný
+- **Verzovatý** - Textový súbor, dokonalý pre Git
+- **Profesionálny** - Používa Microsoft, Adobe, atď.
+- **MSI výstup** - Windows natívny formát
+- **CI/CD ready** - Command-line build
+- **Kompletný** - Projekt je ready-to-build (na rozdiel od .vdproj)
+
+### ❌ **Nevýhody**
+- **Učenie** - XML syntax, treba sa naučiť WiX jazyk
+- **Komplexný** - Viac kódu než VS Installer Projects
+- **Inštalácia** - Treba stiahnuť WiX Toolset samostatne
+
+### 📂 **Súbory v projekte**
+- `BoxPacker3D.wxs` - **KOMPLETNÝ** WiX source (ready to build!)
+- `BoxPacker3D_WiX.wixproj` - WiX projekt súbor
+- `build-wix.bat` - Build script
+
+### 🚀 **Ako použiť**
+1. Stiahnite a nainštalujte [WiX Toolset 3.11+](https://wixtoolset.org/releases/)
+2. Spustite `build-wix.bat`
+3. MSI súbor: `bin\BoxPacker3D.msi`
+
+**Alebo ručne:**
+```batch
+dotnet build BoxPacker3D.csproj -c Release
+candle.exe BoxPacker3D.wxs -ext WixUIExtension
+light.exe BoxPacker3D.wixobj -out BoxPacker3D.msi -ext WixUIExtension
+```
+
+**Najlepšie pre:** Vývojárov, ktorí chcú plnú kontrolu a CI/CD integráciu
+
+---
+
+## 🎨 Možnosť 3: Inno Setup (.iss)
+
+### ✅ **Výhody**
+- **Zadarmo navždy** - Žiadne licenčné poplatky
+- **Jednoduchý** - Pascal-like skriptovací jazyk
+- **EXE výstup** - Samostatný inštalátor
+- **Malý** - Najmenšie súbory
+- **Flexibilný** - Custom stránky, skripty, akcie
+- **Verzovatý** - Textový `.iss` súbor
+
+### ❌ **Nevýhody**
+- **EXE, nie MSI** - Nie je Windows natívny (ale funguje rovnako dobre)
+- **Samostatný nástroj** - Treba stiahnuť Inno Setup
+- **Menej "corporate"** - MSI vyzerá profesionálnejšie v niektorých firmách
+
+### 📂 **Súbory v projekte**
+- `installer.iss.OLD` - Inno Setup script (odstráňte .OLD)
+- `build-installer-inno.bat.OLD` - Build script (odstráňte .OLD)
+- `INSTALLER_README_INNO.md.OLD` - Návod (odstráňte .OLD)
+
+### 🚀 **Ako použiť**
+1. Odstráňte `.OLD` príponu zo súborov
+2. Stiahnite [Inno Setup](https://jrsoftware.org/isdl.php)
+3. Otvorte `installer.iss` v Inno Setup Compiler
+4. Build → EXE súbor v `installer\BoxPacker3D_Setup.exe`
+
+**Najlepšie pre:** Malé projekty, shareware, open-source aplikácie
+
+---
+
+## 🤔 Ktorú možnosť si vybrať?
+
+### Preferujete **GUI** a máte **VS 2017**?
+→ **Visual Studio Installer Projects** (.vdproj)
+- Otvorte návod: `VISUAL_STUDIO_INSTALLER_GUIDE.md`
+
+### Chcete **plnú kontrolu** a **Git-friendly** riešenie?
+→ **WiX Toolset** (.wxs)
+- Spustite: `build-wix.bat` (po inštalácii WiX)
+- **Bonus:** WiX súbor je už kompletný, pripravený na build!
+
+### Chcete **najjednoduchšie** a **najmenšie** súbory?
+→ **Inno Setup** (.iss)
+- Odstráňte `.OLD` z `installer.iss.OLD`
+- Otvorte v Inno Setup Compiler
+
+---
+
+## 📊 Priame porovnanie
+
+| Feature | VS Installer | WiX | Inno Setup |
+|---------|-------------|-----|------------|
+| **Výstup** | MSI | MSI | EXE |
+| **GUI Editor** | ✅ Áno | ❌ Nie | ⚠️ Čiastočný |
+| **Textový súbor** | ❌ Nie | ✅ Áno | ✅ Áno |
+| **Git-friendly** | ❌ Nie | ✅ Áno | ✅ Áno |
+| **CI/CD** | ⚠️ Potrebuje VS | ✅ Áno | ✅ Áno |
+| **Komplexnosť** | 😊 Ľahké | 😐 Stredné | 😊 Ľahké |
+| **Flexibilita** | ⚠️ Obmedzená | ✅ Vysoká | ✅ Vysoká |
+| **Inštalácia** | VS rozšírenie | WiX Toolset | Inno Setup |
+| **Cena** | ✅ Zadarmo | ✅ Zadarmo | ✅ Zadarmo |
+| **Ready to build** | ❌ Nie* | ✅ Áno | ✅ Áno |
+
+*`.vdproj` je len kostra, treba dotvoriť v GUI
+
+---
+
+## 🎯 Moja odporúčanie
+
+### Pre **nových** používateľov:
+**Inno Setup** - Najjednoduchší, funguje okamžite, malé súbory
+
+### Pre **korporátne** prostredie:
+**WiX** - Profesionálny MSI, plná kontrola, CI/CD ready, **kompletný projekt už pripravený**
+
+### Pre **VS 2017** používateľov:
+**VS Installer Projects** - Ak už máte VS otvorené, GUI je pohodlné
+
+---
+
+## 📝 Poznámky
+
+- Všetky tri možnosti vytvárajú **rovnako funkčné** inštalátory
+- MSI (VS Installer, WiX) sa lepšie integruje do Windows systému
+- EXE (Inno Setup) je univerzálnejší a menší
+- **WiX súbor (`BoxPacker3D.wxs`) je už kompletný** - na rozdiel od .vdproj kotvry
+
+---
+
+## 🆘 Potrebujete pomoc?
+
+1. **VS Installer Projects** → `VISUAL_STUDIO_INSTALLER_GUIDE.md`
+2. **WiX** → Spustite `build-wix.bat` a sledujte chyby
+3. **Inno Setup** → `INSTALLER_README_INNO.md.OLD` (odstráňte .OLD)
+
+Akákoľvek metóda je dobrá - záleží len na vašich preferenciách! 🎉
diff --git a/ItemDatabase.cs b/ItemDatabase.cs
new file mode 100644
index 0000000..14993cb
--- /dev/null
+++ b/ItemDatabase.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Data.Sqlite;
+
+namespace BoxPacker3D
+{
+ ///
+ /// Persistent template of an item that can be re-used across packing sessions.
+ /// Stored in an on-disk SQLite database.
+ ///
+ public class ItemTemplate
+ {
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ public double Width { get; set; }
+ public double Height { get; set; }
+ public double Depth { get; set; }
+
+ public override string ToString() =>
+ $"{Name} ({Width}×{Height}×{Depth} mm)";
+ }
+
+ ///
+ /// Simple SQLite-backed repository for s.
+ /// Database schema is created on first use.
+ ///
+ public class ItemDatabase
+ {
+ private readonly string _connectionString;
+
+ public ItemDatabase(string dbPath)
+ {
+ // Ensure directory exists
+ var dir = Path.GetDirectoryName(dbPath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ _connectionString = new SqliteConnectionStringBuilder
+ {
+ DataSource = dbPath,
+ Mode = SqliteOpenMode.ReadWriteCreate
+ }.ToString();
+
+ EnsureSchema();
+ }
+
+ private void EnsureSchema()
+ {
+ using var conn = Open();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = @"
+ CREATE TABLE IF NOT EXISTS items (
+ Id INTEGER PRIMARY KEY AUTOINCREMENT,
+ Name TEXT NOT NULL UNIQUE COLLATE NOCASE,
+ Width REAL NOT NULL,
+ Height REAL NOT NULL,
+ Depth REAL NOT NULL
+ );";
+ cmd.ExecuteNonQuery();
+ }
+
+ private SqliteConnection Open()
+ {
+ var conn = new SqliteConnection(_connectionString);
+ conn.Open();
+ return conn;
+ }
+
+ /// Returns all templates ordered alphabetically by name.
+ public List GetAll()
+ {
+ var list = new List();
+ using var conn = Open();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT Id, Name, Width, Height, Depth FROM items ORDER BY Name COLLATE NOCASE;";
+ using var rdr = cmd.ExecuteReader();
+ while (rdr.Read())
+ list.Add(new ItemTemplate
+ {
+ Id = rdr.GetInt32(0),
+ Name = rdr.GetString(1),
+ Width = rdr.GetDouble(2),
+ Height = rdr.GetDouble(3),
+ Depth = rdr.GetDouble(4)
+ });
+ return list;
+ }
+
+ /// Returns the template with the given name or null if none exists.
+ public ItemTemplate? FindByName(string name)
+ {
+ using var conn = Open();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = "SELECT Id, Name, Width, Height, Depth FROM items WHERE Name = $n COLLATE NOCASE LIMIT 1;";
+ cmd.Parameters.AddWithValue("$n", name);
+ using var rdr = cmd.ExecuteReader();
+ if (rdr.Read())
+ return new ItemTemplate
+ {
+ Id = rdr.GetInt32(0),
+ Name = rdr.GetString(1),
+ Width = rdr.GetDouble(2),
+ Height = rdr.GetDouble(3),
+ Depth = rdr.GetDouble(4)
+ };
+ return null;
+ }
+
+ /// Inserts or replaces a template (upsert by Name).
+ public void Upsert(ItemTemplate item)
+ {
+ using var conn = Open();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = @"
+ INSERT INTO items (Name, Width, Height, Depth)
+ VALUES ($n, $w, $h, $d)
+ ON CONFLICT(Name) DO UPDATE SET
+ Width = excluded.Width,
+ Height = excluded.Height,
+ Depth = excluded.Depth;";
+ cmd.Parameters.AddWithValue("$n", item.Name);
+ cmd.Parameters.AddWithValue("$w", item.Width);
+ cmd.Parameters.AddWithValue("$h", item.Height);
+ cmd.Parameters.AddWithValue("$d", item.Depth);
+ cmd.ExecuteNonQuery();
+ }
+
+ public void DeleteByName(string name)
+ {
+ using var conn = Open();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = "DELETE FROM items WHERE Name = $n COLLATE NOCASE;";
+ cmd.Parameters.AddWithValue("$n", name);
+ cmd.ExecuteNonQuery();
+ }
+
+ ///
+ /// Checks if a template with the same name or exact dimensions already exists.
+ /// Returns the existing template if found, null otherwise.
+ ///
+ public ItemTemplate? FindDuplicate(ItemTemplate item)
+ {
+ using var conn = Open();
+ using var cmd = conn.CreateCommand();
+
+ // Check for same name (case-insensitive)
+ cmd.CommandText = "SELECT Id, Name, Width, Height, Depth FROM items WHERE Name = $n COLLATE NOCASE LIMIT 1;";
+ cmd.Parameters.AddWithValue("$n", item.Name);
+ using (var rdr = cmd.ExecuteReader())
+ {
+ if (rdr.Read())
+ {
+ return new ItemTemplate
+ {
+ Id = rdr.GetInt32(0),
+ Name = rdr.GetString(1),
+ Width = rdr.GetDouble(2),
+ Height = rdr.GetDouble(3),
+ Depth = rdr.GetDouble(4)
+ };
+ }
+ }
+
+ // Check for same dimensions (exact match)
+ cmd.CommandText = @"
+ SELECT Id, Name, Width, Height, Depth FROM items
+ WHERE Width = $w AND Height = $h AND Depth = $d
+ LIMIT 1;";
+ cmd.Parameters.Clear();
+ cmd.Parameters.AddWithValue("$w", item.Width);
+ cmd.Parameters.AddWithValue("$h", item.Height);
+ cmd.Parameters.AddWithValue("$d", item.Depth);
+ using (var rdr = cmd.ExecuteReader())
+ {
+ if (rdr.Read())
+ {
+ return new ItemTemplate
+ {
+ Id = rdr.GetInt32(0),
+ Name = rdr.GetString(1),
+ Width = rdr.GetDouble(2),
+ Height = rdr.GetDouble(3),
+ Depth = rdr.GetDouble(4)
+ };
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/ItemDatabaseEditor.cs b/ItemDatabaseEditor.cs
new file mode 100644
index 0000000..4cbd4ca
--- /dev/null
+++ b/ItemDatabaseEditor.cs
@@ -0,0 +1,472 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Windows.Forms;
+
+namespace BoxPacker3D
+{
+ ///
+ /// Modal dialog for managing the item template database.
+ /// Allows adding, editing, and deleting saved item templates.
+ ///
+ public class ItemDatabaseEditor : Form
+ {
+ private ItemDatabase _db;
+ private DataGridView _grid;
+ private TextBox _txtSearch;
+ private Button _btnAdd, _btnEdit, _btnDelete, _btnClose;
+ private List _allTemplates = new List();
+
+ public ItemDatabaseEditor(ItemDatabase db)
+ {
+ _db = db;
+ BuildUI();
+ LoadData();
+ }
+
+ private void BuildUI()
+ {
+ this.Text = "Správa databázy kusov";
+ this.Size = new Size(700, 500);
+ this.MinimumSize = new Size(600, 400);
+ this.StartPosition = FormStartPosition.CenterParent;
+ this.BackColor = Color.FromArgb(30, 30, 40);
+ this.AutoScaleMode = AutoScaleMode.Dpi;
+
+ // Bottom panel with Close button (add FIRST for proper docking)
+ var pnlBottom = new Panel
+ {
+ Dock = DockStyle.Bottom,
+ Height = 50,
+ BackColor = Color.FromArgb(25, 25, 35),
+ Padding = new Padding(10, 8, 10, 8)
+ };
+
+ _btnClose = MakeButton("✖ Zavrieť", Color.FromArgb(60, 60, 80));
+ _btnClose.Dock = DockStyle.Right;
+ _btnClose.Width = 120;
+ _btnClose.Click += (s, e) => this.Close();
+ pnlBottom.Controls.Add(_btnClose);
+ this.Controls.Add(pnlBottom);
+
+ // DataGridView (add SECOND so it fills remaining space)
+ _grid = new DataGridView
+ {
+ Dock = DockStyle.Fill,
+ BackgroundColor = Color.FromArgb(40, 40, 55),
+ GridColor = Color.FromArgb(60, 60, 80),
+ BorderStyle = BorderStyle.None,
+ RowHeadersVisible = false,
+ AllowUserToAddRows = false,
+ AllowUserToDeleteRows = false,
+ ReadOnly = true,
+ SelectionMode = DataGridViewSelectionMode.FullRowSelect,
+ MultiSelect = false,
+ AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
+ EnableHeadersVisualStyles = false
+ };
+
+ _grid.DefaultCellStyle.BackColor = Color.FromArgb(40, 40, 55);
+ _grid.DefaultCellStyle.ForeColor = Color.White;
+ _grid.DefaultCellStyle.SelectionBackColor = Color.FromArgb(60, 100, 160);
+ _grid.DefaultCellStyle.SelectionForeColor = Color.White;
+
+ _grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(30, 30, 45);
+ _grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(100, 200, 255);
+ _grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9f, FontStyle.Bold);
+
+ _grid.Columns.Add("Id", "ID");
+ _grid.Columns.Add("Name", "Názov");
+ _grid.Columns.Add("Width", "Šírka (mm)");
+ _grid.Columns.Add("Height", "Výška (mm)");
+ _grid.Columns.Add("Depth", "Dĺžka (mm)");
+
+ _grid.Columns[0].Width = 60; // ID column narrow
+
+ _grid.DoubleClick += (s, e) => BtnEdit_Click(s, e); // double-click = edit
+
+ this.Controls.Add(_grid);
+
+ // Search panel (add THIRD - will dock at top but below button panel)
+ var pnlSearch = new Panel
+ {
+ Dock = DockStyle.Top,
+ Height = 45,
+ BackColor = Color.FromArgb(30, 30, 40),
+ Padding = new Padding(10, 8, 10, 8)
+ };
+
+ var lblSearch = new Label
+ {
+ Text = "🔍 Hľadať:",
+ ForeColor = Color.FromArgb(180, 180, 200),
+ AutoSize = true,
+ Location = new Point(10, 13)
+ };
+
+ _txtSearch = new TextBox
+ {
+ BackColor = Color.FromArgb(45, 45, 60),
+ ForeColor = Color.White,
+ BorderStyle = BorderStyle.FixedSingle,
+ Font = new Font("Segoe UI", 10f),
+ Location = new Point(95, 10),
+ Width = 300
+ };
+ _txtSearch.TextChanged += (s, e) => ApplyFilter(_txtSearch.Text);
+
+ pnlSearch.Controls.Add(lblSearch);
+ pnlSearch.Controls.Add(_txtSearch);
+ this.Controls.Add(pnlSearch);
+
+ // Top panel with buttons (add LAST - will be at the very top)
+ var pnlTop = new Panel
+ {
+ Dock = DockStyle.Top,
+ Height = 50,
+ BackColor = Color.FromArgb(25, 25, 35),
+ Padding = new Padding(10, 8, 10, 8)
+ };
+
+ var flow = new FlowLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ FlowDirection = FlowDirection.LeftToRight,
+ WrapContents = false,
+ AutoSize = false
+ };
+
+ _btnAdd = MakeButton("➕ Pridať nový kus", Color.FromArgb(40, 120, 60));
+ _btnAdd.Click += BtnAdd_Click;
+
+ _btnEdit = MakeButton("✏️ Upraviť vybraný", Color.FromArgb(60, 100, 160));
+ _btnEdit.Click += BtnEdit_Click;
+
+ _btnDelete = MakeButton("🗑 Zmazať vybraný", Color.FromArgb(140, 40, 40));
+ _btnDelete.Click += BtnDelete_Click;
+
+ flow.Controls.Add(_btnAdd);
+ flow.Controls.Add(_btnEdit);
+ flow.Controls.Add(_btnDelete);
+ pnlTop.Controls.Add(flow);
+ this.Controls.Add(pnlTop);
+ }
+
+ private Button MakeButton(string text, Color bg)
+ {
+ return new Button
+ {
+ Text = text,
+ BackColor = bg,
+ ForeColor = Color.White,
+ FlatStyle = FlatStyle.Flat,
+ Font = new Font("Segoe UI", 9f, FontStyle.Bold),
+ Height = 34,
+ Width = 160,
+ Margin = new Padding(0, 0, 8, 0),
+ Cursor = Cursors.Hand
+ };
+ }
+
+ private void LoadData()
+ {
+ _grid.Rows.Clear();
+ _allTemplates.Clear();
+ if (_db == null) return;
+
+ try
+ {
+ _allTemplates = _db.GetAll();
+ ApplyFilter(_txtSearch?.Text ?? "");
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Chyba pri načítaní databázy:\n{ex.Message}", "Chyba",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ private void ApplyFilter(string searchText)
+ {
+ _grid.Rows.Clear();
+
+ var filtered = string.IsNullOrWhiteSpace(searchText)
+ ? _allTemplates
+ : _allTemplates.Where(t => t.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase)).ToList();
+
+ foreach (var t in filtered)
+ _grid.Rows.Add(t.Id, t.Name, t.Width, t.Height, t.Depth);
+ }
+
+ private void BtnAdd_Click(object sender, EventArgs e)
+ {
+ using (var dlg = new ItemEditDialog(null))
+ {
+ if (dlg.ShowDialog(this) == DialogResult.OK)
+ {
+ try
+ {
+ // Check for duplicates
+ var duplicate = _db.FindDuplicate(dlg.Template);
+ if (duplicate != null)
+ {
+ string msg;
+ if (duplicate.Name.Equals(dlg.Template.Name, 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);
+ return;
+ }
+
+ _db.Upsert(dlg.Template);
+ LoadData();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Chyba pri ukladaní:\n{ex.Message}", "Chyba",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+ }
+ }
+
+ private void BtnEdit_Click(object sender, EventArgs e)
+ {
+ if (_grid.SelectedRows.Count == 0)
+ {
+ MessageBox.Show("Vyberte kus na úpravu.", "Upozornenie",
+ MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ var row = _grid.SelectedRows[0];
+ var template = new ItemTemplate
+ {
+ Id = Convert.ToInt32(row.Cells[0].Value),
+ Name = row.Cells[1].Value?.ToString() ?? "",
+ Width = Convert.ToDouble(row.Cells[2].Value),
+ Height = Convert.ToDouble(row.Cells[3].Value),
+ Depth = Convert.ToDouble(row.Cells[4].Value)
+ };
+
+ using (var dlg = new ItemEditDialog(template))
+ {
+ if (dlg.ShowDialog(this) == DialogResult.OK)
+ {
+ try
+ {
+ // Check for duplicates (excluding self)
+ var duplicate = _db.FindDuplicate(dlg.Template);
+ if (duplicate != null && duplicate.Id != dlg.Template.Id)
+ {
+ string msg;
+ if (duplicate.Name.Equals(dlg.Template.Name, 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);
+ return;
+ }
+
+ _db.Upsert(dlg.Template);
+ LoadData();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Chyba pri ukladaní:\n{ex.Message}", "Chyba",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+ }
+ }
+
+ private void BtnDelete_Click(object sender, EventArgs e)
+ {
+ if (_grid.SelectedRows.Count == 0)
+ {
+ MessageBox.Show("Vyberte kus na zmazanie.", "Upozornenie",
+ MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ var row = _grid.SelectedRows[0];
+ string name = row.Cells[1].Value?.ToString() ?? "";
+
+ var dr = MessageBox.Show(
+ $"Naozaj zmazať kus \"{name}\" z databázy?\n\nTáto akcia je nevratná.",
+ "Potvrdenie zmazania",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Warning);
+
+ if (dr == DialogResult.Yes)
+ {
+ try
+ {
+ _db.DeleteByName(name);
+ LoadData();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Chyba pri mazaní:\n{ex.Message}", "Chyba",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Small dialog for adding/editing a single item template.
+ ///
+ internal class ItemEditDialog : Form
+ {
+ public ItemTemplate Template { get; private set; }
+
+ private TextBox _txtName;
+ private NumericUpDown _nudW, _nudH, _nudD;
+
+ public ItemEditDialog(ItemTemplate existing)
+ {
+ Template = existing ?? new ItemTemplate();
+ BuildUI();
+ }
+
+ private void BuildUI()
+ {
+ this.Text = Template.Id == 0 ? "Nový kus" : "Úprava kusu";
+ this.Size = new Size(400, 280);
+ this.StartPosition = FormStartPosition.CenterParent;
+ this.FormBorderStyle = FormBorderStyle.FixedDialog;
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.BackColor = Color.FromArgb(35, 35, 48);
+ this.AutoScaleMode = AutoScaleMode.Dpi;
+
+ var tbl = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 2,
+ RowCount = 5,
+ Padding = new Padding(15)
+ };
+ tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120));
+ tbl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
+ tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ tbl.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ tbl.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
+
+ tbl.Controls.Add(MakeLabel("Názov:"), 0, 0);
+ _txtName = new TextBox
+ {
+ Text = Template.Name,
+ Dock = DockStyle.Fill,
+ BackColor = Color.FromArgb(45, 45, 60),
+ ForeColor = Color.White,
+ BorderStyle = BorderStyle.FixedSingle,
+ Margin = new Padding(0, 3, 0, 8)
+ };
+ tbl.Controls.Add(_txtName, 1, 0);
+
+ tbl.Controls.Add(MakeLabel("Šírka (mm):"), 0, 1);
+ _nudW = MakeNUD(Template.Width);
+ tbl.Controls.Add(_nudW, 1, 1);
+
+ tbl.Controls.Add(MakeLabel("Výška (mm):"), 0, 2);
+ _nudH = MakeNUD(Template.Height);
+ tbl.Controls.Add(_nudH, 1, 2);
+
+ tbl.Controls.Add(MakeLabel("Dĺžka (mm):"), 0, 3);
+ _nudD = MakeNUD(Template.Depth);
+ tbl.Controls.Add(_nudD, 1, 3);
+
+ // Buttons
+ var pnlButtons = new FlowLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ FlowDirection = FlowDirection.RightToLeft,
+ Margin = new Padding(0, 10, 0, 0)
+ };
+
+ var btnOk = new Button
+ {
+ Text = "✔ Uložiť",
+ DialogResult = DialogResult.OK,
+ BackColor = Color.FromArgb(40, 120, 60),
+ ForeColor = Color.White,
+ FlatStyle = FlatStyle.Flat,
+ Font = new Font("Segoe UI", 9f, FontStyle.Bold),
+ Width = 100,
+ Height = 32
+ };
+ btnOk.Click += (s, e) =>
+ {
+ if (string.IsNullOrWhiteSpace(_txtName.Text))
+ {
+ MessageBox.Show("Názov nemôže byť prázdny.", "Chyba", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ this.DialogResult = DialogResult.None;
+ return;
+ }
+ Template.Name = _txtName.Text.Trim();
+ Template.Width = (double)_nudW.Value;
+ Template.Height = (double)_nudH.Value;
+ Template.Depth = (double)_nudD.Value;
+ };
+
+ var btnCancel = new Button
+ {
+ Text = "✖ Zrušiť",
+ DialogResult = DialogResult.Cancel,
+ BackColor = Color.FromArgb(80, 80, 100),
+ ForeColor = Color.White,
+ FlatStyle = FlatStyle.Flat,
+ Font = new Font("Segoe UI", 9f),
+ Width = 100,
+ Height = 32,
+ Margin = new Padding(0, 0, 8, 0)
+ };
+
+ pnlButtons.Controls.Add(btnOk);
+ pnlButtons.Controls.Add(btnCancel);
+ tbl.Controls.Add(pnlButtons, 0, 4);
+ tbl.SetColumnSpan(pnlButtons, 2);
+
+ this.Controls.Add(tbl);
+ this.AcceptButton = btnOk;
+ this.CancelButton = btnCancel;
+ }
+
+ private Label MakeLabel(string text)
+ {
+ return new Label
+ {
+ Text = text,
+ ForeColor = Color.FromArgb(180, 180, 200),
+ AutoSize = true,
+ Anchor = AnchorStyles.Left,
+ Margin = new Padding(0, 8, 8, 0)
+ };
+ }
+
+ private NumericUpDown MakeNUD(double val)
+ {
+ return new NumericUpDown
+ {
+ Value = (decimal)Math.Max(1, val),
+ Minimum = 1,
+ Maximum = 100000,
+ DecimalPlaces = 0,
+ Increment = 10,
+ BackColor = Color.FromArgb(45, 45, 60),
+ ForeColor = Color.White,
+ BorderStyle = BorderStyle.FixedSingle,
+ Dock = DockStyle.Fill,
+ Margin = new Padding(0, 3, 0, 8)
+ };
+ }
+ }
+}
diff --git a/LayerPacker.cs b/LayerPacker.cs
new file mode 100644
index 0000000..f645cec
--- /dev/null
+++ b/LayerPacker.cs
@@ -0,0 +1,314 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace BoxPacker3D
+{
+ ///
+ /// Layer-based packing algorithm.
+ ///
+ /// 1. Compute total padded volume: each item slot = (W+p)×(H+p)×(D+p), sum all types.
+ /// 2. t = paddedVol / maxInnerVol → estimate box via linear interpolation
+ /// dim(t) = dim_min + t*(dim_max – dim_min) for W, H, D
+ /// 3. Start with the type whose single item has the largest volume.
+ /// Items may only rotate around the vertical axis (W↔D), H is always preserved.
+ /// 4. Snap D: fit a whole number of items into estimated D.
+ /// 5. Snap W: fit a whole number of items into estimated W.
+ /// 6. Stack H: add as many layers upward as needed for all items of this type.
+ /// Cap at maxBox.H; excess items → unpacked.
+ /// 7. Subsequent types: add full horizontal layers on top (same W×D footprint).
+ /// If the type does not fill a complete layer in original orientation, try W↔D rotation.
+ /// 8. After every placement step, verify that no dimension exceeds maxBox.
+ /// 9. Tighten box so wall insulation touches items on all six sides.
+ /// 10. Items that do not fit → unpacked list (shown in PDF export).
+ ///
+ public class LayerPacker
+ {
+ private const double EPS = 0.001;
+
+ public (List packed, Box box, List
- unpacked) Pack(
+ List
- allItems,
+ Box minBox, Box maxBox,
+ double wallPad, double itemPad, double wallThickness)
+ {
+ var packed = new List();
+ var unpacked = new List
- ();
+ double edge = wallPad + wallThickness;
+
+ if (allItems.Count == 0)
+ return (packed, minBox, unpacked);
+
+ // ── Step 1: total padded volume ───────────────────────────────────────
+ // Each item occupies (W+p)×(H+p)×(D+p) — padding is shared between
+ // neighbours so only one layer per side is counted.
+ double totalPaddedVol = allItems.Sum(i =>
+ (i.W + itemPad) * (i.H + itemPad) * (i.D + itemPad));
+
+ // ── Step 2: estimated box size via linear interpolation ───────────────
+ // t = ratio of needed inner volume to maximum available inner volume
+ double maxIW = Math.Max(EPS, maxBox.W - 2 * edge);
+ double maxIH = Math.Max(EPS, maxBox.H - 2 * edge);
+ double maxID = Math.Max(EPS, maxBox.D - 2 * edge);
+ double maxInnerVol = maxIW * maxIH * maxID;
+
+ double t = Math.Min(1.0, Math.Max(0.0, totalPaddedVol / maxInnerVol));
+
+ // a(t) = a_min + t*(a_max – a_min)
+ double estW = minBox.W + t * (maxBox.W - minBox.W);
+ double estH = minBox.H + t * (maxBox.H - minBox.H);
+ double estD = minBox.D + t * (maxBox.D - minBox.D);
+
+ // Inner (usable) dimensions from the estimate
+ double estIW = Math.Max(0, estW);
+ double estIH = Math.Max(0, estH);
+ double estID = Math.Max(0, estD);
+
+ // ── Step 3: sort types by single-item volume descending ───────────────
+ var types = allItems
+ .GroupBy(i => i.Name)
+ .Select(g => (Proto: g.First(), Items: g.ToList()))
+ .OrderByDescending(g => g.Proto.Volume)
+ .ToList();
+
+ // ── Steps 4–6: place first type ───────────────────────────────────────
+ var (proto0, items0) = types[0];
+
+ bool firstPlaced = false;
+ double extW = 0, extH = 0, extD = 0;
+
+ // Open (incomplete) last layer state
+ double openLayerY = double.NaN;
+ int openFilled = 0; // how many slots are already occupied
+ int openCapacity = 0; // total slots in that layer (nW×nD)
+ double openIW = 0, openIH = 0, openID = 0;
+ int openNW = 0, openND = 0;
+
+ foreach (var (iW, iH, iD) in Orientations(proto0))
+ {
+ // Step 4: snap D to whole number of items (based on estimated D)
+ int nD = Math.Max(1, (int)((estID + itemPad) / (iD + itemPad)));
+ nD = Math.Min(nD, (int)((maxID + itemPad) / (iD + itemPad)));
+ if (nD < 1) continue;
+
+ // Step 5: snap W to whole number of items (based on estimated W)
+ int nW = Math.Max(1, (int)((estIW + itemPad) / (iW + itemPad)));
+ nW = Math.Min(nW, (int)((maxIW + itemPad) / (iW + itemPad)));
+ if (nW < 1) continue;
+
+ // Step 6: H layers needed for all items; cap at maxBox.H
+ int nH = (int)Math.Ceiling((double)items0.Count / (nD * nW));
+ nH = Math.Min(nH, (int)((maxIH + itemPad) / (iH + itemPad)));
+ if (nH < 1) continue;
+
+ // Place items layer by layer from bottom to top
+ int idx = 0;
+ for (int iy = 0; iy < nH && idx < items0.Count; iy++)
+ for (int iz = 0; iz < nD && idx < items0.Count; iz++)
+ for (int ix = 0; ix < nW && idx < items0.Count; ix++)
+ {
+ packed.Add(new PackedItem(items0[idx++],
+ edge + ix * (iW + itemPad),
+ edge + iy * (iH + itemPad),
+ edge + iz * (iD + itemPad),
+ iW, iH, iD));
+ }
+
+ // Items that didn't fit (nH was capped) → unpacked
+ for (int i = idx; i < items0.Count; i++)
+ unpacked.Add(items0[i]);
+
+ extW = nW * (iW + itemPad) - itemPad;
+ extH = nH * (iH + itemPad) - itemPad;
+ extD = nD * (iD + itemPad) - itemPad;
+
+ // Track open last layer if it was not completely filled.
+ // lastFilled == 0 when: (a) last layer is perfectly full, OR
+ // (b) nH was capped so idx = nH*perLayer0 (exact multiple) — in
+ // that case the last placed layer IS full, nothing to fill.
+ // In both cases there is no open layer → openLayerY stays NaN.
+ int perLayer0 = nW * nD;
+ int lastFilled = idx % perLayer0;
+ if (lastFilled > 0)
+ {
+ openLayerY = edge + (nH - 1) * (iH + itemPad);
+ openFilled = lastFilled;
+ openCapacity = perLayer0;
+ openIW = iW; openIH = iH; openID = iD;
+ openNW = nW; openND = nD;
+ }
+
+ Debug.WriteLine(
+ $"[LP] Type0 nW={nW} nD={nD} nH={nH} perLayer={perLayer0} " +
+ $"idx={idx} lastFilled={lastFilled} " +
+ $"openLayerY={openLayerY:F1} openIW={openIW} openIH={openIH} openID={openID}");
+
+ Debug.WriteLine($"[LayerPacker] Type0: nW={nW} nD={nD} nH={nH} perLayer={nW*nD} idx={idx} lastFilled={idx%(nW*nD)} openLayerY={openLayerY:F1} openIW={openIW} openIH={openIH} openID={openID}");
+
+ firstPlaced = true;
+ break;
+ }
+
+ if (!firstPlaced)
+ {
+ unpacked.AddRange(allItems);
+ return (packed, minBox, unpacked);
+ }
+
+ // ── Step 7: subsequent types — fill open layer then full layers on top ─
+ for (int ti = 1; ti < types.Count; ti++)
+ {
+ var (proto, typeItems) = types[ti];
+ var queue = new Queue
- (typeItems);
+
+ // ── 7a: fill the open (incomplete) last layer of the previous type ─
+ Debug.WriteLine(
+ $"[LP] Type{ti} '{proto.Name}': openLayerY={openLayerY:F1} " +
+ $"openFilled={openFilled} openCap={openCapacity} " +
+ $"openIW={openIW} openIH={openIH} openID={openID}");
+ if (!double.IsNaN(openLayerY) && queue.Count > 0)
+ {
+ int ixPartial = openFilled % openNW;
+ int izPartial = openFilled / openNW;
+
+ // Region A: X-remainder of the partially-filled Z-row.
+ // Items must fit within openID depth to avoid overlapping Region B.
+ if (ixPartial > 0)
+ {
+ double aXBase = edge + ixPartial * (openIW + itemPad);
+ double aZBase = edge + izPartial * (openID + itemPad);
+ FillRectangle(queue, packed,
+ aXBase, aZBase,
+ extW - ixPartial * (openIW + itemPad), openID,
+ openLayerY, openIH, itemPad, proto);
+ }
+
+ // Region B: completely free Z-rows (uses both orientations via FillRectangle).
+ int izFree = izPartial + (ixPartial > 0 ? 1 : 0);
+ double bZOff = izFree * (openID + itemPad);
+ double bRectD = extD - bZOff;
+ if (bRectD > EPS)
+ FillRectangle(queue, packed,
+ edge, edge + bZOff, extW, bRectD,
+ openLayerY, openIH, itemPad, proto);
+
+ openLayerY = double.NaN;
+ }
+
+ // ── 7b: add layers on top, filling each layer with both orientations ─
+ foreach (var (iW, iH, iD) in Orientations(proto))
+ {
+ if (queue.Count == 0) break;
+
+ // Step 8: check remaining height
+ double availH = maxBox.H - 2 * edge - extH;
+ int nHavail = (int)((availH + itemPad) / (iH + itemPad));
+ if (nHavail < 1) continue;
+
+ // Verify at least one item fits per layer in this orientation
+ if ((int)((extW + itemPad) / (iW + itemPad)) < 1) continue;
+ if ((int)((extD + itemPad) / (iD + itemPad)) < 1) continue;
+
+ while (queue.Count > 0 && nHavail > 0)
+ {
+ int before = queue.Count;
+ double y0 = edge + extH + itemPad;
+ // Fill the full extW×extD footprint using both orientations.
+ // FillRectangle recurses into remaining strips with rotated items.
+ FillRectangle(queue, packed, edge, edge, extW, extD,
+ y0, iH, itemPad, proto);
+ if (queue.Count == before) break; // items too large for remaining space
+ extH += iH + itemPad;
+ nHavail--;
+ }
+
+ openLayerY = double.NaN;
+ break;
+ }
+
+ // Leftover items → unpacked
+ unpacked.AddRange(queue);
+ }
+
+ // ── Step 9: tighten box ───────────────────────────────────────────────
+ var box = TightenBox(packed, edge, minBox, maxBox, wallThickness);
+ return (packed, box, unpacked);
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────────────
+
+ ///
+ /// Fills a rectangular horizontal region (xBase,zBase)+(rectW×rectD) at height y
+ /// with items from queue, trying both W/D orientations. After placing the primary
+ /// grid the remaining two strips (Z-remainder and X-remainder) are filled recursively,
+ /// so rotated items can be used to fill gaps left by the primary orientation.
+ /// layerH caps the maximum item height that may be placed.
+ ///
+ private static void FillRectangle(
+ Queue
- queue, List packed,
+ double xBase, double zBase, double rectW, double rectD,
+ double y, double layerH, double itemPad, Item proto)
+ {
+ if (queue.Count == 0 || rectW < EPS || rectD < EPS) return;
+
+ foreach (var (iW, iH, iD) in Orientations(proto))
+ {
+ if (iH > layerH + EPS) continue;
+
+ int nW = (int)((rectW + itemPad) / (iW + itemPad));
+ int nD = (int)((rectD + itemPad) / (iD + itemPad));
+ if (nW < 1 || nD < 1) continue;
+
+ for (int iz = 0; iz < nD && queue.Count > 0; iz++)
+ for (int ix = 0; ix < nW && queue.Count > 0; ix++)
+ packed.Add(new PackedItem(queue.Dequeue(),
+ xBase + ix * (iW + itemPad),
+ y,
+ zBase + iz * (iD + itemPad),
+ iW, iH, iD));
+
+ // Z-strip: depth remaining after primary grid rows
+ double zRemain = rectD - nD * (iD + itemPad);
+ if (zRemain > EPS && queue.Count > 0)
+ FillRectangle(queue, packed,
+ xBase, zBase + nD * (iD + itemPad), rectW, zRemain,
+ y, layerH, itemPad, proto);
+
+ // X-strip: width remaining alongside primary grid's Z range
+ double xRemain = rectW - nW * (iW + itemPad);
+ if (xRemain > EPS && queue.Count > 0)
+ FillRectangle(queue, packed,
+ xBase + nW * (iW + itemPad), zBase,
+ xRemain, nD * (iD + itemPad),
+ y, layerH, itemPad, proto);
+
+ break; // orientation chosen — done
+ }
+ }
+
+ ///
+ /// Two allowed orientations: original and rotated 90° around vertical axis (W↔D).
+ /// H is always preserved — items cannot be tilted sideways.
+ ///
+ private static IEnumerable<(double iW, double iH, double iD)> Orientations(Item proto)
+ {
+ yield return (proto.W, proto.H, proto.D);
+ if (Math.Abs(proto.W - proto.D) > EPS)
+ yield return (proto.D, proto.H, proto.W);
+ }
+
+ ///
+ /// Shrinks the box to just enclose all packed items plus edge on every side.
+ ///
+ private static Box TightenBox(
+ List packed, double edge,
+ Box minBox, Box maxBox, double wallThickness)
+ {
+ if (packed.Count == 0) return minBox;
+ double tw = Math.Clamp(Math.Ceiling(packed.Max(p => p.X + p.RW) + edge), minBox.W, maxBox.W);
+ double th = Math.Clamp(Math.Ceiling(packed.Max(p => p.Y + p.RH) + edge), minBox.H, maxBox.H);
+ double td = Math.Clamp(Math.Ceiling(packed.Max(p => p.Z + p.RD) + edge), minBox.D, maxBox.D);
+ return new Box(tw, th, td, wallThickness);
+ }
+ }
+}
diff --git a/Models.cs b/Models.cs
new file mode 100644
index 0000000..8548ddb
--- /dev/null
+++ b/Models.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Drawing;
+
+namespace BoxPacker3D
+{
+ public class Box
+ {
+ public double W { get; set; }
+ public double H { get; set; }
+ public double D { get; set; }
+ /// Thickness of the cardboard/material wall in the same units (mm).
+ public double WallThickness { get; set; }
+
+ public Box(double w, double h, double d, double wallThickness = 0)
+ {
+ W = w; H = h; D = d; WallThickness = wallThickness;
+ }
+
+ /// Outer volume.
+ public double Volume => W * H * D;
+ /// Usable interior volume (after subtracting wall thickness on all 6 sides).
+ public double InnerVolume
+ {
+ get
+ {
+ double iw = Math.Max(0, W - 2 * WallThickness);
+ double ih = Math.Max(0, H - 2 * WallThickness);
+ double id = Math.Max(0, D - 2 * WallThickness);
+ return iw * ih * id;
+ }
+ }
+ }
+
+ public class Item
+ {
+ public string Name { get; set; }
+ public double W { get; set; }
+ public double H { get; set; }
+ public double D { get; set; }
+ public Color Color { get; set; }
+
+ public Item(string name, double w, double h, double d, Color color)
+ {
+ Name = name; W = w; H = h; D = d; Color = color;
+ }
+
+ public double Volume => W * H * D;
+
+ public bool FitsInBox(Box box)
+ {
+ // Check allowed rotations (horizontal only - height preserved)
+ double[] dims = { W, H, D };
+ var rotations = GetRotations();
+ foreach (var r in rotations)
+ if (r[0] <= box.W && r[1] <= box.H && r[2] <= box.D)
+ return true;
+ return false;
+ }
+
+ public double[][] GetRotations(bool preferRotated = false)
+ {
+ var orig = new[] { W, H, D };
+ var rot = new[] { D, H, W };
+ return preferRotated
+ ? new double[][] { rot, orig }
+ : new double[][] { orig, rot };
+ }
+ }
+
+ public class ItemType
+ {
+ public string Name { get; set; }
+ public double W { get; set; }
+ public double H { get; set; }
+ public double D { get; set; }
+ public int Count { get; set; }
+ public Color Color { get; set; }
+ }
+
+ public class PackedItem
+ {
+ public Item Item { get; set; }
+ public double X { get; set; }
+ public double Y { get; set; }
+ public double Z { get; set; }
+ // Actual dimensions used (after rotation)
+ public double RW { get; set; }
+ public double RH { get; set; }
+ public double RD { get; set; }
+
+ public PackedItem(Item item, double x, double y, double z, double rw, double rh, double rd)
+ {
+ Item = item; X = x; Y = y; Z = z; RW = rw; RH = rh; RD = rd;
+ }
+ }
+}
diff --git a/Packer3D.cs b/Packer3D.cs
new file mode 100644
index 0000000..af938a0
--- /dev/null
+++ b/Packer3D.cs
@@ -0,0 +1,23 @@
+/*
+ * PÔVODNÝ ALGORITMUS – zakomentovaný, nahradený LayerPacker
+ *
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace BoxPacker3D
+{
+ public class Packer3D
+ {
+ private const double EPS = 0.001;
+
+ public List Pack(List
- items, Box box,
+ double wallPad = 0, double itemPad = 0, Action? onProgress = null,
+ bool allowRotation = true, bool preferRotated = false)
+ {
+ // ... (Extreme Points algorithm)
+ }
+ // ... helper methods
+ }
+}
+*/
diff --git a/PdfExporter.cs b/PdfExporter.cs
new file mode 100644
index 0000000..1601642
--- /dev/null
+++ b/PdfExporter.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace BoxPacker3D
+{
+ public static class PdfExporter
+ {
+ private const float PageW = 842f, PageH = 595f, Margin = 36f;
+
+ public static void Export(string path,
+ List solutions,
+ List overviews,
+ List layerBitmaps,
+ List<(double YMin, double YMax)> layers,
+ double wallPad, double itemPad,
+ List
- unpackedItems = null)
+ {
+ var pages = new List();
+
+ // Layer pages
+ foreach (var bmp in layerBitmaps)
+ pages.Add(BitmapToJpeg(bmp));
+
+ // Overview pages
+ foreach (var bmp in overviews)
+ pages.Add(BitmapToJpeg(bmp));
+
+ // Add unpacked items text page if needed
+ bool hasUnpackedPage = unpackedItems != null && unpackedItems.Count > 0;
+
+ using (var fs = new FileStream(path, FileMode.Create))
+ {
+ var w = new PdfWriter(fs);
+ w.Raw("%PDF-1.4\n%\xe2\xe3\xcf\xd3\n");
+
+ int n = pages.Count + (hasUnpackedPage ? 1 : 0); // Total pages including text page
+ int basePage = 3;
+ int baseContent = basePage + n;
+ int baseImage = baseContent + n;
+ int objFont1 = baseImage + pages.Count; // After image objects
+ int objFont2 = objFont1 + 1;
+
+ var offsets = new long[objFont2 + 1];
+
+ // Object 1: Catalog
+ offsets[1] = w.Pos;
+ w.Obj(1); w.Raw("<< /Type /Catalog /Pages 2 0 R >>\n"); w.EndObj();
+
+ // Object 2: Pages
+ var kids = string.Join(" ", Enumerable.Range(basePage, n).Select(i => $"{i} 0 R"));
+ offsets[2] = w.Pos;
+ w.Obj(2);
+ w.Raw($"<< /Type /Pages /Kids [{kids}] /Count {n} >>\n");
+ w.EndObj();
+
+ // Per-page objects (image pages)
+ for (int i = 0; i < pages.Count; i++)
+ {
+ bool isLayer = (i < layerBitmaps.Count);
+ string pageTitle;
+ if (isLayer)
+ {
+ int layIdx = i;
+ pageTitle = $"Vrstva {layIdx + 1} / {layerBitmaps.Count}" +
+ (layIdx < layers.Count ? $" (Y: {layers[layIdx].YMin:F1} – {layers[layIdx].YMax:F1} mm)" : "");
+ }
+ else
+ {
+ int boxIdx = i - layerBitmaps.Count;
+ var sol = solutions[boxIdx];
+ pageTitle = $"Škatuľa {boxIdx + 1} / {solutions.Count} " +
+ $"({sol.Box.W}×{sol.Box.D}×{sol.Box.H} mm)";
+ }
+
+ int imgObj = baseImage + i;
+ int contentObj= baseContent + i;
+ int pageObj = basePage + i;
+
+ var imgBytes = pages[i];
+ int imgW, imgH;
+ using (var ms = new MemoryStream(imgBytes))
+ using (var bmp = new Bitmap(ms))
+ { imgW = bmp.Width; imgH = bmp.Height; }
+
+ float headerH = 52f, footerH = 22f;
+ float availW = PageW - 2 * Margin;
+ float availH = PageH - headerH - footerH - Margin;
+ float sc = Math.Min(availW / imgW, availH / imgH);
+ float scaledW = imgW * sc, scaledH = imgH * sc;
+ float imgX = Margin + (availW - scaledW) / 2;
+ float imgY = PageH - headerH - scaledH - 4;
+
+ var cs = new StringBuilder();
+ // White background
+ cs.AppendLine("1 1 1 rg");
+ cs.AppendLine($"0 0 {PageW:F2} {PageH:F2} re f");
+ // Header bar
+ cs.AppendLine("0.06 0.35 0.72 rg");
+ cs.AppendLine($"0 {PageH - 44:F2} {PageW:F2} 44 re f");
+ // Title
+ cs.AppendLine("1 1 1 rg BT /F1 14 Tf");
+ cs.AppendLine($"{Margin:F2} {PageH - 30:F2} Td");
+ cs.AppendLine($"({EscPdf(pageTitle)}) Tj ET");
+ // Date
+ cs.AppendLine("0.70 0.80 0.90 rg BT /F2 8 Tf");
+ cs.AppendLine($"{PageW - 200:F2} {PageH - 30:F2} Td");
+ cs.AppendLine($"(Strana {i + 1} / {n} {DateTime.Now:dd.MM.yyyy}) Tj ET");
+ // Box info
+ if (isLayer && i < solutions.Count)
+ {
+ var sol = solutions[0]; // assume first box for layer pages
+ cs.AppendLine("0.40 0.40 0.50 rg BT /F2 8 Tf");
+ cs.AppendLine($"{Margin:F2} {PageH - 44:F2} Td");
+ string infoLine = $"Skatula: {sol.Box.W}×{sol.Box.H}×{sol.Box.D} mm | " +
+ $"Stenova izolacia: {wallPad} mm | Medikusova: {itemPad} mm";
+ cs.AppendLine($"({EscPdf(infoLine)}) Tj ET");
+ }
+ // Image
+ cs.AppendLine("q");
+ cs.AppendLine($"{scaledW:F2} 0 0 {scaledH:F2} {imgX:F2} {imgY:F2} cm");
+ cs.AppendLine($"/Im{i} Do Q");
+ // Footer
+ cs.AppendLine("0.35 0.35 0.45 rg BT /F2 7 Tf");
+ cs.AppendLine($"{Margin:F2} 10 Td");
+ cs.AppendLine("(Generovane programom 3D Box Packer) Tj ET");
+
+ byte[] csBytes = Encoding.Latin1.GetBytes(cs.ToString());
+
+ // Page
+ offsets[pageObj] = w.Pos;
+ w.Obj(pageObj);
+ w.Raw($"<< /Type /Page /Parent 2 0 R\n");
+ w.Raw($" /MediaBox [0 0 {PageW:F2} {PageH:F2}]\n");
+ w.Raw($" /Contents {contentObj} 0 R\n");
+ w.Raw($" /Resources << /Font << /F1 {objFont1} 0 R /F2 {objFont2} 0 R >>");
+ w.Raw($" /XObject << /Im{i} {imgObj} 0 R >> >>\n>>\n");
+ w.EndObj();
+
+ // Content
+ offsets[contentObj] = w.Pos;
+ w.Obj(contentObj);
+ w.Raw($"<< /Length {csBytes.Length} >>\nstream\n");
+ w.Bytes(csBytes);
+ w.Raw("\nendstream\n");
+ w.EndObj();
+
+ // Image
+ offsets[imgObj] = w.Pos;
+ w.Obj(imgObj);
+ w.Raw($"<< /Type /XObject /Subtype /Image /Width {imgW} /Height {imgH}");
+ w.Raw($" /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode");
+ w.Raw($" /Length {imgBytes.Length} >>\nstream\n");
+ w.Bytes(imgBytes);
+ w.Raw("\nendstream\n");
+ w.EndObj();
+ }
+
+ // Unpacked items text page (if any)
+ if (hasUnpackedPage)
+ {
+ int pageIdx = pages.Count;
+ int pageObj = basePage + pageIdx;
+ int contentObj = baseContent + pageIdx;
+
+ // Group unpacked items by name
+ var grouped = unpackedItems
+ .GroupBy(it => it.Name)
+ .Select(g => new {
+ Name = g.Key,
+ Count = g.Count(),
+ W = g.First().W,
+ H = g.First().H,
+ D = g.First().D
+ })
+ .OrderBy(x => x.Name)
+ .ToList();
+
+ var cs = new StringBuilder();
+ // White background
+ cs.AppendLine("1 1 1 rg");
+ cs.AppendLine($"0 0 {PageW:F2} {PageH:F2} re f");
+
+ // Header bar (red for warning)
+ cs.AppendLine("0.83 0.18 0.18 rg");
+ cs.AppendLine($"0 {PageH - 44:F2} {PageW:F2} 44 re f");
+
+ // Title
+ cs.AppendLine("1 1 1 rg BT /F1 14 Tf");
+ cs.AppendLine($"{Margin:F2} {PageH - 30:F2} Td");
+ cs.AppendLine($"(NEZABALENE KUSY) Tj ET");
+
+ // Date
+ cs.AppendLine("1 1 1 rg BT /F2 8 Tf");
+ cs.AppendLine($"{PageW - 200:F2} {PageH - 30:F2} Td");
+ cs.AppendLine($"(Strana {n} / {n} {DateTime.Now:dd.MM.yyyy}) Tj ET");
+
+ // Summary
+ cs.AppendLine("0.20 0.20 0.30 rg BT /F1 11 Tf");
+ cs.AppendLine($"{Margin:F2} {PageH - 80:F2} Td");
+ cs.AppendLine($"(Nasledujuce kusy sa nezmestili do skatul:) Tj ET");
+
+ // Table header
+ float y = PageH - 120;
+ cs.AppendLine("0.85 0.85 0.90 rg");
+ cs.AppendLine($"{Margin:F2} {y:F2} {PageW - 2*Margin:F2} 20 re f");
+
+ cs.AppendLine("0.25 0.25 0.35 rg BT /F1 9 Tf");
+ cs.AppendLine($"{Margin + 10:F2} {y + 6:F2} Td");
+ cs.AppendLine("(Nazov) Tj");
+ cs.AppendLine($"{250:F2} {0:F2} Td (Rozmery \\(mm\\)) Tj");
+ cs.AppendLine($"{160:F2} {0:F2} Td (Pocet) Tj ET");
+
+ y -= 25;
+
+ // Table rows
+ int rowNum = 0;
+ foreach (var item in grouped)
+ {
+ if (y < 60) break; // Don't overflow page
+
+ // Alternating row background
+ if (rowNum % 2 == 0)
+ {
+ cs.AppendLine("0.96 0.96 0.98 rg");
+ cs.AppendLine($"{Margin:F2} {y:F2} {PageW - 2*Margin:F2} 18 re f");
+ }
+
+ cs.AppendLine("0.20 0.20 0.30 rg BT /F2 8 Tf");
+ cs.AppendLine($"{Margin + 10:F2} {y + 5:F2} Td");
+ cs.AppendLine($"({EscPdf(item.Name)}) Tj");
+ cs.AppendLine($"{250:F2} {0:F2} Td ({item.W:F0} x {item.H:F0} x {item.D:F0}) Tj");
+ cs.AppendLine($"{160:F2} {0:F2} Td ({item.Count}) Tj ET");
+
+ y -= 18;
+ rowNum++;
+ }
+
+ if (grouped.Count > rowNum)
+ {
+ cs.AppendLine("0.50 0.50 0.60 rg BT /F2 7 Tf");
+ cs.AppendLine($"{Margin + 10:F2} {y:F2} Td");
+ cs.AppendLine($"(... a dalsich {grouped.Count - rowNum} typov kusov) Tj ET");
+ }
+
+ // Footer
+ cs.AppendLine("0.35 0.35 0.45 rg BT /F2 7 Tf");
+ cs.AppendLine($"{Margin:F2} 10 Td");
+ cs.AppendLine("(Generovane programom 3D Box Packer) Tj ET");
+
+ byte[] csBytes = Encoding.Latin1.GetBytes(cs.ToString());
+
+ // Page
+ offsets[pageObj] = w.Pos;
+ w.Obj(pageObj);
+ w.Raw($"<< /Type /Page /Parent 2 0 R\n");
+ w.Raw($" /MediaBox [0 0 {PageW:F2} {PageH:F2}]\n");
+ w.Raw($" /Contents {contentObj} 0 R\n");
+ w.Raw($" /Resources << /Font << /F1 {objFont1} 0 R /F2 {objFont2} 0 R >> >>\n>>\n");
+ w.EndObj();
+
+ // Content
+ offsets[contentObj] = w.Pos;
+ w.Obj(contentObj);
+ w.Raw($"<< /Length {csBytes.Length} >>\nstream\n");
+ w.Bytes(csBytes);
+ w.Raw("\nendstream\n");
+ w.EndObj();
+ }
+
+ // Fonts
+ offsets[objFont1] = w.Pos;
+ w.Obj(objFont1);
+ w.Raw("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\n");
+ w.EndObj();
+
+ offsets[objFont2] = w.Pos;
+ w.Obj(objFont2);
+ w.Raw("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\n");
+ w.EndObj();
+
+ // xref
+ long xrefPos = w.Pos;
+ w.Raw($"xref\n0 {offsets.Length}\n");
+ w.Raw("0000000000 65535 f \n");
+ for (int i = 1; i < offsets.Length; i++)
+ w.Raw($"{offsets[i]:D10} 00000 n \n");
+
+ w.Raw($"trailer\n<< /Size {offsets.Length} /Root 1 0 R >>\n");
+ w.Raw($"startxref\n{xrefPos}\n%%EOF\n");
+ }
+ }
+
+ private static byte[] BitmapToJpeg(Bitmap bmp)
+ {
+ using (var ms = new MemoryStream())
+ {
+ var enc = GetJpegEncoder();
+ var prms = new EncoderParameters(1);
+ prms.Param[0] = new EncoderParameter(
+ System.Drawing.Imaging.Encoder.Quality, (long)88);
+ bmp.Save(ms, enc, prms);
+ return ms.ToArray();
+ }
+ }
+
+ private static ImageCodecInfo GetJpegEncoder() =>
+ ImageCodecInfo.GetImageEncoders().First(c => c.MimeType == "image/jpeg");
+
+ private static string EscPdf(string s) =>
+ s.Replace("\\", "\\\\").Replace("(", "\\(").Replace(")", "\\)");
+
+ private class PdfWriter
+ {
+ private readonly FileStream _fs;
+ public long Pos => _fs.Position;
+
+ public PdfWriter(FileStream fs) { _fs = fs; }
+
+ public void Raw(string s) { var b = Encoding.Latin1.GetBytes(s); _fs.Write(b, 0, b.Length); }
+ public void Bytes(byte[] b) { _fs.Write(b, 0, b.Length); }
+ public void Obj(int n) { Raw($"{n} 0 obj\n"); }
+ public void EndObj() { Raw("endobj\n"); }
+ }
+ }
+}
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..92e6abe
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Windows.Forms;
+
+namespace BoxPacker3D
+{
+ static class Program
+ {
+ [STAThread]
+ static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new Form1());
+ }
+ }
+}
diff --git a/README_INSTALLER.md b/README_INSTALLER.md
new file mode 100644
index 0000000..a0594a4
--- /dev/null
+++ b/README_INSTALLER.md
@@ -0,0 +1,128 @@
+# BoxPacker3D - Installer Information
+
+## 🎯 Máte 3 možnosti!
+
+Tento projekt podporuje **tri spôsoby** vytvorenia inštalátora:
+
+1. **WiX Toolset** (.wxs) - **ODPORÚČANÉ** - Kompletný, ready-to-build
+2. **Visual Studio Installer Projects** (.vdproj) - GUI, jednoduchý
+3. **Inno Setup** (.iss) - Najmenší, EXE formát
+
+### 📖 **Podrobné porovnanie:** `INSTALLER_OPTIONS_COMPARISON.md`
+
+---
+
+## ⚡ Odporúčanie: WiX Toolset
+
+**Prečo WiX?**
+- ✅ **Kompletný** - `BoxPacker3D.wxs` je ready-to-build (všetky súbory definované)
+- ✅ **Textový** - Git-friendly, dá sa verzovať
+- ✅ **MSI výstup** - Profesionálny Windows formát
+- ✅ **Build skript** - Jednoduchý `build-wix.bat`
+
+### 🚀 Rýchly štart s WiX
+
+1. **Stiahnite WiX Toolset:**
+ - https://wixtoolset.org/releases/
+ - Verzia 3.11 alebo novšia
+
+2. **Build MSI:**
+ ```batch
+ build-wix.bat
+ ```
+
+3. **Výstup:**
+ ```
+ bin\BoxPacker3D.msi
+ ```
+
+Hotovo! 🎉
+
+---
+
+## 📦 Možnosť 2: Visual Studio Installer Projects
+
+**Súbory:**
+- `BoxPacker3D.Setup.vdproj` - Setup projekt (kostra - treba dotvoriť v GUI)
+- `BoxPacker3D_WithSetup.sln` - Solution so Setup projektom
+- `VISUAL_STUDIO_INSTALLER_GUIDE.md` - Podrobný 12-krokový návod
+
+**Použitie:**
+1. Otvorte `BoxPacker3D_WithSetup.sln` vo VS 2017
+2. Nainštalujte rozšírenie "Microsoft Visual Studio Installer Projects"
+3. V Setup projekte pridajte súbory cez GUI (Project Output)
+4. Build → MSI v `BoxPacker3D.Setup\Release\`
+
+**Výhody:**
+- GUI drag & drop
+- Integrované do VS
+- Jednoduchý upgrade
+
+**Nevýhody:**
+- Binárny `.vdproj` (nie Git-friendly)
+- Treba dotvoriť v GUI
+- Potrebuje Visual Studio
+
+---
+
+## 🎨 Možnosť 3: Inno Setup
+
+**Súbory (premenované na .OLD):**
+- `installer.iss.OLD` → Odstráňte `.OLD` príponu
+- `build-installer-inno.bat.OLD` → Odstráňte `.OLD` príponu
+- `INSTALLER_README_INNO.md.OLD` → Návod
+
+**Použitie:**
+1. Odstráňte `.OLD` zo súborov
+2. Stiahnite [Inno Setup](https://jrsoftware.org/isdl.php)
+3. Otvorte `installer.iss` v Inno Setup Compiler
+4. Build → EXE v `installer\BoxPacker3D_Setup.exe`
+
+**Výhody:**
+- Najmenšie súbory
+- EXE formát
+- Flexibilné skripty
+
+**Nevýhody:**
+- EXE namiesto MSI
+- Samostatný nástroj
+
+---
+
+## 📊 Rýchle porovnanie
+
+| Feature | WiX | VS Installer | Inno Setup |
+|---------|-----|--------------|------------|
+| **Výstup** | MSI | MSI | EXE |
+| **Ready to build** | ✅ Áno | ❌ Nie* | ✅ Áno |
+| **Git-friendly** | ✅ Áno | ❌ Nie | ✅ Áno |
+| **GUI** | ❌ Nie | ✅ Áno | ⚠️ Čiastočný |
+| **Komplexnosť** | Stredná | Ľahká | Ľahká |
+
+*Kostra je vytvorená, ale súbory treba pridať cez GUI
+
+---
+
+## 🔧 Build Tools v projekte
+
+- **build-wix.bat** - Build MSI cez WiX Toolset
+- **build-vstudio.bat** - Build aplikácie (príprava pre installer)
+- **build-installer-inno.bat.OLD** - Build EXE cez Inno Setup (odstráňte .OLD)
+
+---
+
+## 📖 Dokumentácia
+
+- **INSTALLER_OPTIONS_COMPARISON.md** - Podrobné porovnanie všetkých 3 možností
+- **VISUAL_STUDIO_INSTALLER_GUIDE.md** - 12-krokový návod pre VS Installer Projects
+- **INSTALLER_README_INNO.md.OLD** - Návod pre Inno Setup
+
+---
+
+## ❓ Ktorú si vybrať?
+
+- **Chcem najrýchlejší štart** → **WiX** (spustite `build-wix.bat`)
+- **Mám otvorené VS 2017** → **VS Installer Projects** (GUI pohodlie)
+- **Chcem najmenšie súbory** → **Inno Setup** (EXE formát)
+
+Všetky tri fungujú dobre! Záleží len na vašich preferenciách. 🎉
diff --git a/VISUAL_STUDIO_INSTALLER_GUIDE.md b/VISUAL_STUDIO_INSTALLER_GUIDE.md
new file mode 100644
index 0000000..da6edc4
--- /dev/null
+++ b/VISUAL_STUDIO_INSTALLER_GUIDE.md
@@ -0,0 +1,321 @@
+# Visual Studio Installer Projects - Návod
+
+## 1. Inštalácia rozšírenia pre VS 2017
+
+### Krok 1: Otvorte VS 2017
+1. Spustite Visual Studio 2017
+2. Prejdite na **Tools → Extensions and Updates...**
+3. V ľavom paneli vyberte **Online**
+4. Do vyhľadávacieho poľa napíšte: `Microsoft Visual Studio Installer Projects`
+5. Nájdite oficiálne rozšírenie od Microsoft
+6. Kliknite **Download**
+7. Po stiahnutí **zatvorte VS 2017** - inštalácia sa spustí automaticky
+8. Postupujte podľa VSIX Installer krokov
+9. Reštartujte VS 2017
+
+**Alternatíva - Priame stiahnutie:**
+- Navštívte: https://marketplace.visualstudio.com/items?itemName=VisualStudioClient.MicrosoftVisualStudio2017InstallerProjects
+- Stiahnite `.vsix` súbor a spustite ho
+
+---
+
+## 2. Vytvorenie Setup projektu
+
+### Krok 1: Pridajte Setup projekt do solution
+1. Otvorte `BoxPacker3D.sln` vo VS 2017
+2. V **Solution Explorer** kliknite pravým na solution (nie projekt)
+3. **Add → New Project...**
+4. V ľavom paneli: **Other Project Types → Visual Studio Installer**
+5. Vyberte šablónu: **Setup Project**
+6. Meno: `BoxPacker3D.Setup`
+7. Kliknite **OK**
+
+### Krok 2: Nastavte základné vlastnosti
+1. V **Solution Explorer** kliknite pravým na `BoxPacker3D.Setup`
+2. Vyberte **Properties**
+3. Nastavte:
+ - **Author**: Vaše meno alebo firma
+ - **Description**: `3D Box Packer - Aplikácia pre 3D balenie kusov`
+ - **Manufacturer**: Vaše meno/firma
+ - **ManufacturerUrl**: Vaša web stránka (voliteľné)
+ - **ProductName**: `3D Box Packer`
+ - **Title**: `3D Box Packer Installer`
+ - **Version**: `1.0.0`
+
+---
+
+## 3. Pridanie súborov do inštalátora
+
+### Metóda A: Project Output (Odporúčané)
+
+1. V **Solution Explorer** otvorte `BoxPacker3D.Setup`
+2. Vidíte **File System Editor** s tromi priečinkami:
+ - Application Folder
+ - User's Desktop
+ - User's Programs Menu
+
+3. **Kliknite pravým na "Application Folder"** → **Add → Project Output...**
+4. V dialógu:
+ - **Project**: Vyberte `BoxPacker3D`
+ - **Configuration**: Vyberte `Release .NET 9.0`
+ - Zaškrtnite: **Primary output** (obsahuje EXE a všetky DLLs)
+ - Kliknite **OK**
+
+5. **Pridajte konfiguračné súbory**:
+ - Kliknite pravým na **Application Folder** → **Add → File...**
+ - Navigujte do priečinka projektu
+ - Vyberte: `appsettings.json` a `items.db`
+ - Kliknite **OK**
+
+### Metóda B: Publish Output (Alternatíva)
+
+Ak metóda A nefunguje správne:
+
+1. Najprv publikujte projekt:
+ ```
+ dotnet publish -c Release -r win-x64 --self-contained false
+ ```
+
+2. V Setup projekte:
+ - Kliknite pravým na **Application Folder** → **Add → File...**
+ - Navigujte do: `bin\Release\net9.0-windows\win-x64\publish\`
+ - Vyberte **všetky súbory** (`.exe`, `.dll`, `.json`, `.db`)
+ - Kliknite **OK**
+
+---
+
+## 4. Vytvorenie Desktop ikony (voliteľné)
+
+1. V **File System Editor** kliknite pravým na **User's Desktop**
+2. **Add → File...**
+3. V dialógu vyberte **Application Folder** (nie súbor!)
+4. V zozname nájdite **Primary output from BoxPacker3D**
+5. Kliknite **OK**
+6. Ikona sa vytvorí automaticky
+
+Alebo:
+1. Kliknite pravým na **User's Desktop** → **Create New Shortcut**
+2. V dialógu vyberte **Application Folder**
+3. Vyberte **Primary output from BoxPacker3D**
+4. Kliknite **OK**
+5. Premenujte shortcut na "3D Box Packer"
+
+---
+
+## 5. Start Menu položka
+
+1. V **File System Editor** kliknite pravým na **User's Programs Menu**
+2. **Add → Folder**
+3. Premenujte priečinok na `3D Box Packer`
+4. Kliknite pravým na tento priečinok → **Create New Shortcut**
+5. V dialógu vyberte **Application Folder**
+6. Vyberte **Primary output from BoxPacker3D**
+7. Kliknite **OK**
+8. Premenujte shortcut na "3D Box Packer"
+
+---
+
+## 6. Nastavenie .NET 9 prerequisite
+
+### Varient 1: Launch Conditions (Upozornenie)
+
+1. V **Solution Explorer** kliknite pravým na `BoxPacker3D.Setup`
+2. **View → Launch Conditions**
+3. Kliknite pravým na **Requirements on Target Machine** → **Add Launch Condition**
+4. Vlastnosti:
+ - **Condition**: `NETFRAMEWORK90FULL="#1"`
+ - **Message**: `Táto aplikácia vyžaduje .NET 9. Stiahnite z https://dotnet.microsoft.com/download/dotnet/9.0`
+
+### Variant 2: Bootstrapper (Automatická inštalácia)
+
+1. Kliknite pravým na `BoxPacker3D.Setup` → **Properties**
+2. Nájdite sekciu **Prerequisites...**
+3. V dialógu:
+ - Zaškrtnite **Create setup program to install prerequisite components**
+ - V zozname hľadajte `.NET Desktop Runtime 9.0` (ak nie je, musíte použiť custom bootstrapper)
+ - Vyberte umiestnenie: **Download prerequisites from the component vendor's web site**
+4. Kliknite **OK**
+
+**Poznámka:** Ak .NET 9 nie je v zozname prerequisite (starší VS 2017), použite Launch Condition a užívateľ si musí nainštalovať .NET 9 manuálne.
+
+---
+
+## 7. Build inštalátora
+
+### Jednoduchý build:
+
+1. V **Solution Explorer** kliknite pravým na `BoxPacker3D.Setup`
+2. Vyberte **Build**
+3. Počkajte na dokončenie
+4. MSI súbor nájdete v: `BoxPacker3D.Setup\Release\BoxPacker3D.Setup.msi`
+
+### Batch script pre automatizáciu:
+
+Vytvorte `build-installer-vstudio.bat`:
+
+```batch
+@echo off
+echo ================================================
+echo Building BoxPacker3D with VS Installer Projects
+echo ================================================
+
+echo.
+echo [1/3] Building Release configuration...
+msbuild BoxPacker3D.sln /p:Configuration=Release /p:Platform="Any CPU" /v:minimal
+
+if errorlevel 1 (
+ echo ERROR: Project build failed!
+ pause
+ exit /b 1
+)
+
+echo.
+echo [2/3] Building Setup project...
+msbuild BoxPacker3D.Setup\BoxPacker3D.Setup.vdproj /p:Configuration=Release /v:minimal
+
+if errorlevel 1 (
+ echo ERROR: Setup build failed!
+ pause
+ exit /b 1
+)
+
+echo.
+echo [3/3] Installer created successfully!
+echo Location: BoxPacker3D.Setup\Release\BoxPacker3D.Setup.msi
+echo.
+
+explorer /select,"BoxPacker3D.Setup\Release\BoxPacker3D.Setup.msi"
+pause
+```
+
+---
+
+## 8. Vlastnosti MSI inštalátora
+
+### Upgrade Behavior (Automatický upgrade)
+
+1. V **Solution Explorer** kliknite pravým na `BoxPacker3D.Setup`
+2. **Properties**
+3. Nastavte:
+ - **RemovePreviousVersions**: `True` (umožní upgrade cez nainštalovanie novej verzie)
+ - **DetectNewerInstalledVersion**: `True` (zabráni inštalácii staršej verzie)
+
+### Product Code a Upgrade Code
+
+- **ProductCode**: Automaticky sa vygeneruje nový pri každom zmene verzie
+- **UpgradeCode**: **NIKDY nemeňte!** Toto ID identifikuje vašu aplikáciu naprieč verziami
+
+---
+
+## 9. Pokročilé nastavenia
+
+### Custom Actions (Voliteľné)
+
+Ak potrebujete spustiť akcie počas inštalácie:
+
+1. Kliknite pravým na `BoxPacker3D.Setup` → **View → Custom Actions**
+2. Môžete pridať:
+ - **Install**: Spustí sa pri inštalácii
+ - **Commit**: Po úspešnej inštalácii
+ - **Rollback**: Ak inštalácia zlyhá
+ - **Uninstall**: Pri odinštalácii
+
+### Registry Keys (Voliteľné)
+
+1. **View → Registry**
+2. Navigujte do `HKEY_LOCAL_MACHINE\Software`
+3. Kliknite pravým → **New → Key**
+4. Vytvorte štruktúru napr. `YourCompany\BoxPacker3D`
+5. Pridajte hodnoty (napr. install path)
+
+### File Associations (Voliteľné)
+
+1. **View → File Types**
+2. Pridajte vlastné prípony (.box, .pack, atď.)
+3. Asociujte s vašou aplikáciou
+
+---
+
+## 10. Testovanie
+
+### Pred vydaním:
+
+1. **Nainštalujte MSI** na čistom PC alebo VM
+2. Overte že:
+ - ✅ Aplikácia sa spustí
+ - ✅ Všetky súbory sú prítomné
+ - ✅ items.db sa vytvorí/skopíruje
+ - ✅ appsettings.json funguje
+ - ✅ Desktop ikona funguje (ak je)
+ - ✅ Start Menu položka funguje
+ - ✅ Uninstaller funguje správne
+
+3. **Test upgrade:**
+ - Zmeňte verziu v Setup properties (napr. 1.0.0 → 1.0.1)
+ - Rebuild
+ - Nainštalujte cez starú verziu
+ - Overte že upgrade prebehol bez chýb
+
+---
+
+## 11. Distribúcia
+
+MSI súbor je pripravený na distribúciu:
+- Môžete ho nahrať na web
+- Poslať emailom
+- Distribuovať na USB
+- Nie je potrebný žiadny iný súbor
+
+**Veľkosť inštalátora:**
+- Framework-dependent (vyžaduje .NET 9): ~5-10 MB
+- Self-contained (zahrnutý .NET runtime): ~60-80 MB
+
+---
+
+## 12. Riešenie problémov
+
+### "The project could not be opened"
+- **Príčina:** Rozšírenie nie je nainštalované
+- **Riešenie:** Nainštalujte "Microsoft Visual Studio Installer Projects"
+
+### "Unable to update dependencies"
+- **Príčina:** Konflikt verzií
+- **Riešenie:** Použite **Project Output** namiesto manuálneho pridávania súborov
+
+### MSI zlyhá pri inštalácii
+- Skontrolujte Windows Event Log (Application)
+- Zapnite MSI logging: `msiexec /i BoxPacker3D.Setup.msi /l*v install.log`
+
+### items.db sa neprenesie
+- V Properties súboru `items.db` overte:
+ - **Build Action**: None alebo Content
+ - **Copy to Output Directory**: Copy if newer
+- Pridajte manuálne do Application Folder v Setup projekte
+
+---
+
+## Porovnanie: VS Installer vs Inno Setup
+
+| Feature | VS Installer Projects | Inno Setup |
+|---------|----------------------|------------|
+| **Výstup** | MSI | EXE |
+| **GUI** | ✅ Visual Studio | ❌ Text editor |
+| **Verzovanie** | ⚠️ Binárny .vdproj | ✅ Text .iss |
+| **Cena** | ✅ Zadarmo | ✅ Zadarmo |
+| **Jednoduchosť** | ✅ Drag & drop | ⚠️ Scripting |
+| **Flexibilita** | ⚠️ Obmedzená | ✅ Vysoká |
+| **Windows štandard** | ✅ MSI natívny | ⚠️ EXE custom |
+| **CI/CD** | ⚠️ Potrebuje VS | ✅ Command-line |
+
+---
+
+## Ďalšie kroky
+
+1. Nainštalujte rozšírenie
+2. Vytvorte Setup projekt
+3. Pridajte súbory pomocou Project Output
+4. Nastavte vlastnosti
+5. Build a test
+6. Distribuujte MSI
+
+**Tip:** Ak plánujete často updatovať, zachovajte `UpgradeCode` konštantný a len zvyšujte verziu!
diff --git a/Viewer3D.cs b/Viewer3D.cs
new file mode 100644
index 0000000..e974379
--- /dev/null
+++ b/Viewer3D.cs
@@ -0,0 +1,495 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Linq;
+using System.Windows.Forms;
+
+namespace BoxPacker3D
+{
+ public class Viewer3D
+ {
+ private Panel panel;
+
+ public Viewer3D(Panel panel)
+ {
+ this.panel = panel;
+ panel.Paint += Panel_Paint;
+ _packedItems = new List();
+ }
+
+ private List _packedItems;
+ private Box _box;
+ private double _rotX = 30, _rotY = 30, _zoom = 1.0;
+ private double _wallPad = 0, _itemPad = 0;
+
+ public void Clear()
+ {
+ _packedItems = new List();
+ _box = null;
+ panel.Invalidate();
+ }
+
+ public void Render(List packedItems, Box box,
+ double rotX, double rotY, double zoom = 1.0,
+ double wallPad = 0, double itemPad = 0)
+ {
+ _packedItems = packedItems;
+ _box = box;
+ _rotX = rotX; _rotY = rotY;
+ _zoom = Math.Max(0.1, zoom);
+ _wallPad = wallPad; _itemPad = itemPad;
+ panel.Invalidate();
+ }
+
+ private void Panel_Paint(object sender, PaintEventArgs e)
+ {
+ var g = e.Graphics;
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.Clear(Color.FromArgb(18, 18, 28));
+ if (_box == null) { DrawWelcome(g); return; }
+ DrawScene(g, panel.Width, panel.Height);
+ }
+
+ private void DrawWelcome(Graphics g)
+ {
+ int w = panel.Width, h = panel.Height;
+ using (var brush = new SolidBrush(Color.FromArgb(40, 40, 60)))
+ g.FillRectangle(brush, 0, 0, w, h);
+ using (var pen = new Pen(Color.FromArgb(35, 35, 55), 1))
+ {
+ for (int x = 0; x < w; x += 40) g.DrawLine(pen, x, 0, x, h);
+ for (int y = 0; y < h; y += 40) g.DrawLine(pen, 0, y, w, y);
+ }
+ using (var font = new Font("Segoe UI", 14f))
+ using (var brush = new SolidBrush(Color.FromArgb(80, 100, 160)))
+ {
+ var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
+ g.DrawString("Zadajte rozmery škatule a kusy,\npotom kliknite 🚀 SPUSTIŤ BALENIE",
+ font, brush, new RectangleF(0, 0, w, h), sf);
+ }
+ }
+
+ public Bitmap RenderToBitmap(List packedItems, Box box,
+ double rotX, double rotY, double zoom, int width, int height,
+ double wallPad = 0, double itemPad = 0, bool whiteBg = false)
+ {
+ var bmp = new Bitmap(width, height);
+ using (var g = Graphics.FromImage(bmp))
+ {
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.Clear(whiteBg ? Color.White : Color.FromArgb(18, 18, 28));
+ DrawSceneEx(g, width, height, packedItems, box, rotX, rotY, zoom, wallPad, itemPad, null, whiteBg);
+ }
+ return bmp;
+ }
+
+ public Bitmap RenderLayerToBitmap(List packedItems, Box box,
+ double rotX, double rotY, double zoom,
+ double layerYMin, double layerYMax,
+ int layerIndex, int totalLayers,
+ int width, int height,
+ double wallPad = 0, double itemPad = 0, bool whiteBg = false)
+ {
+ var bmp = new Bitmap(width, height);
+ using (var g = Graphics.FromImage(bmp))
+ {
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.Clear(whiteBg ? Color.White : Color.FromArgb(18, 18, 28));
+
+ var layerItems = packedItems
+ .Where(p => p.Y < layerYMax - 0.001 && p.Y + p.RH > layerYMin + 0.001)
+ .ToList();
+
+ DrawSceneEx(g, width, height, layerItems, box, rotX, rotY, zoom, wallPad, itemPad,
+ $"Vrstva {layerIndex + 1} / {totalLayers} (Y: {layerYMin:F1} – {layerYMax:F1} mm)", whiteBg);
+
+ // Draw layer boundary indicators (subtle horizontal planes at YMin and YMax)
+ DrawLayerBoundaries(g, width, height, box, layerYMin, layerYMax, rotX, rotY, zoom, whiteBg);
+ }
+ return bmp;
+ }
+
+ private void DrawLayerBoundaries(Graphics g, int w, int h, Box box,
+ double yMin, double yMax, double rotX, double rotY, double zoom, bool whiteBg)
+ {
+ double radX = rotX * Math.PI / 180.0;
+ double radY = rotY * Math.PI / 180.0;
+ double maxDim = Math.Max(Math.Max(box.W, box.H), box.D);
+ double scale = Math.Min(w, h) * 0.38 / maxDim * zoom;
+ double cx = w / 2.0, cy = h / 2.0;
+
+ // Draw two horizontal planes (YMin and YMax) as subtle dotted lines
+ var planeColor = whiteBg ? Color.FromArgb(80, 255, 100, 100) : Color.FromArgb(60, 255, 180, 100);
+ using (var pen = new Pen(planeColor, 1f) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot })
+ {
+ // YMin plane
+ DrawHorizontalPlane(g, pen, box, yMin, scale, cx, cy, radX, radY);
+ // YMax plane
+ DrawHorizontalPlane(g, pen, box, yMax, scale, cx, cy, radX, radY);
+ }
+ }
+
+ private void DrawHorizontalPlane(Graphics g, Pen pen, Box box, double y,
+ double scale, double cx, double cy, double radX, double radY)
+ {
+ double yRel = y - box.H / 2; // relative to box center
+ double hw = box.W / 2, hd = box.D / 2;
+
+ // Four corners of the plane
+ var p1 = Project(-hw, yRel, -hd, scale, cx, cy, radX, radY);
+ var p2 = Project( hw, yRel, -hd, scale, cx, cy, radX, radY);
+ var p3 = Project( hw, yRel, hd, scale, cx, cy, radX, radY);
+ var p4 = Project(-hw, yRel, hd, scale, cx, cy, radX, radY);
+
+ // Draw rectangle
+ g.DrawLine(pen, p1, p2);
+ g.DrawLine(pen, p2, p3);
+ g.DrawLine(pen, p3, p4);
+ g.DrawLine(pen, p4, p1);
+ }
+
+ private void DrawScene(Graphics g, int w, int h)
+ => DrawSceneEx(g, w, h, _packedItems, _box, _rotX, _rotY, _zoom, _wallPad, _itemPad, null, false);
+
+ private void DrawSceneEx(Graphics g, int w, int h,
+ List items, Box box,
+ double rotX, double rotY, double zoom,
+ double wallPad, double itemPad,
+ string layerLabel, bool whiteBg)
+ {
+ double radX = rotX * Math.PI / 180.0;
+ double radY = rotY * Math.PI / 180.0;
+
+ double maxDim = Math.Max(Math.Max(box.W, box.H), box.D);
+ double scale = Math.Min(w, h) * 0.38 / maxDim * zoom;
+ double cx = w / 2.0, cy = h / 2.0;
+
+ var sorted = items
+ .Select(pi => (pi, depth: Transform(
+ pi.X + pi.RW / 2 - box.W / 2,
+ pi.Y + pi.RH / 2 - box.H / 2,
+ pi.Z + pi.RD / 2 - box.D / 2, radX, radY).Z))
+ .OrderBy(t => t.depth)
+ .ToList();
+
+ DrawBoxOutline(g, scale, cx, cy, box, radX, radY, alpha: whiteBg ? 100 : 40, wallPad: wallPad, whiteBg: whiteBg);
+
+ if (box.WallThickness > 0.001)
+ DrawWallThicknessZone(g, scale, cx, cy, box, radX, radY, whiteBg);
+
+ if (wallPad > 0.001)
+ DrawPaddingZone(g, scale, cx, cy, box, radX, radY, wallPad, whiteBg);
+
+ foreach (var (pi, _) in sorted)
+ DrawCuboid(g, pi, scale, cx, cy, box, radX, radY);
+
+ DrawBoxOutline(g, scale, cx, cy, box, radX, radY, alpha: whiteBg ? 220 : 180, wallPad: wallPad, whiteBg: whiteBg);
+
+ DrawLegend(g, w, h, items, whiteBg);
+ DrawInfo(g, w, h, box, items.Count, whiteBg);
+
+ if (layerLabel != null)
+ {
+ using (var font = new Font("Segoe UI", 11f, FontStyle.Bold))
+ using (var brush = new SolidBrush(whiteBg ? Color.FromArgb(40, 80, 160) : Color.FromArgb(220, 200, 100, 30)))
+ {
+ var sf = new StringFormat { Alignment = StringAlignment.Center };
+ g.DrawString(layerLabel, font, brush, new RectangleF(0, 8, w, 28), sf);
+ }
+ }
+ }
+
+ private static (double X, double Y, double Z) Transform(double x, double y, double z,
+ double radX, double radY)
+ {
+ double x1 = x * Math.Cos(radY) + z * Math.Sin(radY);
+ double y1 = y;
+ double z1 = -x * Math.Sin(radY) + z * Math.Cos(radY);
+ double x2 = x1;
+ double y2 = y1 * Math.Cos(radX) - z1 * Math.Sin(radX);
+ double z2 = y1 * Math.Sin(radX) + z1 * Math.Cos(radX);
+ return (x2, y2, z2);
+ }
+
+ private PointF Project(double x, double y, double z,
+ double scale, double cx, double cy, double radX, double radY)
+ {
+ var (tx, ty, _) = Transform(x, y, z, radX, radY);
+ return new PointF((float)(cx + tx * scale), (float)(cy - ty * scale));
+ }
+
+ private void DrawCuboid(Graphics g, PackedItem pi,
+ double scale, double cx, double cy,
+ Box box, double radX, double radY)
+ {
+ double x0 = pi.X - box.W / 2, y0 = pi.Y - box.H / 2, z0 = pi.Z - box.D / 2;
+ double x1 = x0 + pi.RW, y1 = y0 + pi.RH, z1 = z0 + pi.RD;
+
+ var pts = Corners8(x0, y0, z0, x1, y1, z1, scale, cx, cy, radX, radY);
+ var baseColor = pi.Item.Color;
+
+ DrawFace(g, new[] { pts[4], pts[5], pts[6], pts[7] }, Shade(baseColor, 1.00f));
+ DrawFace(g, new[] { pts[5], pts[1], pts[2], pts[6] }, Shade(baseColor, 0.75f));
+ DrawFace(g, new[] { pts[3], pts[2], pts[6], pts[7] }, Shade(baseColor, 0.55f));
+
+ using (var pen = new Pen(Color.FromArgb(200, Color.Black), 0.8f))
+ {
+ g.DrawPolygon(pen, new[] { pts[4], pts[5], pts[6], pts[7] });
+ g.DrawPolygon(pen, new[] { pts[5], pts[1], pts[2], pts[6] });
+ g.DrawPolygon(pen, new[] { pts[3], pts[2], pts[6], pts[7] });
+ }
+ }
+
+ private void DrawPaddingZone(Graphics g, double scale, double cx, double cy,
+ Box box, double radX, double radY, double pad, bool whiteBg)
+ {
+ double edge = pad + box.WallThickness;
+ double hw = box.W / 2 - edge, hh = box.H / 2 - edge, hd = box.D / 2 - edge;
+ if (hw <= 0 || hh <= 0 || hd <= 0) return;
+ var pts = new PointF[8];
+ pts[0] = Project(-hw, -hh, -hd, scale, cx, cy, radX, radY);
+ pts[1] = Project(hw, -hh, -hd, scale, cx, cy, radX, radY);
+ pts[2] = Project(hw, hh, -hd, scale, cx, cy, radX, radY);
+ pts[3] = Project(-hw, hh, -hd, scale, cx, cy, radX, radY);
+ pts[4] = Project(-hw, -hh, hd, scale, cx, cy, radX, radY);
+ pts[5] = Project(hw, -hh, hd, scale, cx, cy, radX, radY);
+ pts[6] = Project(hw, hh, hd, scale, cx, cy, radX, radY);
+ pts[7] = Project(-hw, hh, hd, scale, cx, cy, radX, radY);
+
+ var col = whiteBg ? Color.FromArgb(150, 200, 150, 50) : Color.FromArgb(60, 255, 220, 80);
+ using (var pen = new Pen(col, 1f))
+ {
+ pen.DashStyle = DashStyle.Dot;
+ g.DrawPolygon(pen, new[] { pts[0], pts[1], pts[2], pts[3] });
+ g.DrawPolygon(pen, new[] { pts[4], pts[5], pts[6], pts[7] });
+ g.DrawLine(pen, pts[0], pts[4]); g.DrawLine(pen, pts[1], pts[5]);
+ g.DrawLine(pen, pts[2], pts[6]); g.DrawLine(pen, pts[3], pts[7]);
+ }
+ }
+
+ /// Draws inner wire cube + corner connectors to visualise the thickness of the box walls.
+ private void DrawWallThicknessZone(Graphics g, double scale, double cx, double cy,
+ Box box, double radX, double radY, bool whiteBg)
+ {
+ double t = box.WallThickness;
+ double hwO = box.W / 2, hhO = box.H / 2, hdO = box.D / 2;
+ double hwI = hwO - t, hhI = hhO - t, hdI = hdO - t;
+ if (hwI <= 0 || hhI <= 0 || hdI <= 0) return;
+
+ // Outer 8 corners (used for connector lines)
+ var outer = new PointF[8];
+ outer[0] = Project(-hwO, -hhO, -hdO, scale, cx, cy, radX, radY);
+ outer[1] = Project( hwO, -hhO, -hdO, scale, cx, cy, radX, radY);
+ outer[2] = Project( hwO, hhO, -hdO, scale, cx, cy, radX, radY);
+ outer[3] = Project(-hwO, hhO, -hdO, scale, cx, cy, radX, radY);
+ outer[4] = Project(-hwO, -hhO, hdO, scale, cx, cy, radX, radY);
+ outer[5] = Project( hwO, -hhO, hdO, scale, cx, cy, radX, radY);
+ outer[6] = Project( hwO, hhO, hdO, scale, cx, cy, radX, radY);
+ outer[7] = Project(-hwO, hhO, hdO, scale, cx, cy, radX, radY);
+
+ // Inner 8 corners
+ var inner = new PointF[8];
+ inner[0] = Project(-hwI, -hhI, -hdI, scale, cx, cy, radX, radY);
+ inner[1] = Project( hwI, -hhI, -hdI, scale, cx, cy, radX, radY);
+ inner[2] = Project( hwI, hhI, -hdI, scale, cx, cy, radX, radY);
+ inner[3] = Project(-hwI, hhI, -hdI, scale, cx, cy, radX, radY);
+ inner[4] = Project(-hwI, -hhI, hdI, scale, cx, cy, radX, radY);
+ inner[5] = Project( hwI, -hhI, hdI, scale, cx, cy, radX, radY);
+ inner[6] = Project( hwI, hhI, hdI, scale, cx, cy, radX, radY);
+ inner[7] = Project(-hwI, hhI, hdI, scale, cx, cy, radX, radY);
+
+ // Cardboard-brown tones (slightly darker on white backgrounds for print visibility)
+ var innerCol = whiteBg ? Color.FromArgb(220, 120, 80, 40)
+ : Color.FromArgb(200, 180, 140, 90);
+ var connCol = whiteBg ? Color.FromArgb(160, 140, 95, 55)
+ : Color.FromArgb(140, 180, 140, 90);
+
+ // Inner wire cube (solid line)
+ using (var pen = new Pen(innerCol, 1.3f))
+ {
+ g.DrawPolygon(pen, new[] { inner[0], inner[1], inner[2], inner[3] });
+ g.DrawPolygon(pen, new[] { inner[4], inner[5], inner[6], inner[7] });
+ g.DrawLine(pen, inner[0], inner[4]); g.DrawLine(pen, inner[1], inner[5]);
+ g.DrawLine(pen, inner[2], inner[6]); g.DrawLine(pen, inner[3], inner[7]);
+ }
+
+ // 8 corner connectors (outer corner → inner corner)
+ using (var pen = new Pen(connCol, 0.9f))
+ {
+ for (int i = 0; i < 8; i++)
+ g.DrawLine(pen, outer[i], inner[i]);
+ }
+ }
+
+ private void DrawBoxOutline(Graphics g, double scale, double cx, double cy,
+ Box box, double radX, double radY, int alpha, double wallPad, bool whiteBg)
+ {
+ double hw = box.W / 2, hh = box.H / 2, hd = box.D / 2;
+ var pts = new PointF[8];
+ pts[0] = Project(-hw, -hh, -hd, scale, cx, cy, radX, radY);
+ pts[1] = Project(hw, -hh, -hd, scale, cx, cy, radX, radY);
+ pts[2] = Project(hw, hh, -hd, scale, cx, cy, radX, radY);
+ pts[3] = Project(-hw, hh, -hd, scale, cx, cy, radX, radY);
+ pts[4] = Project(-hw, -hh, hd, scale, cx, cy, radX, radY);
+ pts[5] = Project(hw, -hh, hd, scale, cx, cy, radX, radY);
+ pts[6] = Project(hw, hh, hd, scale, cx, cy, radX, radY);
+ pts[7] = Project(-hw, hh, hd, scale, cx, cy, radX, radY);
+
+ var col = whiteBg ? Color.FromArgb(alpha, 40, 80, 180) : Color.FromArgb(alpha, 100, 200, 255);
+ using (var pen = new Pen(col, 1.5f))
+ {
+ pen.DashStyle = DashStyle.Dash;
+ g.DrawPolygon(pen, new[] { pts[0], pts[1], pts[2], pts[3] });
+ g.DrawPolygon(pen, new[] { pts[4], pts[5], pts[6], pts[7] });
+ g.DrawLine(pen, pts[0], pts[4]); g.DrawLine(pen, pts[1], pts[5]);
+ g.DrawLine(pen, pts[2], pts[6]); g.DrawLine(pen, pts[3], pts[7]);
+ }
+
+ if (alpha >= 100)
+ {
+ using var dimFont = new Font("Segoe UI", 11f, FontStyle.Bold);
+ var colW = whiteBg ? Color.FromArgb(200, 190, 50, 50) : Color.FromArgb(220, 255, 110, 110);
+ var colH = whiteBg ? Color.FromArgb(200, 50, 150, 50) : Color.FromArgb(220, 110, 230, 110);
+ var colD = whiteBg ? Color.FromArgb(200, 50, 50, 190) : Color.FromArgb(220, 110, 130, 255);
+
+ // midpoint helpers
+ PointF Mid(PointF a, PointF b) => new PointF((a.X + b.X) / 2, (a.Y + b.Y) / 2);
+
+ void DrawDimLabel(string text, PointF pos, Color dimCol)
+ {
+ var sz = g.MeasureString(text, dimFont);
+ float tx = pos.X - sz.Width / 2;
+ float ty = pos.Y - sz.Height / 2;
+ // dark background rect for readability
+ var bgCol = whiteBg ? Color.FromArgb(180, 255, 255, 255) : Color.FromArgb(180, 20, 20, 30);
+ using var bgBr = new SolidBrush(bgCol);
+ g.FillRectangle(bgBr, tx - 2, ty - 1, sz.Width + 4, sz.Height + 2);
+ using var br = new SolidBrush(dimCol);
+ g.DrawString(text, dimFont, br, tx, ty);
+ }
+
+ // šírka: midpoint of bottom-front edge, offset down
+ var midW = Mid(pts[0], pts[1]);
+ DrawDimLabel($"šírka {box.W:F0}", new PointF(midW.X, midW.Y + 24), colW);
+
+ // dĺžka: midpoint of bottom-right edge, offset right+down
+ var midD = Mid(pts[1], pts[5]);
+ DrawDimLabel($"dĺžka {box.D:F0}", new PointF(midD.X + 24, midD.Y + 10), colD);
+
+ // výška: midpoint of right-front edge, offset right
+ var midH = Mid(pts[1], pts[2]);
+ DrawDimLabel($"výška {box.H:F0}", new PointF(midH.X + 30, midH.Y), colH);
+ }
+ }
+
+ private PointF[] Corners8(double x0, double y0, double z0,
+ double x1, double y1, double z1,
+ double scale, double cx, double cy, double radX, double radY)
+ {
+ return new[]
+ {
+ Project(x0, y0, z0, scale, cx, cy, radX, radY),
+ Project(x1, y0, z0, scale, cx, cy, radX, radY),
+ Project(x1, y1, z0, scale, cx, cy, radX, radY),
+ Project(x0, y1, z0, scale, cx, cy, radX, radY),
+ Project(x0, y0, z1, scale, cx, cy, radX, radY),
+ Project(x1, y0, z1, scale, cx, cy, radX, radY),
+ Project(x1, y1, z1, scale, cx, cy, radX, radY),
+ Project(x0, y1, z1, scale, cx, cy, radX, radY),
+ };
+ }
+
+ private static void DrawFace(Graphics g, PointF[] corners, Color color)
+ {
+ using (var brush = new SolidBrush(Color.FromArgb(210, color)))
+ g.FillPolygon(brush, corners);
+ }
+
+ private static Color Shade(Color c, float f) => Color.FromArgb(
+ Math.Min(255, (int)(c.R * f)),
+ Math.Min(255, (int)(c.G * f)),
+ Math.Min(255, (int)(c.B * f)));
+
+ private void DrawLegend(Graphics g, int w, int h, List items, bool whiteBg)
+ {
+ if (items == null || items.Count == 0) return;
+ var unique = items
+ .GroupBy(p => p.Item.Name)
+ .Select(grp => (grp.Key, grp.First().Item.Color, grp.Count()))
+ .ToList();
+
+ int lx = 10, ly = 10;
+ var txtCol = whiteBg ? Color.Black : Color.White;
+ using (var titleFont = new Font("Segoe UI", 8.5f, FontStyle.Bold))
+ using (var itemFont = new Font("Segoe UI", 8f))
+ {
+ using (var b = new SolidBrush(txtCol))
+ g.DrawString("Legenda:", titleFont, b, lx, ly);
+ ly += 18;
+ foreach (var (name, color, cnt) in unique)
+ {
+ using (var b = new SolidBrush(color))
+ g.FillRectangle(b, lx, ly, 12, 12);
+ using (var p = new Pen(Color.Black, 0.5f))
+ g.DrawRectangle(p, lx, ly, 12, 12);
+ using (var b = new SolidBrush(txtCol))
+ g.DrawString($"{name} (×{cnt})", itemFont, b, lx + 16, ly);
+ ly += 16;
+ }
+ }
+ }
+
+ private void DrawInfo(Graphics g, int w, int h, Box box, int count, bool whiteBg)
+ {
+ string info = $"Škatuľa: {box.W} × {box.D} × {box.H} mm | Kusov: {count}";
+ var txtCol = whiteBg ? Color.FromArgb(60, 60, 60) : Color.FromArgb(120, 120, 140);
+ using (var font = new Font("Segoe UI", 8.5f))
+ using (var brush = new SolidBrush(txtCol))
+ {
+ var sf = new StringFormat { Alignment = StringAlignment.Center };
+ g.DrawString(info, font, brush, new RectangleF(0, h - 22, w, 20), sf);
+ }
+ }
+
+ public static List<(double YMin, double YMax)> GetLayers(List packed)
+ {
+ if (packed == null || packed.Count == 0) return new List<(double, double)>();
+
+ var yVals = new SortedSet();
+ yVals.Add(0);
+ foreach (var p in packed)
+ {
+ yVals.Add(Math.Round(p.Y, 3));
+ yVals.Add(Math.Round(p.Y + p.RH, 3));
+ }
+
+ var sorted = yVals.ToList();
+ var layers = new List<(double, double)>();
+ for (int i = 0; i < sorted.Count - 1; i++)
+ {
+ double yMin = sorted[i], yMax = sorted[i + 1];
+ bool hasItem = packed.Any(p =>
+ p.Y < yMax - 0.001 && p.Y + p.RH > yMin + 0.001);
+ if (hasItem)
+ layers.Add((yMin, yMax));
+ }
+
+ var merged = new List<(double, double)>();
+ if (layers.Count == 0) return merged;
+ double curMin = layers[0].Item1, curMax = layers[0].Item2;
+ for (int i = 1; i < layers.Count; i++)
+ {
+ bool newItemStartsHere = packed.Any(p => Math.Abs(p.Y - layers[i].Item1) < 0.001);
+ if (newItemStartsHere)
+ {
+ merged.Add((curMin, curMax));
+ curMin = layers[i].Item1;
+ }
+ curMax = layers[i].Item2;
+ }
+ merged.Add((curMin, curMax));
+ return merged;
+ }
+ }
+}
diff --git a/app.manifest b/app.manifest
new file mode 100644
index 0000000..c7f5f87
--- /dev/null
+++ b/app.manifest
@@ -0,0 +1,23 @@
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appsettings.json b/appsettings.json
new file mode 100644
index 0000000..bba9943
--- /dev/null
+++ b/appsettings.json
@@ -0,0 +1,36 @@
+{
+ "Database": {
+ "FilePath": "C:\\Users\\roman\\AppData\\Roaming\\BoxPacker3D\\items.db",
+ "_comment": "Relative path: database is stored in %AppData%\\BoxPacker3D\\items.db (user-writable). Absolute path overrides this behaviour (developer use)."
+ },
+ "DefaultBox": {
+ "Width": 600,
+ "Height": 400,
+ "Depth": 500,
+ "WallThickness": 20,
+ "_comment": "Default box dimensions and wall material thickness in millimetres."
+ },
+ "MaxBox": {
+ "Width": 1200,
+ "Height": 800,
+ "Depth": 1000,
+ "_comment": "Maximum allowed box dimensions used when suggesting a bigger box."
+ },
+ "Padding": {
+ "WallPadding": 10,
+ "ItemPadding": 3,
+ "_comment": "Default isolation padding in millimetres – wall-to-item and item-to-item."
+ },
+ "View": {
+ "InitialRotationX": 30,
+ "InitialRotationY": 30,
+ "InitialZoomPercent": 100,
+ "_comment": "Initial 3D view angles in degrees and zoom percentage."
+ },
+ "UI": {
+ "WindowWidth": 1340,
+ "WindowHeight": 840,
+ "LeftPanelWidth": 500,
+ "_comment": "Initial window and panel sizing."
+ }
+}
diff --git a/build-vstudio.bat b/build-vstudio.bat
new file mode 100644
index 0000000..413a628
--- /dev/null
+++ b/build-vstudio.bat
@@ -0,0 +1,53 @@
+@echo off
+REM Build script for Visual Studio Installer Projects
+REM Requires: Visual Studio 2017 with Installer Projects extension
+
+echo ================================================
+echo Building BoxPacker3D with VS Installer Projects
+echo ================================================
+
+REM Check if MSBuild is available
+where msbuild >nul 2>&1
+if errorlevel 1 (
+ echo ERROR: MSBuild not found!
+ echo.
+ echo Please run this from "Developer Command Prompt for VS 2017"
+ echo OR add MSBuild to PATH:
+ echo C:\Program Files ^(x86^)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin
+ echo.
+ pause
+ exit /b 1
+)
+
+echo.
+echo [1/3] Cleaning previous build...
+if exist "bin\Release" rmdir /s /q "bin\Release"
+if exist "obj\Release" rmdir /s /q "obj\Release"
+
+echo.
+echo [2/3] Building BoxPacker3D (Release)...
+msbuild BoxPacker3D.csproj /p:Configuration=Release /p:Platform="Any CPU" /v:minimal /nologo
+
+if errorlevel 1 (
+ echo.
+ echo ERROR: Project build failed!
+ pause
+ exit /b 1
+)
+
+echo.
+echo [3/3] Build completed successfully!
+echo.
+echo ================================================
+echo Next steps for installer:
+echo ================================================
+echo.
+echo 1. Open BoxPacker3D.sln in Visual Studio 2017
+echo 2. Add new Setup Project to solution
+echo 3. Follow VISUAL_STUDIO_INSTALLER_GUIDE.md
+echo.
+echo OR if you already have Setup project:
+echo.
+echo msbuild BoxPacker3D.Setup\BoxPacker3D.Setup.vdproj /p:Configuration=Release
+echo.
+pause
diff --git a/build-wix.bat b/build-wix.bat
new file mode 100644
index 0000000..4f80c50
--- /dev/null
+++ b/build-wix.bat
@@ -0,0 +1,79 @@
+@echo off
+setlocal EnableDelayedExpansion
+
+echo ================================================
+echo BoxPacker3D – Build MSI Installer (WiX v5)
+echo ================================================
+echo.
+
+REM ── 1. Check / install WiX v5 dotnet tool ────────────────────────────────
+where wix.exe >nul 2>&1
+if errorlevel 1 (
+ echo [1/5] Instalaacia WiX v5 ako dotnet tool...
+ dotnet tool install --global wix --version "5.*"
+ if errorlevel 1 (
+ echo CHYBA: Nepodarilo sa nainštalovat WiX. Uistite sa, ze je nainštalovany .NET SDK.
+ pause & exit /b 1
+ )
+ echo.
+) else (
+ echo [1/5] WiX je nainštalovany. OK
+)
+
+REM ── 2. Ensure required WiX extensions are installed ──────────────────────
+echo [2/5] Kontrola WiX rozšírení...
+wix extension add "WixToolset.UI.wixext/5.0.2" --global 2>nul
+wix extension add "WixToolset.Netfx.wixext/5.0.2" --global 2>nul
+wix extension add "WixToolset.Util.wixext/5.0.2" --global 2>nul
+echo.
+
+REM ── 3. Build the application (Release) ───────────────────────────────────
+echo [3/5] Build BoxPacker3D (Release)...
+dotnet build BoxPacker3D.csproj -c Release -v minimal
+if errorlevel 1 (
+ echo CHYBA: Build aplikacie zlyhał!
+ pause & exit /b 1
+)
+echo.
+
+REM ── 4. Publish win-x64 (framework-dependent, only win-x64 native DLLs) ──
+echo [4/5] Publish pre win-x64...
+if exist publish\win-x64 rmdir /s /q publish\win-x64
+dotnet publish BoxPacker3D.csproj -c Release -r win-x64 --self-contained false -o publish\win-x64 -v minimal
+if errorlevel 1 (
+ echo CHYBA: Publish zlyhał!
+ pause & exit /b 1
+)
+
+REM Verify expected native DLLs are present
+if not exist "publish\win-x64\glfw3.dll" (
+ echo UPOZORNENIE: glfw3.dll nebol najdeny v publish\win-x64\
+ echo Skuste: dotnet publish -r win-x64 --self-contained true
+)
+if not exist "publish\win-x64\e_sqlite3.dll" (
+ echo UPOZORNENIE: e_sqlite3.dll nebol najdeny v publish\win-x64\
+)
+echo.
+
+REM ── 5. Build MSI ──────────────────────────────────────────────────────────
+echo [5/5] Kompilujem MSI...
+if not exist bin mkdir bin
+wix build BoxPacker3D.wxs ^
+ -o bin\BoxPacker3D.Setup.msi ^
+ -ext WixToolset.UI.wixext ^
+ -ext WixToolset.Netfx.wixext ^
+ -ext WixToolset.Util.wixext
+
+if errorlevel 1 (
+ echo.
+ echo CHYBA: Build MSI zlyhał! Skontrolujte chybove spravy vyššie.
+ pause & exit /b 1
+)
+
+echo.
+echo ================================================
+echo HOTOVO: bin\BoxPacker3D.Setup.msi
+echo ================================================
+echo.
+explorer /select,"%~dp0bin\BoxPacker3D.Setup.msi"
+pause
diff --git a/items.db b/items.db
new file mode 100644
index 0000000..07d1025
Binary files /dev/null and b/items.db differ