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