CI / build-test (push) Failing after 1m7s
Collisions: init bucket heads to -1 (QueryAabb hung before the first rebuild), reset query stamps on truncated QueryAabb (later queries silently dropped entities), inside-origin raycasts hit at fraction 0 for circles too, exactly-touching boxes now pair like touching circles. Graphics: render into the letterbox viewport so the picture matches ScreenToWorld/WorldToScreen instead of stretching; Y-sort by the transform pivot rather than the quad center; lock-free snapshot LayerRegistry (parallel submit read it unsynchronized); validate InitialCapacity; warn when UseRenderer2D drops options of a later scene. Core: scenes are explicitly single-use (re-loading threw silently duplicated systems/entities before — now it throws), Scene.RegisterUnload for per-scene resources, a switch requested during the reveal phase covers again instead of hard-swapping, borderless fullscreen (HardwareModeSwitch off), InputCapture service for input-suppressing overlays, host disposes the transition renderer and IDisposable services on shutdown. Input: game input reads as released while InputCapture is held; mouse position and wheel freeze so deltas stay zero. DevConsole: holds InputCapture while open (typing no longer drives the camera), Revision increments only under the lock, quoted command arguments, history capped at 256. UI: scene Desktop skips Myra input processing while the console is open (clicks no longer fall through), is disposed on scene unload, and Myra init no longer depends on a process-static flag. Audio: validate channel count/sample rate before stopping the previous track, empty looped oggs no longer hang FillBuffers, the instance stops when a non-looping track drains (IsPlaying was stuck true). Atlases: metadata v2 stores per-source size+mtime snapshots, so timestamp-preserving copies and renames invalidate correctly; loader checks the version and disposes pages on partial load failure; shared pages never exceed a non-POT MaxPageSize; oversized items pack first onto exact-size pages instead of splitting an open shared page; the CLI validates numeric options. Assets.Generator: file names are escaped in XML docs and string literals, members no longer collide with the enclosing class (CS0542), and the Assets root is resolved against build_property.projectdir so nested "Assets" directories do not shift region paths. Pathfinding: queries throw when the grid was resized after construction; generation stamps survive int overflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
149 lines
5.1 KiB
C#
149 lines
5.1 KiB
C#
using MrGameEng.Atlases;
|
||
using Xunit;
|
||
|
||
namespace MrGameEng.Atlases.Tests;
|
||
|
||
public class ShelfPackerTests
|
||
{
|
||
private static List<PackItem> 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<PackItem>();
|
||
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<PackItem> { 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 + одна общая
|
||
}
|
||
}
|