Add project files.
This commit is contained in:
@@ -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
@@ -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\<filename> so the
|
||||
/// file is always user-writable, even when the app is installed in
|
||||
/// Program Files (which is read-only for normal users).
|
||||
///
|
||||
/// On first run the seed database is copied from the application directory
|
||||
/// to AppData so the user starts with the pre-built item catalogue.
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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). 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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
+576
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 4–6: place first type ───────────────────────────────────────
|
||||
var (proto0, items0) = types[0];
|
||||
|
||||
bool firstPlaced = false;
|
||||
double extW = 0, extH = 0, extD = 0;
|
||||
|
||||
// Open (incomplete) last layer state
|
||||
double openLayerY = double.NaN;
|
||||
int openFilled = 0; // how many slots are already occupied
|
||||
int openCapacity = 0; // total slots in that layer (nW×nD)
|
||||
double openIW = 0, openIH = 0, openID = 0;
|
||||
int openNW = 0, openND = 0;
|
||||
|
||||
foreach (var (iW, iH, iD) in Orientations(proto0))
|
||||
{
|
||||
// Step 4: snap D to whole number of items (based on estimated D)
|
||||
int nD = Math.Max(1, (int)((estID + itemPad) / (iD + itemPad)));
|
||||
nD = Math.Min(nD, (int)((maxID + itemPad) / (iD + itemPad)));
|
||||
if (nD < 1) continue;
|
||||
|
||||
// Step 5: snap W to whole number of items (based on estimated W)
|
||||
int nW = Math.Max(1, (int)((estIW + itemPad) / (iW + itemPad)));
|
||||
nW = Math.Min(nW, (int)((maxIW + itemPad) / (iW + itemPad)));
|
||||
if (nW < 1) continue;
|
||||
|
||||
// Step 6: H layers needed for all items; cap at maxBox.H
|
||||
int nH = (int)Math.Ceiling((double)items0.Count / (nD * nW));
|
||||
nH = Math.Min(nH, (int)((maxIH + itemPad) / (iH + itemPad)));
|
||||
if (nH < 1) continue;
|
||||
|
||||
// Place items layer by layer from bottom to top
|
||||
int idx = 0;
|
||||
for (int iy = 0; iy < nH && idx < items0.Count; iy++)
|
||||
for (int iz = 0; iz < nD && idx < items0.Count; iz++)
|
||||
for (int ix = 0; ix < nW && idx < items0.Count; ix++)
|
||||
{
|
||||
packed.Add(new PackedItem(items0[idx++],
|
||||
edge + ix * (iW + itemPad),
|
||||
edge + iy * (iH + itemPad),
|
||||
edge + iz * (iD + itemPad),
|
||||
iW, iH, iD));
|
||||
}
|
||||
|
||||
// Items that didn't fit (nH was capped) → unpacked
|
||||
for (int i = idx; i < items0.Count; i++)
|
||||
unpacked.Add(items0[i]);
|
||||
|
||||
extW = nW * (iW + itemPad) - itemPad;
|
||||
extH = nH * (iH + itemPad) - itemPad;
|
||||
extD = nD * (iD + itemPad) - itemPad;
|
||||
|
||||
// Track open last layer if it was not completely filled.
|
||||
// lastFilled == 0 when: (a) last layer is perfectly full, OR
|
||||
// (b) nH was capped so idx = nH*perLayer0 (exact multiple) — in
|
||||
// that case the last placed layer IS full, nothing to fill.
|
||||
// In both cases there is no open layer → openLayerY stays NaN.
|
||||
int perLayer0 = nW * nD;
|
||||
int lastFilled = idx % perLayer0;
|
||||
if (lastFilled > 0)
|
||||
{
|
||||
openLayerY = edge + (nH - 1) * (iH + itemPad);
|
||||
openFilled = lastFilled;
|
||||
openCapacity = perLayer0;
|
||||
openIW = iW; openIH = iH; openID = iD;
|
||||
openNW = nW; openND = nD;
|
||||
}
|
||||
|
||||
Debug.WriteLine(
|
||||
$"[LP] Type0 nW={nW} nD={nD} nH={nH} perLayer={perLayer0} " +
|
||||
$"idx={idx} lastFilled={lastFilled} " +
|
||||
$"openLayerY={openLayerY:F1} openIW={openIW} openIH={openIH} openID={openID}");
|
||||
|
||||
Debug.WriteLine($"[LayerPacker] Type0: nW={nW} nD={nD} nH={nH} perLayer={nW*nD} idx={idx} lastFilled={idx%(nW*nD)} openLayerY={openLayerY:F1} openIW={openIW} openIH={openIH} openID={openID}");
|
||||
|
||||
firstPlaced = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!firstPlaced)
|
||||
{
|
||||
unpacked.AddRange(allItems);
|
||||
return (packed, minBox, unpacked);
|
||||
}
|
||||
|
||||
// ── Step 7: subsequent types — fill open layer then full layers on top ─
|
||||
for (int ti = 1; ti < types.Count; ti++)
|
||||
{
|
||||
var (proto, typeItems) = types[ti];
|
||||
var queue = new Queue<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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. 🎉
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user