Add project files.

This commit is contained in:
roman
2026-06-05 22:02:32 +02:00
parent f09ad1b7c6
commit 274d607d54
25 changed files with 5462 additions and 0 deletions
+36
View File
@@ -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\")"
]
}
}
+125
View File
@@ -0,0 +1,125 @@
using System;
using System.IO;
using System.Text.Json;
namespace BoxPacker3D
{
/// <summary>
/// Strongly-typed representation of appsettings.json.
/// Loaded once at startup via <see cref="Load"/>; falls back to sensible defaults
/// if the file is missing or malformed.
/// </summary>
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;
}
/// <summary>
/// Loads appsettings.json from the application directory.
/// If the file is missing or invalid, returns defaults and writes a warning to stderr.
/// </summary>
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<AppSettings>(json, opts) ?? new AppSettings();
}
catch (Exception ex)
{
Console.Error.WriteLine($"[AppSettings] Failed to parse: {ex.Message} using defaults.");
return new AppSettings();
}
}
/// <summary>
/// Resolves the writable database path.
///
/// Priority:
/// 1. If FilePath is absolute → use it as-is (developer override).
/// 2. Otherwise → store in %AppData%\BoxPacker3D\&lt;filename&gt; 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.
/// </summary>
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;
}
}
}
}
+439
View File
@@ -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:<VsdDialogDir>\\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:<VsdDialogDir>\\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:<VsdDialogDir>\\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:<VsdDialogDir>\\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:<VsdDialogDir>\\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:<VsdDialogDir>\\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"
{
}
}
}
+35
View File
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>disable</ImplicitUsings>
<AssemblyName>BoxPacker3D</AssemblyName>
<RootNamespace>BoxPacker3D</RootNamespace>
<ApplicationIcon></ApplicationIcon>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK" Version="4.8.2" />
<PackageReference Include="OpenTK.Graphics" Version="4.8.2" />
<PackageReference Include="OpenTK.Windowing.Common" Version="4.8.2" />
<PackageReference Include="OpenTK.Windowing.Desktop" Version="4.8.2" />
<PackageReference Include="OpenTK.Mathematics" Version="4.8.2" />
<PackageReference Include="OpenTK.WinForms" Version="4.0.0-pre.5" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="items.db">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+202
View File
@@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
WiX v5 installer for BoxPacker3D
Build: run build-wix.bat
Requires: dotnet SDK (WiX installed automatically as dotnet tool)
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:netfx="http://wixtoolset.org/schemas/v4/wxs/netfx"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Name="BoxPacker3D"
Language="1033"
Version="1.0.0"
Manufacturer="BoxPacker3D"
UpgradeCode="73799267-8F2E-4994-AE60-A0CFA8390BEE"
Scope="perMachine"
Codepage="65001">
<SummaryInformation Description="BoxPacker3D 3D bin-packing application" />
<MajorUpgrade DowngradeErrorMessage="Novšia verzia [ProductName] je už nainštalovaná." />
<MediaTemplate EmbedCab="yes" />
<!-- Require .NET 9 Windows Desktop Runtime (x64) -->
<netfx:DotNetCompatibilityCheck Property="DOTNETDESKTOP9_OK"
RuntimeType="desktop"
Platform="x64"
Version="9.0.0"
RollForward="latestMinor" />
<Launch Condition="DOTNETDESKTOP9_OK"
Message="Táto aplikácia vyžaduje .NET 9.0 Windows Desktop Runtime (x64).&#13;&#10;Stiahnite ho z: https://dotnet.microsoft.com/download/dotnet/9.0" />
<!-- Install-dir UI with optional launch checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Spustiť BoxPacker3D" />
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
<!-- Launch app after install when checkbox is ticked -->
<CustomAction Id="LaunchApp"
FileRef="BoxPacker3D_exe"
ExeCommand=""
Execute="immediate"
Impersonate="yes"
Return="asyncNoWait" />
<InstallExecuteSequence>
<Custom Action="LaunchApp" After="InstallFinalize"
Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" />
</InstallExecuteSequence>
<Feature Id="Main" Title="BoxPacker3D" Level="1">
<ComponentGroupRef Id="AppFiles" />
<ComponentRef Id="Comp_Shortcut_StartMenu" />
<ComponentRef Id="Comp_Shortcut_Desktop" />
</Feature>
</Package>
<!-- ── Directory structure ─────────────────────────────────────────────── -->
<Fragment>
<StandardDirectory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="BoxPacker3D" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="StartMenuDir" Name="BoxPacker3D" />
</StandardDirectory>
<StandardDirectory Id="DesktopFolder" />
</Fragment>
<!-- ── Application files (from: dotnet publish -r win-x64) ───────────── -->
<Fragment>
<ComponentGroup Id="AppFiles" Directory="INSTALLFOLDER">
<!-- Main executable -->
<Component Id="Comp_MainExe">
<File Id="BoxPacker3D_exe"
Source="publish\win-x64\BoxPacker3D.exe"
KeyPath="yes" />
</Component>
<Component Id="Comp_MainDll">
<File Source="publish\win-x64\BoxPacker3D.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_DepsJson">
<File Source="publish\win-x64\BoxPacker3D.deps.json" KeyPath="yes" />
</Component>
<Component Id="Comp_RuntimeConfig">
<File Source="publish\win-x64\BoxPacker3D.runtimeconfig.json" KeyPath="yes" />
</Component>
<!-- OpenTK managed assemblies -->
<Component Id="Comp_OTK_Audio">
<File Source="publish\win-x64\OpenTK.Audio.OpenAL.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_Compute">
<File Source="publish\win-x64\OpenTK.Compute.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_Core">
<File Source="publish\win-x64\OpenTK.Core.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_Gfx">
<File Source="publish\win-x64\OpenTK.Graphics.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_Input">
<File Source="publish\win-x64\OpenTK.Input.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_Math">
<File Source="publish\win-x64\OpenTK.Mathematics.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_WinForms">
<File Source="publish\win-x64\OpenTK.WinForms.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_WndCom">
<File Source="publish\win-x64\OpenTK.Windowing.Common.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_WndDsk">
<File Source="publish\win-x64\OpenTK.Windowing.Desktop.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_OTK_GLFW">
<File Source="publish\win-x64\OpenTK.Windowing.GraphicsLibraryFramework.dll" KeyPath="yes" />
</Component>
<!-- SQLite managed assemblies -->
<Component Id="Comp_SQLiteData">
<File Source="publish\win-x64\Microsoft.Data.Sqlite.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_SQLiteRawBat">
<File Source="publish\win-x64\SQLitePCLRaw.batteries_v2.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_SQLiteRawCore">
<File Source="publish\win-x64\SQLitePCLRaw.core.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_SQLiteRawProv">
<File Source="publish\win-x64\SQLitePCLRaw.provider.e_sqlite3.dll" KeyPath="yes" />
</Component>
<!-- Native DLLs (placed to root by dotnet publish -r win-x64) -->
<Component Id="Comp_Glfw3">
<File Source="publish\win-x64\glfw3.dll" KeyPath="yes" />
</Component>
<Component Id="Comp_Sqlite3">
<File Source="publish\win-x64\e_sqlite3.dll" KeyPath="yes" />
</Component>
<!-- Config overwrite on reinstall -->
<Component Id="Comp_AppSettings">
<File Source="publish\win-x64\appsettings.json" KeyPath="yes" />
</Component>
<!-- Seed database copied to %AppData%\BoxPacker3D\ on first run by the app.
Installed here as read-only template; user data lives in AppData. -->
<Component Id="Comp_ItemsDb">
<File Source="publish\win-x64\items.db" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
<!-- ── Start-menu shortcut ────────────────────────────────────────────── -->
<Fragment>
<DirectoryRef Id="StartMenuDir">
<Component Id="Comp_Shortcut_StartMenu">
<Shortcut Id="StartMenuShortcut"
Name="BoxPacker3D"
Description="3D Box Packer aplikácia pre 3D balenie kusov"
Target="[INSTALLFOLDER]BoxPacker3D.exe"
WorkingDirectory="INSTALLFOLDER"
Icon="BoxPacker3D_ico"
IconIndex="0" />
<RemoveFolder Id="CleanStartMenuDir" On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\BoxPacker3D"
Name="StartMenu"
Type="integer" Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Fragment>
<!-- ── Desktop shortcut ───────────────────────────────────────────────── -->
<Fragment>
<StandardDirectory Id="DesktopFolder">
<Component Id="Comp_Shortcut_Desktop">
<Shortcut Id="DesktopShortcut"
Name="BoxPacker3D"
Description="3D Box Packer aplikácia pre 3D balenie kusov"
Target="[INSTALLFOLDER]BoxPacker3D.exe"
WorkingDirectory="INSTALLFOLDER"
Icon="BoxPacker3D_ico"
IconIndex="0" />
<RegistryValue Root="HKCU"
Key="Software\BoxPacker3D"
Name="Desktop"
Type="integer" Value="1"
KeyPath="yes" />
</Component>
</StandardDirectory>
</Fragment>
<!-- ── Icon ───────────────────────────────────────────────────────────── -->
<Fragment>
<Icon Id="BoxPacker3D_ico" SourceFile="publish\win-x64\BoxPacker3D.exe" />
</Fragment>
</Wix>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" InitialTargets="EnsureWixToolsetInstalled" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>3.10</ProductVersion>
<ProjectGuid>a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6</ProjectGuid>
<SchemaVersion>2.0</SchemaVersion>
<OutputName>BoxPacker3D.Setup</OutputName>
<OutputType>Package</OutputType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<OutputPath>bin\$(Configuration)\</OutputPath>
<IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath>
<DefineConstants>Debug</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<OutputPath>bin\$(Configuration)\</OutputPath>
<IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="BoxPacker3D.wxs" />
</ItemGroup>
<ItemGroup>
<WixExtension Include="WixUIExtension">
<HintPath>$(WixExtDir)\WixUIExtension.dll</HintPath>
<Name>WixUIExtension</Name>
</WixExtension>
</ItemGroup>
<Import Project="$(WixTargetsPath)" Condition=" '$(WixTargetsPath)' != '' " />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets" Condition=" '$(WixTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets') " />
<Target Name="EnsureWixToolsetInstalled" Condition=" '$(WixTargetsImported)' != 'true' ">
<Error Text="The WiX Toolset v3.11 (or newer) build tools must be installed to build this project. To download the WiX Toolset, see http://wixtoolset.org/releases/" />
</Target>
</Project>
+27
View File
@@ -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
+1235
View File
File diff suppressed because it is too large Load Diff
+576
View File
@@ -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<PackedItem> _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<PackedItem> items, Box box,
double rotX, double rotY, double zoom,
double wallPad = 0, double itemPad = 0)
{
_items = items ?? new List<PackedItem>();
_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<PackedItem>();
_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<ViewArgs>? 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();
}
}
}
+168
View File
@@ -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! 🎉
+192
View File
@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Data.Sqlite;
namespace BoxPacker3D
{
/// <summary>
/// Persistent template of an item that can be re-used across packing sessions.
/// Stored in an on-disk SQLite database.
/// </summary>
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)";
}
/// <summary>
/// Simple SQLite-backed repository for <see cref="ItemTemplate"/>s.
/// Database schema is created on first use.
/// </summary>
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;
}
/// <summary>Returns all templates ordered alphabetically by name.</summary>
public List<ItemTemplate> GetAll()
{
var list = new List<ItemTemplate>();
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;
}
/// <summary>Returns the template with the given name or null if none exists.</summary>
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;
}
/// <summary>Inserts or replaces a template (upsert by Name).</summary>
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();
}
/// <summary>
/// Checks if a template with the same name or exact dimensions already exists.
/// Returns the existing template if found, null otherwise.
/// </summary>
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;
}
}
}
+472
View File
@@ -0,0 +1,472 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace BoxPacker3D
{
/// <summary>
/// Modal dialog for managing the item template database.
/// Allows adding, editing, and deleting saved item templates.
/// </summary>
public class ItemDatabaseEditor : Form
{
private ItemDatabase _db;
private DataGridView _grid;
private TextBox _txtSearch;
private Button _btnAdd, _btnEdit, _btnDelete, _btnClose;
private List<ItemTemplate> _allTemplates = new List<ItemTemplate>();
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);
}
}
}
}
/// <summary>
/// Small dialog for adding/editing a single item template.
/// </summary>
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)
};
}
}
}
+314
View File
@@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace BoxPacker3D
{
/// <summary>
/// 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).
/// </summary>
public class LayerPacker
{
private const double EPS = 0.001;
public (List<PackedItem> packed, Box box, List<Item> unpacked) Pack(
List<Item> allItems,
Box minBox, Box maxBox,
double wallPad, double itemPad, double wallThickness)
{
var packed = new List<PackedItem>();
var unpacked = new List<Item>();
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 46: 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<Item>(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 ───────────────────────────────────────────────────────────────
/// <summary>
/// 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.
/// </summary>
private static void FillRectangle(
Queue<Item> queue, List<PackedItem> 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
}
}
/// <summary>
/// Two allowed orientations: original and rotated 90° around vertical axis (W↔D).
/// H is always preserved — items cannot be tilted sideways.
/// </summary>
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);
}
/// <summary>
/// Shrinks the box to just enclose all packed items plus edge on every side.
/// </summary>
private static Box TightenBox(
List<PackedItem> 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);
}
}
}
+96
View File
@@ -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; }
/// <summary>Thickness of the cardboard/material wall in the same units (mm).</summary>
public double WallThickness { get; set; }
public Box(double w, double h, double d, double wallThickness = 0)
{
W = w; H = h; D = d; WallThickness = wallThickness;
}
/// <summary>Outer volume.</summary>
public double Volume => W * H * D;
/// <summary>Usable interior volume (after subtracting wall thickness on all 6 sides).</summary>
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;
}
}
}
+23
View File
@@ -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<PackedItem> Pack(List<Item> items, Box box,
double wallPad = 0, double itemPad = 0, Action<int, int>? onProgress = null,
bool allowRotation = true, bool preferRotated = false)
{
// ... (Extreme Points algorithm)
}
// ... helper methods
}
}
*/
+330
View File
@@ -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<BoxSolution> solutions,
List<Bitmap> overviews,
List<Bitmap> layerBitmaps,
List<(double YMin, double YMax)> layers,
double wallPad, double itemPad,
List<Item> unpackedItems = null)
{
var pages = new List<byte[]>();
// 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"); }
}
}
}
+16
View File
@@ -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());
}
}
}
+128
View File
@@ -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. 🎉
+321
View File
@@ -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!
+495
View File
@@ -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<PackedItem>();
}
private List<PackedItem> _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<PackedItem>();
_box = null;
panel.Invalidate();
}
public void Render(List<PackedItem> 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<PackedItem> 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<PackedItem> 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<PackedItem> 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<PackedItem> 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<PackedItem> packed)
{
if (packed == null || packed.Count == 0) return new List<(double, double)>();
var yVals = new SortedSet<double>();
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;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="BoxPacker3D"
type="win32"
/>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 / 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
</application>
</compatibility>
</assembly>
+36
View File
@@ -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."
}
}
+53
View File
@@ -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
+79
View File
@@ -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
BIN
View File
Binary file not shown.