Skip to content

SpriteSheet

Namespace Nfty.Core.Publish in assembly Nfty.Core.

public static class SpriteSheet

Stitches a cooked Set's assets into one image, left to right and top to bottom.

Remarks

The order is the Set's own numbering, which is what makes a sheet addressable: cell n is asset n + 1, so an engine indexing frames and a reader counting rows agree without anything having to be recorded. SetReader orders by number rather than by filename for the same reason.

It is built one asset at a time, never all of them at once. A Set is a folder of PNGs on disk and the sheet is the only large thing in memory here — decoding five hundred thousand-pixel assets to composite them in one pass would cost gigabytes for an operation whose output is a single image. Each asset is opened, drawn into its cell and disposed.

There is a hard pixel ceiling and it is not arbitrary. A sheet is one contiguous raster: a 500-asset Set at 1000px in a 23x23 grid is 23,000 square, which is 529 million pixels and about 2 GB of RGBA before the encoder sees it. The limit is stated, checked before anything is allocated, and reported with the numbers that would have to change — a refusal an author can act on beats an OutOfMemoryException half a minute in.

Fields

MaxPixels

public const long MaxPixels = 268435456;

The largest sheet this will build, in pixels.

Remarks

16,384 square. Chosen because it is the maximum texture dimension on essentially every GPU that would consume a sheet, so a sheet past it is not merely large but unusable by the thing it exists for — and because at RGBA it is about 1 GB, which is already the outer edge of what is reasonable to hold for one image.

MaxSide

public const int MaxSide = 16384;

The longest side this will build, in pixels.

Remarks

Checked separately from SpriteSheet.MaxPixels: a 1 x 400,000 strip is small in area and is still not an image anything can open.

Methods

Fit(int, int, int)

public static SpriteSheetLayout Fit(int count, int cellWidth, int cellHeight);

The grid a Set falls into when nobody has chosen one: as square as it gets.

Parameters

  • count (int) - How many assets.
  • cellWidth (int) - The canvas width.
  • cellHeight (int) - The canvas height.

Returns. A layout that holds every asset.

Exceptions

  • ArgumentOutOfRangeException - An argument is not positive.

Remarks

Square by COLUMN COUNT rather than by pixels, because a sheet is read as a grid of cells and a 4:1 canvas would otherwise produce a grid four cells wide. The last row is allowed to be short — that is what a spritesheet looks like — and the empty cells are transparent.

ProblemWith(SpriteSheetLayout, int)

public static string? ProblemWith(SpriteSheetLayout layout, int count);

Why a layout cannot be built, or null when it can.

Parameters

  • layout (SpriteSheetLayout) - The grid.
  • count (int) - How many assets have to fit in it.

Returns. A sentence to show the user, or null.

Remarks

Returns a problem rather than throwing, the way Validator does and for the same reason: a dialog binds this live while someone is still typing into the row and column boxes, and a half-typed grid is a normal state to be in rather than an error to raise. The writer below throws on the same conditions, so a caller that skips the check still cannot produce a broken sheet.

Write(IReadOnlyList<SetItem>, SpriteSheetLayout, string, IProgress<SpriteSheetProgress>?, CancellationToken)

public static SpriteSheetLayout Write(IReadOnlyList<SetItem> items, SpriteSheetLayout layout, string destination, IProgress<SpriteSheetProgress>? progress = null, CancellationToken cancellationToken = default);

SpriteSheet.WriteAsync(), for a caller that is not on a UI thread.

Parameters

  • items (IReadOnlyList<SetItem>) - The Set's assets, in the order they should be placed.
  • layout (SpriteSheetLayout) - The grid.
  • destination (string) - The .png to write.
  • progress (IProgress<SpriteSheetProgress>?) - Reports one step per asset placed.
  • cancellationToken (CancellationToken) - Stops between assets.

Returns. The layout that was written.

Exceptions

  • ArgumentException - The grid cannot hold the Set, or is too large to build.

Remarks

The async one is the real implementation and this blocks on it, which is the opposite of Generator.GenerateAsync and right for the opposite reason: generation is CPU-bound with nothing to await, and this is PNG codec work against the disk for every asset in the collection. Blocking is safe here because nothing in Core captures a synchronization context — every await inside is ConfigureAwait(false) — so there is no context for a continuation to deadlock against.

WriteAsync(IReadOnlyList<SetItem>, SpriteSheetLayout, string, IProgress<SpriteSheetProgress>?, CancellationToken)

public static Task<SpriteSheetLayout> WriteAsync(IReadOnlyList<SetItem> items, SpriteSheetLayout layout, string destination, IProgress<SpriteSheetProgress>? progress = null, CancellationToken cancellationToken = default);

Stitches a Set into a sheet and writes it as a PNG.

Parameters

  • items (IReadOnlyList<SetItem>) - The Set's assets, in the order they should be placed.
  • layout (SpriteSheetLayout) - The grid.
  • destination (string) - The .png to write.
  • progress (IProgress<SpriteSheetProgress>?) - Reports one step per asset placed.
  • cancellationToken (CancellationToken) - Stops between assets.

Returns. The layout that was written.

Exceptions

  • ArgumentException - The grid cannot hold the Set, or is too large to build.

Remarks

A missing or unreadable asset leaves its cell EMPTY rather than failing the sheet. The Set browser already takes that position — a browser over a damaged Set shows the damage rather than refusing to open — and a sheet is more useful with a hole in it than not at all. The hole is visible, which is the point: a transparent cell among drawn ones is unmistakable, where a sheet that silently closed the gap would renumber every frame after it.