using MrGameEng.Atlases; using Xunit; namespace MrGameEng.Atlases.Tests; public class ShelfPackerTests { private static List Squares(int count, int size) => Enumerable.Range(0, count).Select(i => new PackItem($"item{i:D3}", size, size)).ToList(); [Fact] public void Pack_PlacesEveryItem_WithinPageBounds() { var result = ShelfPacker.Pack(Squares(50, 60), maxPageSize: 256, padding: 2); Assert.Equal(50, result.Placements.Count); foreach (var p in result.Placements) { var (pageWidth, pageHeight) = result.PageSizes[p.Page]; Assert.True(p.X >= 2 && p.Y >= 2); Assert.True(p.X + p.Width <= pageWidth); Assert.True(p.Y + p.Height <= pageHeight); } } [Fact] public void Pack_NoTwoPlacements_Overlap() { var items = new List(); var random = new Random(42); for (var i = 0; i < 200; i++) { items.Add(new PackItem($"r{i:D3}", random.Next(4, 90), random.Next(4, 90))); } var result = ShelfPacker.Pack(items, maxPageSize: 512, padding: 2); var byPage = result.Placements.GroupBy(p => p.Page); foreach (var page in byPage) { var list = page.ToList(); for (var i = 0; i < list.Count; i++) { for (var j = i + 1; j < list.Count; j++) { var a = list[i]; var b = list[j]; var separated = a.X + a.Width + 2 <= b.X || b.X + b.Width + 2 <= a.X || a.Y + a.Height + 2 <= b.Y || b.Y + b.Height + 2 <= a.Y; Assert.True(separated, $"{a.Key} overlaps {b.Key} (padding included)"); } } } } [Fact] public void Pack_Overflows_ToMultiplePages() { // 9 квадратов 100² на страницу 256² помещаются максимум по 4. var result = ShelfPacker.Pack(Squares(9, 100), maxPageSize: 256, padding: 2); Assert.True(result.PageSizes.Count >= 3); Assert.Equal(Enumerable.Range(0, result.PageSizes.Count), result.Placements.Select(p => p.Page).Distinct().Order()); } [Fact] public void Pack_OversizedItem_GetsDedicatedPage() { var items = Squares(2, 30); items.Add(new PackItem("huge", 500, 40)); var result = ShelfPacker.Pack(items, maxPageSize: 256, padding: 2); var huge = result.Placements.Single(p => p.Key == "huge"); Assert.Single(result.Placements, p => p.Page == huge.Page); Assert.True(result.PageSizes[huge.Page].Width >= 504); } [Fact] public void Pack_IsDeterministic_RegardlessOfInputOrder() { var items = Squares(30, 20).Concat(Squares(10, 50).Select(i => i with { Key = "b" + i.Key })).ToList(); var shuffled = items.AsEnumerable().Reverse().ToList(); var a = ShelfPacker.Pack(items, 128, 2); var b = ShelfPacker.Pack(shuffled, 128, 2); Assert.Equal( a.Placements.OrderBy(p => p.Key, StringComparer.Ordinal), b.Placements.OrderBy(p => p.Key, StringComparer.Ordinal)); } [Fact] public void Pack_TrimsPages_ToPowerOfTwo() { var result = ShelfPacker.Pack(Squares(1, 50), maxPageSize: 2048, padding: 2); Assert.Equal((64, 64), result.PageSizes.Single()); } [Theory] [InlineData(1, 1)] [InlineData(64, 64)] [InlineData(65, 128)] [InlineData(2048, 2048)] public void NextPowerOfTwo_RoundsUp(int value, int expected) { Assert.Equal(expected, ShelfPacker.NextPowerOfTwo(value)); } [Fact] public void Pack_NonPowerOfTwoLimit_SharedPagesNeverExceedIt() { // 40² с padding на лимит 100: POT-округление дало бы 128 > лимита. var result = ShelfPacker.Pack(Squares(10, 40), maxPageSize: 100, padding: 2); Assert.All(result.PageSizes, size => { Assert.True(size.Width <= 100, $"page width {size.Width} > 100"); Assert.True(size.Height <= 100, $"page height {size.Height} > 100"); }); } [Fact] public void Pack_OversizedItem_PageHasExactPaddedSize() { var result = ShelfPacker.Pack([new PackItem("big", 300, 50)], maxPageSize: 128, padding: 2); Assert.Equal((304, 54), result.PageSizes.Single()); } [Fact] public void Pack_OversizedAmongSmall_DoesNotSplitSharedPage() { // Широкий и низкий негабарит сортируется в конец по высоте; раньше он закрывал // наполовину заполненную общую страницу, и остаток уезжал на новую. var items = new List { new("big", 300, 10) }; for (var i = 0; i < 4; i++) { items.Add(new PackItem($"s{i}", 30, 30)); } var result = ShelfPacker.Pack(items, maxPageSize: 128, padding: 2); Assert.Equal(2, result.PageSizes.Count); // отдельная страница big + одна общая } }