├── .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 | [](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 | 
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 | 
56 |
57 | It also works like a charm for texture atlases or sprite sheets!
58 | 
59 | 
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 | 
64 | 
65 |
66 | Fuck it, here's 65536 random rectangles in a ~24k x 24k bin.
67 | 
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
--------------------------------------------------------------------------------