├── .gitignore ├── LICENSE ├── README.md ├── RectpackSharp.sln ├── RectpackSharp ├── PackingException.cs ├── PackingHints.cs ├── PackingRectangle.cs ├── RectanglePacker.cs └── RectpackSharp.csproj ├── Tests ├── Program.cs └── Tests.csproj ├── images ├── rectangles_random1.png ├── rectangles_random2.png ├── rectangles_random65536.jpeg ├── rectangles_similar.png ├── rectangles_spritesheet.png ├── rectangles_spritesheet2.png └── rectangles_squares.png └── packsharp-logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ThomasMiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RectpackSharp 2 | [![NuGet](https://img.shields.io/nuget/v/RectpackSharp)](https://nuget.org/packages/RectpackSharp) 3 | 4 | A rectangle packing library made in C# for .NET Standard. 5 | 6 | Loosely based on the well-known C++ [rectpack-2D library](https://github.com/TeamHypersomnia/rectpack2D) 7 | 8 | This started as a side-project for the [TrippyGL graphics library](https://github.com/ThomasMiz/TrippyGL) but as it grew, I decided to give it it's own repository and open the project for everyone to use. 9 | 10 | The libary is quite small, so you can even just chuck the files directly onto your project if you don't want additional DLLs! 11 | 12 | ## Usage 13 | 14 | Once you have the library, just add ``using RectpackSharp`` to access the library types. 15 | 16 | You will see the ``PackingRectangle`` type and the ``RectanglePacker`` static class. 17 | 18 | Create a ``PackingRectangle`` for each rectangle you want and put them all in a single array. You can identify your rectangles using the ``PackingRectangle.Id`` field, as the order of the rectangles is not preserved. Afterwards, just call ``RectanglePacker.Pack``. That's it! 19 | 20 | ```cs 21 | PackingRectangle[] rectangles = new PackingRectangle[amount]; 22 | // Set the width and height of your rectangles 23 | // ... 24 | 25 | RectanglePacker.Pack(rectangles, out PackingRectangle bounds); 26 | // All the rectangles in the array were assigned X and Y values. Bounds contains the width and height of the bin. 27 | ``` 28 | 29 | Specifying no extra parameters means the library will try all it's tools in order to find the best bin it can. If performance is important, you can trade space efficiency for performance with the optional parameters: 30 | 31 | * ``packingHint`` allows you to specify which methods to try when packing. Default is `PackingHints.FindBest`. 32 | * ``acceptableDensity`` makes the library stop searching once it found a solution with said density or better. Density is calculated as usedArea/binArea, so a density of 0 will yield the fastest solution it can, but possibly not an efficient one. Default is 1. 33 | * ``stepSize`` is by how much to vary the bin size after each try. Higher values might be faster but skip possibly better solutions. Default is 1. 34 | 35 | So for example, if you know all your rectangles are squares, you might wanna try 36 | ```cs 37 | RectanglePacker.Pack(rectangles, out PackingRectangle bounds, PackingHints.Width, 1, 1); 38 | ``` 39 | 40 | `Pack` also provides two additional arguments of type `uint?`, called `maxBoundsWidth` and `maxBoundsHeight`. These may be used to constrain the resulting bin to a given width and/or height. If, for example, you want a max bin height of 500, you may do something like this: 41 | ```cs 42 | RectanglePacker.Pack(rectangles, out PackingRectangle bounds, PackingHints.FindBest, 1, 1, null, 500); 43 | ``` 44 | 45 | 46 | ## Need Help? 47 | Feel free to come ask questions over at the [TrippyGL Discord server](https://discord.gg/3j5Q4zN)! 48 | 49 | ## Gallery 50 | 51 | Here's a test case where the rectangles have relatively similar dimentions. 52 | ![](https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/main/images/rectangles_similar.png) 53 | 54 | In this test case, all the squares are the same size. Currently, the library doesn't handle the edges very well on these cases. 55 | ![](https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/main/images/rectangles_squares.png) 56 | 57 | It also works like a charm for texture atlases or sprite sheets! 58 | ![](https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/main/images/rectangles_spritesheet2.png) 59 | ![](https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/main/images/rectangles_spritesheet.png) 60 | 61 | The most complicated cases are when the rectangles have very irregular dimentions, because there's no good answer to "what to put where". 62 | For these next test cases, we simply generated 512 or 2048 random rectangles (each side being from 20 to 200) and packed them. 63 | ![](https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/main/images/rectangles_random1.png) 64 | ![](https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/main/images/rectangles_random2.png) 65 | 66 | Fuck it, here's 65536 random rectangles in a ~24k x 24k bin. 67 | ![](https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/main/images/rectangles_random65536.jpeg) 68 | -------------------------------------------------------------------------------- /RectpackSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30204.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RectpackSharp", "RectpackSharp\RectpackSharp.csproj", "{6FA7B27A-CF8E-4155-897B-640D105C6E3B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{CD9AF60B-AF65-47CF-8F6E-BBB25B0370E6}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {6FA7B27A-CF8E-4155-897B-640D105C6E3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {6FA7B27A-CF8E-4155-897B-640D105C6E3B}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {6FA7B27A-CF8E-4155-897B-640D105C6E3B}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {6FA7B27A-CF8E-4155-897B-640D105C6E3B}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {CD9AF60B-AF65-47CF-8F6E-BBB25B0370E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {CD9AF60B-AF65-47CF-8F6E-BBB25B0370E6}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {CD9AF60B-AF65-47CF-8F6E-BBB25B0370E6}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {CD9AF60B-AF65-47CF-8F6E-BBB25B0370E6}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {F04AE7AE-EF14-4929-94F4-E898EF0AA5D1} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /RectpackSharp/PackingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RectpackSharp 4 | { 5 | public class PackingException : Exception 6 | { 7 | public PackingException() : base() { } 8 | 9 | public PackingException(string message) : base(message) { } 10 | 11 | public PackingException(string message, Exception innerException) : base(message, innerException) { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RectpackSharp/PackingHints.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RectpackSharp 4 | { 5 | /// 6 | /// Specifies hints that help optimize the rectangle packing algorithm. 7 | /// 8 | [Flags] 9 | public enum PackingHints 10 | { 11 | /// Tells the rectangle packer to try inserting the rectangles ordered by area. 12 | TryByArea = 1, 13 | 14 | /// Tells the rectangle packer to try inserting the rectangles ordered by perimeter. 15 | TryByPerimeter = 2, 16 | 17 | /// Tells the rectangle packer to try inserting the rectangles ordered by bigger side. 18 | TryByBiggerSide = 4, 19 | 20 | /// Tells the rectangle packer to try inserting the rectangles ordered by width. 21 | TryByWidth = 8, 22 | 23 | /// Tells the rectangle packer to try inserting the rectangles ordered by height. 24 | TryByHeight = 16, 25 | 26 | /// Tells the rectangle packer to try inserting the rectangles ordered by a pathological multiplier. 27 | TryByPathologicalMultiplier = 32, 28 | 29 | /// Specifies to try all the possible hints, as to find the best packing configuration. 30 | FindBest = TryByArea | TryByPerimeter | TryByBiggerSide | TryByWidth | TryByHeight | TryByPathologicalMultiplier, 31 | 32 | /// Specifies hints to optimize for rectangles who have one side much bigger than the other. 33 | UnusualSizes = TryByPerimeter | TryByBiggerSide | TryByPathologicalMultiplier, 34 | 35 | /// Specifies hints to optimize for rectangles whose sides are relatively similar. 36 | MostlySquared = TryByArea | TryByBiggerSide | TryByWidth | TryByHeight, 37 | } 38 | 39 | /// 40 | /// Provides internal values and functions used by the rectangle packing algorithm. 41 | /// 42 | internal static class PackingHintExtensions 43 | { 44 | /// 45 | /// Represents a method for calculating a sort key from a . 46 | /// 47 | /// The whose sort key to calculate. 48 | /// The value that should be assigned to . 49 | private delegate uint GetSortKeyDelegate(in PackingRectangle rectangle); 50 | 51 | /// The maximum amount of hints that can be specified by a . 52 | internal const int MaxHintCount = 6; 53 | 54 | public static uint GetArea(in PackingRectangle rectangle) => rectangle.Area; 55 | public static uint GetPerimeter(in PackingRectangle rectangle) => rectangle.Perimeter; 56 | public static uint GetBiggerSide(in PackingRectangle rectangle) => rectangle.BiggerSide; 57 | public static uint GetWidth(in PackingRectangle rectangle) => rectangle.Width; 58 | public static uint GetHeight(in PackingRectangle rectangle) => rectangle.Height; 59 | public static uint GetPathologicalMultiplier(in PackingRectangle rectangle) => rectangle.PathologicalMultiplier; 60 | 61 | /// 62 | /// Separates a into the multiple options it contains, 63 | /// saving each of those separately onto a . 64 | /// 65 | /// The to separate. 66 | /// The span in which to write the resulting hints. This span's excess will be sliced. 67 | public static void GetFlagsFrom(PackingHints packingHint, ref Span span) 68 | { 69 | int index = 0; 70 | if (packingHint.HasFlag(PackingHints.TryByArea)) 71 | span[index++] = PackingHints.TryByArea; 72 | if (packingHint.HasFlag(PackingHints.TryByPerimeter)) 73 | span[index++] = PackingHints.TryByPerimeter; 74 | if (packingHint.HasFlag(PackingHints.TryByBiggerSide)) 75 | span[index++] = PackingHints.TryByBiggerSide; 76 | if (packingHint.HasFlag(PackingHints.TryByWidth)) 77 | span[index++] = PackingHints.TryByWidth; 78 | if (packingHint.HasFlag(PackingHints.TryByHeight)) 79 | span[index++] = PackingHints.TryByHeight; 80 | if (packingHint.HasFlag(PackingHints.TryByPathologicalMultiplier)) 81 | span[index++] = PackingHints.TryByPathologicalMultiplier; 82 | span = span.Slice(0, index); 83 | } 84 | 85 | /// 86 | /// Sorts the given array using the specified . 87 | /// 88 | /// The rectangles to sort. 89 | /// The hint to sort by. Must be a single bit value. 90 | /// 91 | /// The values will be modified. 92 | /// 93 | #if NET5_0_OR_GREATER 94 | public static void SortByPackingHint(Span rectangles, PackingHints packingHint) 95 | #elif NETSTANDARD2_0 96 | public static void SortByPackingHint(PackingRectangle[] rectangles, PackingHints packingHint) 97 | #endif 98 | { 99 | // We first get the appropiate delegate for getting a rectangle's sort key. 100 | GetSortKeyDelegate getKeyDelegate; 101 | switch (packingHint) 102 | { 103 | case PackingHints.TryByArea: 104 | getKeyDelegate = GetArea; 105 | break; 106 | case PackingHints.TryByPerimeter: 107 | getKeyDelegate = GetPerimeter; 108 | break; 109 | case PackingHints.TryByBiggerSide: 110 | getKeyDelegate = GetBiggerSide; 111 | break; 112 | case PackingHints.TryByWidth: 113 | getKeyDelegate = GetWidth; 114 | break; 115 | case PackingHints.TryByHeight: 116 | getKeyDelegate = GetHeight; 117 | break; 118 | case PackingHints.TryByPathologicalMultiplier: 119 | getKeyDelegate = GetPathologicalMultiplier; 120 | break; 121 | default: 122 | throw new ArgumentException(nameof(packingHint)); 123 | }; 124 | 125 | // We use the getKeyDelegate to set the sort keys for all the rectangles. 126 | for (int i = 0; i < rectangles.Length; i++) 127 | rectangles[i].SortKey = getKeyDelegate(rectangles[i]); 128 | 129 | // We sort the array, using the default rectangle comparison (which compares sort keys). 130 | #if NET5_0_OR_GREATER 131 | rectangles.Sort(); 132 | #elif NETSTANDARD2_0 133 | Array.Sort(rectangles); 134 | #endif 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /RectpackSharp/PackingRectangle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | namespace RectpackSharp 5 | { 6 | /// 7 | /// A rectangle that can be used for a rectangle packing operation. 8 | /// 9 | public struct PackingRectangle : IEquatable, IComparable 10 | { 11 | /// 12 | /// A value that can be used to identify this . This value is 13 | /// never touched by the rectangle packing algorithm. 14 | /// 15 | public int Id; 16 | 17 | /// A value used internally by the packing algorithm for sorting rectangles. 18 | public uint SortKey; 19 | 20 | /// The X coordinate of the left edge of this . 21 | public uint X; 22 | 23 | /// The Y coordinate of the top edge of this . 24 | public uint Y; 25 | 26 | /// The width of this . 27 | public uint Width; 28 | 29 | /// The height of this . 30 | public uint Height; 31 | 32 | /// 33 | /// Gets or sets the X coordinate of the right edge of this . 34 | /// 35 | /// Setting this will only modify the . 36 | public uint Right 37 | { 38 | get => X + Width; 39 | set => Width = value - X; 40 | } 41 | 42 | /// 43 | /// Gets or sets the Y coordinate of the bottom edge of this . 44 | /// 45 | /// Setting this will only modify the . 46 | public uint Bottom 47 | { 48 | get => Y + Height; 49 | set => Height = value - Y; 50 | } 51 | 52 | /// Calculates this 's area. 53 | public uint Area => Width * Height; 54 | 55 | /// Calculates this 's perimeter. 56 | public uint Perimeter => Width + Width + Height + Height; 57 | 58 | /// Gets this 's bigger side. 59 | public uint BiggerSide => Math.Max(Width, Height); 60 | 61 | /// Calculates this 's pathological multiplier. 62 | /// This is calculated as: max(width, height) / min(width, height) * width * height 63 | public uint PathologicalMultiplier => (Width > Height ? (Width / Height) : (Height / Width)) * Width * Height; 64 | 65 | /// 66 | /// Creates a with the specified values. 67 | /// 68 | public PackingRectangle(uint x, uint y, uint width, uint height, int id = 0) 69 | { 70 | X = x; 71 | Y = y; 72 | Width = width; 73 | Height = height; 74 | Id = id; 75 | SortKey = 0; 76 | } 77 | 78 | /// 79 | /// Creates a from a . 80 | /// 81 | public PackingRectangle(Rectangle rectangle, int id = 0) 82 | : this((uint)rectangle.X, (uint)rectangle.Y, (uint)rectangle.Width, (uint)rectangle.Height, id) 83 | { 84 | 85 | } 86 | 87 | public static implicit operator Rectangle(PackingRectangle rectangle) 88 | => new Rectangle((int)rectangle.X, (int)rectangle.Y, (int)rectangle.Width, (int)rectangle.Height); 89 | 90 | public static implicit operator PackingRectangle(Rectangle rectangle) 91 | => new PackingRectangle((uint)rectangle.X, (uint)rectangle.Y, (uint)rectangle.Width, (uint)rectangle.Height); 92 | 93 | public static bool operator ==(PackingRectangle left, PackingRectangle right) => left.Equals(right); 94 | public static bool operator !=(PackingRectangle left, PackingRectangle right) => !left.Equals(right); 95 | 96 | /// 97 | /// Returns whether the given is contained 98 | /// entirely within this . 99 | /// 100 | public bool Contains(in PackingRectangle other) 101 | { 102 | return X <= other.X && Y <= other.Y && Right >= other.Right && Bottom >= other.Bottom; 103 | } 104 | 105 | /// 106 | /// Returns whether the given intersects with 107 | /// this . 108 | /// 109 | public bool Intersects(in PackingRectangle other) 110 | { 111 | return other.X < X + Width && X < (other.X + other.Width) 112 | && other.Y < Y + Height && Y < other.Y + other.Height; 113 | } 114 | 115 | /// 116 | /// Calculates the intersection of this with another. 117 | /// 118 | public PackingRectangle Intersection(in PackingRectangle other) 119 | { 120 | uint x1 = Math.Max(X, other.X); 121 | uint x2 = Math.Min(Right, other.Right); 122 | uint y1 = Math.Max(Y, other.Y); 123 | uint y2 = Math.Min(Bottom, other.Bottom); 124 | 125 | if (x2 >= x1 && y2 >= y1) 126 | return new PackingRectangle(x1, y1, x2 - x1, y2 - y1); 127 | return default; 128 | } 129 | 130 | public override string ToString() 131 | { 132 | return string.Concat("{ X=", X.ToString(), ", Y=", Y.ToString(), ", Width=", Width.ToString() + ", Height=", Height.ToString(), ", Id=", Id.ToString(), " }"); 133 | } 134 | 135 | public override int GetHashCode() 136 | { 137 | return HashCode.Combine(X, Y, Width, Height, Id); 138 | } 139 | 140 | public bool Equals(PackingRectangle other) 141 | { 142 | return X == other.X && Y == other.Y && Width == other.Width 143 | && Height == other.Height && Id == other.Id; 144 | } 145 | 146 | public override bool Equals(object obj) 147 | { 148 | if (obj is PackingRectangle viewport) 149 | return Equals(viewport); 150 | return false; 151 | } 152 | 153 | /// 154 | /// Compares this with another 's. 155 | /// 156 | public int CompareTo(PackingRectangle other) 157 | { 158 | return -SortKey.CompareTo(other.SortKey); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /RectpackSharp/RectanglePacker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RectpackSharp 5 | { 6 | /// 7 | /// A static class providing functionality for packing rectangles into a bin as small as possible. 8 | /// 9 | public static class RectanglePacker 10 | { 11 | /// A weak reference to the last list used, so it can be reused in subsequent packs. 12 | private static WeakReference> oldListReference; 13 | private static readonly object oldListReferenceLock = new object(); 14 | 15 | /// 16 | /// Finds a way to pack all the given rectangles into a single bin. Performance can be traded for 17 | /// space efficiency by using the optional parameters. 18 | /// 19 | /// The rectangles to pack. The result is saved onto this array. 20 | /// The bounds of the resulting bin. This will always be at X=Y=0. 21 | /// Specifies hints for optimizing performance. 22 | /// Searching stops once a bin is found with this density (usedArea/boundsArea) or better. 23 | /// The amount by which to increment/decrement size when trying to pack another bin. 24 | /// The maximum allowed width for the resulting bin, or null for no limit. 25 | /// The maximum allowed height for the resulting bin, or null for no limit. 26 | /// 27 | /// The values are never touched. Use this to identify your rectangles. 28 | /// 29 | #if NET5_0_OR_GREATER 30 | public static void Pack(Span rectangles, out PackingRectangle bounds, 31 | PackingHints packingHint = PackingHints.FindBest, double acceptableDensity = 1, uint stepSize = 1, 32 | uint? maxBoundsWidth = null, uint? maxBoundsHeight = null) 33 | #elif NETSTANDARD2_0 34 | public static void Pack(PackingRectangle[] rectangles, out PackingRectangle bounds, 35 | PackingHints packingHint = PackingHints.FindBest, double acceptableDensity = 1, uint stepSize = 1, 36 | uint? maxBoundsWidth = null, uint? maxBoundsHeight = null) 37 | #endif 38 | { 39 | if (rectangles == null) 40 | throw new ArgumentNullException(nameof(rectangles)); 41 | 42 | if (stepSize == 0) 43 | throw new ArgumentOutOfRangeException(nameof(stepSize), stepSize, nameof(stepSize) + " must be greater than 0."); 44 | 45 | if (double.IsNaN(acceptableDensity) || double.IsInfinity(acceptableDensity)) 46 | throw new ArgumentException("Must be a real number", nameof(acceptableDensity)); 47 | 48 | if (maxBoundsWidth != null && maxBoundsWidth.Value == 0) 49 | throw new ArgumentOutOfRangeException(nameof(maxBoundsWidth), maxBoundsWidth, nameof(maxBoundsWidth) + " must be greater than 0."); 50 | 51 | if (maxBoundsHeight != null && maxBoundsHeight.Value == 0) 52 | throw new ArgumentOutOfRangeException(nameof(maxBoundsHeight), maxBoundsHeight, nameof(maxBoundsHeight) + " must be greater than 0."); 53 | 54 | bounds = default; 55 | if (rectangles.Length == 0) 56 | return; 57 | 58 | // We separate the value in packingHint into the different options it specifies. 59 | Span hints = stackalloc PackingHints[PackingHintExtensions.MaxHintCount]; 60 | PackingHintExtensions.GetFlagsFrom(packingHint, ref hints); 61 | 62 | if (hints.Length == 0) 63 | throw new ArgumentException("No valid packing hints specified.", nameof(packingHint)); 64 | 65 | // We'll try uint.MaxValue as initial bin size. The packing algoritm already tries to 66 | // use as little space as possible, so this will be QUICKLY cut down closer to the 67 | // final bin size. 68 | uint binWidth = maxBoundsWidth.GetValueOrDefault(uint.MaxValue); 69 | uint binHeight = maxBoundsHeight.GetValueOrDefault(uint.MaxValue); 70 | 71 | // We turn the acceptableDensity parameter into an acceptableArea value, so we can 72 | // compare the area directly rather than having to calculate the density each time. 73 | uint rectanglesAreaSum = CalculateTotalArea(rectangles); 74 | double acceptableBoundsAreaTmp = Math.Ceiling(rectanglesAreaSum / acceptableDensity); 75 | uint acceptableBoundsArea = (acceptableBoundsAreaTmp <= 0) ? rectanglesAreaSum : 76 | double.IsPositiveInfinity(acceptableBoundsAreaTmp) ? uint.MaxValue : 77 | (uint)acceptableBoundsAreaTmp; 78 | 79 | // We get a list that will be used (and reused) by the packing algorithm. 80 | List emptySpaces = GetList(rectangles.Length * 2); 81 | 82 | // We'll store the area of the best solution so far here. 83 | uint currentBestArea = uint.MaxValue; 84 | bool hasSolution = false; 85 | 86 | // In one array we'll store the current best solution, and we'll also need two temporary arrays. 87 | #if NET5_0_OR_GREATER 88 | Span currentBest = rectangles; 89 | Span tmpBest = new PackingRectangle[rectangles.Length]; 90 | Span tmpArray = new PackingRectangle[rectangles.Length]; 91 | #elif NETSTANDARD2_0 92 | PackingRectangle[] currentBest = rectangles; 93 | PackingRectangle[] tmpBest = new PackingRectangle[rectangles.Length]; 94 | PackingRectangle[] tmpArray = new PackingRectangle[rectangles.Length]; 95 | #endif 96 | 97 | 98 | // For each of the specified hints, we try to pack and see if we can find a better solution. 99 | for (int i = 0; i < hints.Length && (!hasSolution || currentBestArea > acceptableBoundsArea); i++) 100 | { 101 | // We copy the rectangles onto the tmpBest array, then sort them by what the packing hint says. 102 | #if NET5_0_OR_GREATER 103 | currentBest.CopyTo(tmpBest); 104 | #elif NETSTANDARD2_0 105 | currentBest.CopyTo(tmpBest, 0); 106 | #endif 107 | PackingHintExtensions.SortByPackingHint(tmpBest, hints[i]); 108 | 109 | // We try to find the best bin for the rectangles in tmpBest. We give the function as 110 | // initial bin size the size of the best bin we got so far. The function never tries 111 | // bigger bin sizes, so if with a specified packingHint it can't pack smaller than 112 | // with the last solution, it simply stops. 113 | if (TryFindBestBin(emptySpaces, ref tmpBest, ref tmpArray, binWidth, binHeight, stepSize, acceptableBoundsArea, 114 | out PackingRectangle boundsTmp)) 115 | { 116 | // We have a better solution! 117 | // We update the variables tracking the current best solution 118 | bounds = boundsTmp; 119 | currentBestArea = boundsTmp.Area; 120 | binWidth = bounds.Width; 121 | binHeight = bounds.Height; 122 | 123 | // We swap tmpBest and currentBest 124 | #if NET5_0_OR_GREATER 125 | Span swaptmp = tmpBest; 126 | #elif NETSTANDARD2_0 127 | PackingRectangle[] swaptmp = tmpBest; 128 | #endif 129 | tmpBest = currentBest; 130 | currentBest = swaptmp; 131 | hasSolution = true; 132 | } 133 | } 134 | 135 | if (!hasSolution) 136 | throw new Exception("Failed to find a solution. (Do your rectangles have a size close to uint.MaxValue or is your stepSize too high?)"); 137 | 138 | // The solution should be in the "rectangles" array passed as parameter. 139 | if (currentBest != rectangles) 140 | #if NET5_0_OR_GREATER 141 | currentBest.CopyTo(rectangles); 142 | #elif NETSTANDARD2_0 143 | currentBest.CopyTo(rectangles, 0); 144 | #endif 145 | 146 | // We return the list so it can be used in subsequent pack operations. 147 | ReturnList(emptySpaces); 148 | } 149 | 150 | /// 151 | /// Tries to find a solution with the smallest bin size possible, packing 152 | /// the rectangles in the order in which the were provided. 153 | /// 154 | /// The list of empty spaces for reusing. 155 | /// The rectangles to pack. Might get swapped with "tmpArray". 156 | /// A temporary array the function needs. Might get swapped with "rectangles". 157 | /// The maximum bin width to try. 158 | /// The maximum bin height to try. 159 | /// The amount by which to increment/decrement size when trying to pack another bin. 160 | /// Stops searching once a bin with this area or less is found. 161 | /// The bounds of the resulting bin (0, 0, width, height). 162 | /// Whether a solution was found. 163 | #if NET5_0_OR_GREATER 164 | private static bool TryFindBestBin(List emptySpaces, ref Span rectangles, 165 | ref Span tmpArray, uint binWidth, uint binHeight, uint stepSize, uint acceptableArea, out PackingRectangle bounds) 166 | #elif NETSTANDARD2_0 167 | private static bool TryFindBestBin(List emptySpaces, ref PackingRectangle[] rectangles, 168 | ref PackingRectangle[] tmpArray, uint binWidth, uint binHeight, uint stepSize, uint acceptableArea, out PackingRectangle bounds) 169 | #endif 170 | { 171 | // We set boundsWidth and boundsHeight to these initial 172 | // values so they're not good enough for acceptableArea. 173 | uint boundsWidth = 0; 174 | uint boundsHeight = 0; 175 | bool isFirst = true; 176 | bounds = default; 177 | 178 | // We try packing the rectangles until we either fail, or find a solution with acceptable area. 179 | while ((isFirst || boundsWidth * boundsHeight > acceptableArea) && 180 | TryPackAsOrdered(emptySpaces, rectangles, tmpArray, binWidth, binHeight, out boundsWidth, out boundsHeight)) 181 | { 182 | bounds.Width = boundsWidth; 183 | bounds.Height = boundsHeight; 184 | 185 | #if NET5_0_OR_GREATER 186 | Span swaptmp = rectangles; 187 | #elif NETSTANDARD2_0 188 | PackingRectangle[] swaptmp = rectangles; 189 | #endif 190 | rectangles = tmpArray; 191 | tmpArray = swaptmp; 192 | 193 | // As we get close to the final result, we'll reduce the bin size by stepSize. 194 | binWidth = boundsWidth <= stepSize ? 1 : (boundsWidth - stepSize); 195 | binHeight = boundsHeight <= stepSize ? 1 : (boundsHeight - stepSize); 196 | isFirst = false; 197 | } 198 | 199 | // We return true if we've found any solution. Otherwise, false. 200 | return bounds.Width != 0 && bounds.Height != 0; 201 | } 202 | 203 | /// 204 | /// Tries to pack the rectangles in the given order into a bin of the specified size. 205 | /// 206 | /// The list of empty spaces for reusing. 207 | /// The unpacked rectangles. 208 | /// Where the resulting rectangles will be written. 209 | /// The width of the bin. 210 | /// The height of the bin. 211 | /// The width of the resulting bin. 212 | /// The height of the resulting bin. 213 | /// Whether the operation succeeded. 214 | /// The unpacked and packed spans can be the same. 215 | private static bool TryPackAsOrdered(List emptySpaces, Span unpacked, 216 | Span packed, uint binWidth, uint binHeight, out uint boundsWidth, out uint boundsHeight) 217 | { 218 | // We clear the empty spaces list and add one space covering the entire bin. 219 | emptySpaces.Clear(); 220 | emptySpaces.Add(new PackingRectangle(0, 0, binWidth, binHeight)); 221 | 222 | // boundsWidth and boundsHeight both start at 0. 223 | boundsWidth = 0; 224 | boundsHeight = 0; 225 | 226 | // We loop through all the rectangles. 227 | for (int r = 0; r < unpacked.Length; r++) 228 | { 229 | // We try to find a space for the rectangle. If we can't, then we return false. 230 | if (!TryFindBestSpace(unpacked[r], emptySpaces, out int spaceIndex)) 231 | return false; 232 | 233 | PackingRectangle oldSpace = emptySpaces[spaceIndex]; 234 | packed[r] = unpacked[r]; 235 | packed[r].X = oldSpace.X; 236 | packed[r].Y = oldSpace.Y; 237 | boundsWidth = Math.Max(boundsWidth, packed[r].Right); 238 | boundsHeight = Math.Max(boundsHeight, packed[r].Bottom); 239 | 240 | // We calculate the width and height of the rectangles from splitting the empty space 241 | uint freeWidth = oldSpace.Width - packed[r].Width; 242 | uint freeHeight = oldSpace.Height - packed[r].Height; 243 | 244 | if (freeWidth != 0 && freeHeight != 0) 245 | { 246 | emptySpaces.RemoveAt(spaceIndex); 247 | // Both freeWidth and freeHeight are different from 0. We need to split the 248 | // empty space into two (plus the image). We split it in such a way that the 249 | // bigger rectangle will be where there is the most space. 250 | if (freeWidth > freeHeight) 251 | { 252 | emptySpaces.AddSorted(new PackingRectangle(packed[r].Right, oldSpace.Y, freeWidth, oldSpace.Height)); 253 | emptySpaces.AddSorted(new PackingRectangle(oldSpace.X, packed[r].Bottom, packed[r].Width, freeHeight)); 254 | } 255 | else 256 | { 257 | emptySpaces.AddSorted(new PackingRectangle(oldSpace.X, packed[r].Bottom, oldSpace.Width, freeHeight)); 258 | emptySpaces.AddSorted(new PackingRectangle(packed[r].Right, oldSpace.Y, freeWidth, packed[r].Height)); 259 | } 260 | } 261 | else if (freeWidth == 0) 262 | { 263 | // We only need to change the Y and height of the space. 264 | oldSpace.Y += packed[r].Height; 265 | oldSpace.Height = freeHeight; 266 | emptySpaces[spaceIndex] = oldSpace; 267 | EnsureSorted(emptySpaces, spaceIndex); 268 | //emptySpaces.RemoveAt(spaceIndex); 269 | //emptySpaces.Add(new PackingRectangle(oldSpace.X, oldSpace.Y + packed[r].Height, oldSpace.Width, freeHeight)); 270 | } 271 | else if (freeHeight == 0) 272 | { 273 | // We only need to change the X and width of the space. 274 | oldSpace.X += packed[r].Width; 275 | oldSpace.Width = freeWidth; 276 | emptySpaces[spaceIndex] = oldSpace; 277 | EnsureSorted(emptySpaces, spaceIndex); 278 | //emptySpaces.RemoveAt(spaceIndex); 279 | //emptySpaces.Add(new PackingRectangle(oldSpace.X + packed[r].Width, oldSpace.Y, freeWidth, oldSpace.Height)); 280 | } 281 | else // The rectangle uses up the entire empty space. 282 | emptySpaces.RemoveAt(spaceIndex); 283 | } 284 | 285 | return true; 286 | } 287 | 288 | /// 289 | /// Tries to find the best empty space that can fit the given rectangle. 290 | /// 291 | /// The rectangle to find a space for. 292 | /// The list with the empty spaces. 293 | /// The index of the space found. 294 | /// Whether a suitable space was found. 295 | private static bool TryFindBestSpace(in PackingRectangle rectangle, List emptySpaces, out int index) 296 | { 297 | for (int i = 0; i < emptySpaces.Count; i++) 298 | if (rectangle.Width <= emptySpaces[i].Width && rectangle.Height <= emptySpaces[i].Height) 299 | { 300 | index = i; 301 | return true; 302 | } 303 | 304 | index = -1; 305 | return false; 306 | } 307 | 308 | /// 309 | /// Gets a list of rectangles that can be used for empty spaces. 310 | /// 311 | /// If a list has to be created, this is used as initial capacity. 312 | private static List GetList(int preferredCapacity) 313 | { 314 | if (oldListReference == null) 315 | return new List(preferredCapacity); 316 | 317 | lock (oldListReferenceLock) 318 | { 319 | if (oldListReference.TryGetTarget(out List list)) 320 | { 321 | oldListReference.SetTarget(null); 322 | return list; 323 | } 324 | else 325 | return new List(preferredCapacity); 326 | } 327 | } 328 | 329 | /// 330 | /// Returns a list so it can be used in future pack operations. The list should 331 | /// no longer be used after returned. 332 | /// 333 | private static void ReturnList(List list) 334 | { 335 | if (oldListReference == null) 336 | oldListReference = new WeakReference>(list); 337 | else 338 | { 339 | lock (oldListReferenceLock) 340 | { 341 | if (!oldListReference.TryGetTarget(out List oldList) || oldList.Capacity < list.Capacity) 342 | oldListReference.SetTarget(list); 343 | } 344 | } 345 | } 346 | 347 | /// 348 | /// Adds a rectangle to the list in sorted order. 349 | /// 350 | private static void AddSorted(this List list, PackingRectangle rectangle) 351 | { 352 | rectangle.SortKey = Math.Max(rectangle.X, rectangle.Y); 353 | int max = list.Count - 1, min = 0; 354 | int middle, compared; 355 | 356 | // We perform a binary search for the space in which to add the rectangle 357 | while (min <= max) 358 | { 359 | middle = (max + min) / 2; 360 | compared = rectangle.SortKey.CompareTo(list[middle].SortKey); 361 | 362 | if (compared == 0) 363 | { 364 | min = middle + 1; 365 | break; 366 | } 367 | 368 | // If comparison is less than 0, rectangle should be inserted before list[middle]. 369 | // If comparison is greater than 0, rectangle should be after list[middle]. 370 | if (compared < 0) 371 | max = middle - 1; 372 | else 373 | min = middle + 1; 374 | } 375 | 376 | list.Insert(min, rectangle); 377 | } 378 | 379 | /// 380 | /// Updates an item's SortKey and ensures it is in the correct sorted position. 381 | /// If it's not, it is moved to the correct position. 382 | /// 383 | /// If an item needs to be moved, it will only be moved forward. Never backwards. 384 | private static void EnsureSorted(List list, int index) 385 | { 386 | // We update the sort key. If it doesn't differ, we do nothing. 387 | uint newSortKey = Math.Max(list[index].X, list[index].Y); 388 | if (newSortKey == list[index].SortKey) 389 | return; 390 | 391 | int min = index; 392 | int max = list.Count - 1; 393 | int middle, compared; 394 | PackingRectangle rectangle = list[index]; 395 | rectangle.SortKey = newSortKey; 396 | 397 | // We perform a binary search to look for where to put the rectangle. 398 | while (min <= max) 399 | { 400 | middle = (max + min) / 2; 401 | compared = newSortKey.CompareTo(list[middle].SortKey); 402 | 403 | if (compared == 0) 404 | { 405 | min = middle - 1; 406 | break; 407 | } 408 | 409 | // If comparison is less than 0, rectangle should be inserted before list[middle]. 410 | // If comparison is greater than 0, rectangle should be after list[middle]. 411 | if (compared < 0) 412 | max = middle - 1; 413 | else 414 | min = middle + 1; 415 | } 416 | min = Math.Min(min, list.Count - 1); 417 | 418 | // We have to place the rectangle in the index 'min'. 419 | for (int i = index; i < min; i++) 420 | list[i] = list[i + 1]; 421 | 422 | list[min] = rectangle; 423 | } 424 | 425 | /// 426 | /// Calculates the sum of the areas of all the given -s. 427 | /// 428 | public static uint CalculateTotalArea(ReadOnlySpan rectangles) 429 | { 430 | uint totalArea = 0; 431 | for (int i = 0; i < rectangles.Length; i++) 432 | totalArea += rectangles[i].Area; 433 | return totalArea; 434 | } 435 | 436 | /// 437 | /// Calculates the smallest possible rectangle that contains all the given rectangles. 438 | /// 439 | public static PackingRectangle FindBounds(ReadOnlySpan rectangles) 440 | { 441 | PackingRectangle bounds = rectangles[0]; 442 | for (int i = 1; i < rectangles.Length; i++) 443 | { 444 | bounds.X = Math.Min(bounds.X, rectangles[i].X); 445 | bounds.Y = Math.Min(bounds.Y, rectangles[i].Y); 446 | bounds.Right = Math.Max(bounds.Right, rectangles[i].Right); 447 | bounds.Bottom = Math.Max(bounds.Bottom, rectangles[i].Bottom); 448 | } 449 | 450 | return bounds; 451 | } 452 | 453 | /// 454 | /// Returns true if any two different rectangles in the given span intersect. 455 | /// 456 | public static bool AnyIntersects(ReadOnlySpan rectangles) 457 | { 458 | for (int i = 0; i < rectangles.Length; i++) 459 | for (int c = i + 1; c < rectangles.Length; c++) 460 | if (rectangles[c].Intersects(rectangles[i])) 461 | return true; 462 | return false; 463 | } 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /RectpackSharp/RectpackSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0;netstandard2.0 5 | False 6 | ThomasMiz 7 | ThomasMiz 8 | 9 | rectangles;rectpack;pack;packer;textureatlas;spritesheet;texturepacker 10 | A lightweight library for packing a list of rectangles into a bin as small as possible. 11 | packsharp-logo.png 12 | https://github.com/ThomasMiz/RectpackSharp 13 | https://github.com/ThomasMiz/RectpackSharp 14 | true 15 | 1.2.0 16 | - Added Span support on .NET 5.0 and greater 17 | RectpackSharp 18 | README.md 19 | git 20 | True 21 | MIT 22 | 23 | 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | True 35 | 36 | 37 | 38 | True 39 | 40 | 41 | 42 | True 43 | \ 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Tests/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using RectpackSharp; 6 | using SixLabors.ImageSharp; 7 | using SixLabors.ImageSharp.PixelFormats; 8 | using SixLabors.ImageSharp.Processing; 9 | 10 | namespace Tests 11 | { 12 | class Program 13 | { 14 | static readonly Random r = new Random(); 15 | 16 | static void Main(string[] args) 17 | { 18 | Stopwatch stopwatch = new Stopwatch(); 19 | 20 | PackingRectangle[] rectangles = GetRectangles(); 21 | Console.WriteLine("Packing " + rectangles.Length + " rectangles..."); 22 | 23 | stopwatch.Restart(); 24 | RectanglePacker.Pack(rectangles, out PackingRectangle bounds); 25 | stopwatch.Stop(); 26 | 27 | Console.WriteLine("Took ~" + stopwatch.Elapsed.TotalMilliseconds.ToString() + "ms"); 28 | 29 | if (RectanglePacker.AnyIntersects(rectangles)) 30 | { 31 | Console.ForegroundColor = ConsoleColor.Red; 32 | Console.WriteLine("Some rectangles intersect!"); 33 | } 34 | else 35 | { 36 | Console.ForegroundColor = ConsoleColor.Green; 37 | Console.WriteLine("No rectangles intersect."); 38 | } 39 | 40 | Console.ResetColor(); 41 | 42 | string filename = GetImageName(); 43 | Console.WriteLine("Saving as " + filename); 44 | SaveAsImage(rectangles, bounds, filename); 45 | } 46 | 47 | static PackingRectangle[] GetRectangles() 48 | { 49 | // Generate RectangleAmount randomized rectangles. 50 | /*const int RectangleAmount = 2048; 51 | PackingRectangle[] rectangles = new PackingRectangle[RectangleAmount]; 52 | for (int i = 0; i < rectangles.Length; i++) 53 | rectangles[i] = new PackingRectangle(0, 0, (uint)r.Next(20, 200), (uint)r.Next(20, 200));*/ 54 | 55 | 56 | 57 | // Generate a list of rectangles of varying sizes, as if simulating a texture atlas 58 | List list = new List(); 59 | for (int i = r.Next(5); i < 12; i++) 60 | list.Add(new PackingRectangle(0, 0, 128 * (uint)r.Next(5, 9), 128 * (uint)r.Next(2, 5))); 61 | for (int i = 0; i < 1024; i++) 62 | { 63 | list.Add(new PackingRectangle(0, 0, 64, 64)); 64 | list.Add(new PackingRectangle(0, 0, 32, 64)); 65 | list.Add(new PackingRectangle(0, 0, 64, 32)); 66 | } 67 | for (int i = 0; i < 196; i++) 68 | list.Add(new PackingRectangle(0, 0, 4 * (uint)r.Next(4, 11), 4 * (uint)r.Next(4, 11))); 69 | PackingRectangle[] rectangles = list.ToArray(); 70 | 71 | /*List list = new List(); 72 | for (int i = 0; i < 512; i++) 73 | { 74 | list.Add(new PackingRectangle(0, 0, (uint)r.Next(90, 900), (uint)r.Next(20, 200))); 75 | list.Add(new PackingRectangle(0, 0, (uint)r.Next(20, 200), (uint)r.Next(90, 900))); 76 | } 77 | PackingRectangle[] rectangles = list.ToArray();*/ 78 | 79 | return rectangles; 80 | } 81 | 82 | static string GetImageName() 83 | { 84 | string file = "rectangles.png"; 85 | 86 | int num = 1; 87 | while (File.Exists(file)) 88 | { 89 | file = string.Concat("rectangles", num.ToString(), ".png"); 90 | num++; 91 | } 92 | 93 | return file; 94 | } 95 | 96 | static void SaveAsImage(PackingRectangle[] rectangles, in PackingRectangle bounds, string file) 97 | { 98 | using Image image = new Image((int)bounds.Width, (int)bounds.Height); 99 | image.Mutate(x => x.BackgroundColor(Color.Black)); 100 | 101 | for (int i = 0; i < rectangles.Length; i++) 102 | { 103 | PackingRectangle r = rectangles[i]; 104 | Rgba32 color = FromHue(i / 64f % 1); 105 | for (int x = 0; x < r.Width; x++) 106 | for (int y = 0; y < r.Height; y++) 107 | image[x + (int)r.X, y + (int)r.Y] = color; 108 | } 109 | 110 | image.SaveAsPng(file); 111 | } 112 | 113 | static Rgba32 FromHue(float hue) 114 | { 115 | hue *= 360.0f; 116 | 117 | float h = hue / 60.0f; 118 | float x = (1.0f - Math.Abs((h % 2.0f) - 1.0f)); 119 | 120 | float r, g, b; 121 | if (h >= 0.0f && h < 1.0f) 122 | { 123 | r = 1; 124 | g = x; 125 | b = 0.0f; 126 | } 127 | else if (h >= 1.0f && h < 2.0f) 128 | { 129 | r = x; 130 | g = 1; 131 | b = 0.0f; 132 | } 133 | else if (h >= 2.0f && h < 3.0f) 134 | { 135 | r = 0.0f; 136 | g = 1; 137 | b = x; 138 | } 139 | else if (h >= 3.0f && h < 4.0f) 140 | { 141 | r = 0.0f; 142 | g = x; 143 | b = 1; 144 | } 145 | else if (h >= 4.0f && h < 5.0f) 146 | { 147 | r = x; 148 | g = 0.0f; 149 | b = 1; 150 | } 151 | else if (h >= 5.0f && h < 6.0f) 152 | { 153 | r = 1; 154 | g = 0.0f; 155 | b = x; 156 | } 157 | else 158 | { 159 | r = 0.0f; 160 | g = 0.0f; 161 | b = 0.0f; 162 | } 163 | 164 | return new Rgba32(r, g, b); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0;netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /images/rectangles_random1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/images/rectangles_random1.png -------------------------------------------------------------------------------- /images/rectangles_random2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/images/rectangles_random2.png -------------------------------------------------------------------------------- /images/rectangles_random65536.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/images/rectangles_random65536.jpeg -------------------------------------------------------------------------------- /images/rectangles_similar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/images/rectangles_similar.png -------------------------------------------------------------------------------- /images/rectangles_spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/images/rectangles_spritesheet.png -------------------------------------------------------------------------------- /images/rectangles_spritesheet2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/images/rectangles_spritesheet2.png -------------------------------------------------------------------------------- /images/rectangles_squares.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/images/rectangles_squares.png -------------------------------------------------------------------------------- /packsharp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasMiz/RectpackSharp/8826d15e128980d752702c99c6636b3d6367350f/packsharp-logo.png --------------------------------------------------------------------------------