From 274d607d542a3cb9091d2204fa3688cf5d16821a Mon Sep 17 00:00:00 2001 From: roman Date: Fri, 5 Jun 2026 22:02:32 +0200 Subject: [PATCH] Add project files. --- .claude/settings.local.json | 36 + AppSettings.cs | 125 +++ BoxPacker3D.Setup.vdproj | 439 +++++++++++ BoxPacker3D.csproj | 35 + BoxPacker3D.wxs | 202 +++++ BoxPacker3D_WiX.wixproj | 41 + BoxPacker3D_WithSetup.sln | 27 + Form1.cs | 1235 ++++++++++++++++++++++++++++++ GlViewer.cs | 576 ++++++++++++++ INSTALLER_OPTIONS_COMPARISON.md | 168 ++++ ItemDatabase.cs | 192 +++++ ItemDatabaseEditor.cs | 472 ++++++++++++ LayerPacker.cs | 314 ++++++++ Models.cs | 96 +++ Packer3D.cs | 23 + PdfExporter.cs | 330 ++++++++ Program.cs | 16 + README_INSTALLER.md | 128 ++++ VISUAL_STUDIO_INSTALLER_GUIDE.md | 321 ++++++++ Viewer3D.cs | 495 ++++++++++++ app.manifest | 23 + appsettings.json | 36 + build-vstudio.bat | 53 ++ build-wix.bat | 79 ++ items.db | Bin 0 -> 16384 bytes 25 files changed, 5462 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 AppSettings.cs create mode 100644 BoxPacker3D.Setup.vdproj create mode 100644 BoxPacker3D.csproj create mode 100644 BoxPacker3D.wxs create mode 100644 BoxPacker3D_WiX.wixproj create mode 100644 BoxPacker3D_WithSetup.sln create mode 100644 Form1.cs create mode 100644 GlViewer.cs create mode 100644 INSTALLER_OPTIONS_COMPARISON.md create mode 100644 ItemDatabase.cs create mode 100644 ItemDatabaseEditor.cs create mode 100644 LayerPacker.cs create mode 100644 Models.cs create mode 100644 Packer3D.cs create mode 100644 PdfExporter.cs create mode 100644 Program.cs create mode 100644 README_INSTALLER.md create mode 100644 VISUAL_STUDIO_INSTALLER_GUIDE.md create mode 100644 Viewer3D.cs create mode 100644 app.manifest create mode 100644 appsettings.json create mode 100644 build-vstudio.bat create mode 100644 build-wix.bat create mode 100644 items.db 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 0000000000000000000000000000000000000000..07d1025d6402159963e86eecfb93ad303a0dfdf4 GIT binary patch literal 16384 zcmeI%O;5rw7zgkc2pSSzZo5852*kvAHgRI3i=%_FML3ZuRhIA~GK^Q_$#3Bo^2>Pg zXjjBU!7o7mPtrDBpLXkSyEN-|+&EEmJA0T!2_3Nt_bD@jc73_)FQWsTZ6qOqKaH-&~?- zv?fOe;#~OD_MMjP_vupf$qob0k$PrJ$e>}erM{ES4n!{?>FE->E``$Rgo2u$>!!~r zy{6q2tNg2Yn2d<@!@EEEtC0&8-;I)OBWLPL zaA=yWhTc!Hi)mF!D+B}}009U<00Izz00bZa0SG_<0=p_uNH>P_|E|7X>