Repository: flame-engine/flame

Issue:
[flame_texturepacker] make Region immutable
### What could be improved

The Region class should be immutable and all redundant fields in TexturePackerSprite should be removed. WIP: `perf/immutable_region_data`

https://github.com/gnarhard/flame/tree/perf/immutable_region_data

This _parse method is also not the friendliest to read. I think there could be some improvement there too.

### Why should this be improved

I've already confirmed that this will reduce TexturePackerSprite memory consumption by ~50%. These fields that are in the TexturePackerSprite class that I've removed are redundant and already stored in the region data. My branch removes all of that code that shouldn't be mutated anyways, and makes Region immutable. However, I'm hung up on the tests and need help here.

_parse method could be improved to be easier to read and maintain.

### Risks

This will be a breaking change. Currently, I don't have access to the texture pack files in the testing environment. Each field needs to be set every time, but since I don't have access to the texture pack save files, I can't check and see which algorithm is being used to update the atlas files accordingly.

### More information

_No response_

### Other

- [x] Are you interested in working on a PR for this?



Fix patch:
diff --git a/packages/flame_texturepacker/README.md b/packages/flame_texturepacker/README.md
index 6cf8d2036fb..4f842fe7e93 100644
--- a/packages/flame_texturepacker/README.md
+++ b/packages/flame_texturepacker/README.md
@@ -126,7 +126,8 @@ Note: Sprites used in this example can be found OpenGameArt [here][4].
 ## Credits
 
 Thanks to [Jonas Fröber][5] for the original implementation.
-Thanks to [Gnarhard][6] for the feature to load an atlas file from a device's storage and whitelisting.
+Thanks to [Gnarhard][6] for the feature to load an atlas file from a device's storage,
+sprite whitelisting, and memory optimizations.
 
 [1]: https://www.codeandweb.com/texturepacker 'Code & Web Texture Packer'
 [2]: https://github.com/crashinvaders/gdx-texture-packer-gui 'Gdx Texture Packer'
diff --git a/packages/flame_texturepacker/lib/src/model/region.dart b/packages/flame_texturepacker/lib/src/model/region.dart
index a0a3b6fa764..a589444ea15 100644
--- a/packages/flame_texturepacker/lib/src/model/region.dart
+++ b/packages/flame_texturepacker/lib/src/model/region.dart
@@ -1,18 +1,76 @@
 import 'package:flame_texturepacker/src/model/page.dart';
+import 'package:flutter/foundation.dart';
 
 /// Represents a region within the texture packer atlas.
-class Region {
-  late Page page;
-  late String name;
-  late double left;
-  late double top;
-  late double width;
-  late double height;
-  double offsetX = 0;
-  double offsetY = 0;
-  double originalWidth = 0;
-  double originalHeight = 0;
-  int degrees = 0;
-  bool rotate = false;
-  int index = -1;
+@immutable
+final class Region {
+  /// The page in the texture pack this region belongs to.
+  final Page page;
+
+  /// The name of the original image file, without the file's extension.
+  /// If the name ends with an underscore followed by only numbers, that part is
+  /// excluded: underscores denote special instructions to the texture packer.
+  final String name;
+
+  /// The left position of the region in the texture atlas.
+  final double left;
+
+  /// The top position of the region in the texture atlas.
+  final double top;
+
+  /// The width of the image, after whitespace was removed for packing.
+  final double width;
+
+  /// The height of the image, after whitespace was removed for packing.
+  final double height;
+
+  /// The offset from the left of the original image to the left of the packed
+  /// image, after whitespace was removed for packing.
+  final double offsetX;
+
+  /// The offset from the bottom of the original image to the bottom of the
+  /// packed image, after whitespace was removed for packing.
+  final double offsetY;
+
+  /// The width of the image, before whitespace was removed and rotation was
+  /// applied for packing.
+  final double originalWidth;
+
+  /// The height of the image, before whitespace was removed for packing.
+  final double originalHeight;
+
+  /// The degrees the region has been rotated, counter clockwise between 0 and
+  /// 359. Most atlas region handling deals only with 0 or 90 degree rotation
+  /// (enough to handle rectangles).
+  /// More advanced texture packing may support other rotations (eg, for tightly
+  /// packing polygons).
+  final int degrees;
+
+  /// If true, the region has been rotated 90 degrees counter clockwise.
+  final bool rotate;
+
+  /// The number at the end of the original image file name, or -1 if none.
+  ///
+  /// When sprites are packed, if the original file name ends with a number, it
+  /// is stored as the index and is not considered as part of the sprite's name.
+  /// This is useful for keeping animation frames in order.
+  final int index;
+
+  /// Creates a new [Region] with the given properties.
+  const Region({
+    required this.page,
+    required this.name,
+    this.left = 0,
+    this.top = 0,
+    this.width = 0,
+    this.height = 0,
+    this.offsetX = 0,
+    this.offsetY = 0,
+    double? originalWidth,
+    double? originalHeight,
+    this.degrees = 0,
+    this.rotate = false,
+    this.index = -1,
+  }) : originalWidth = originalWidth ?? width,
+       originalHeight = originalHeight ?? height;
 }
diff --git a/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart b/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart
index 4a3d69a2a3c..6177061f12e 100644
--- a/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart
+++ b/packages/flame_texturepacker/lib/src/texture_packer_atlas.dart
@@ -1,5 +1,6 @@
 library flame_texturepacker;
 
+import 'dart:collection';
 import 'dart:convert';
 
 import 'package:collection/collection.dart';
@@ -11,23 +12,23 @@ import 'package:flame_texturepacker/src/model/region.dart';
 import 'package:flame_texturepacker/src/texture_packer_sprite.dart';
 import 'package:flutter/painting.dart';
 
+/// A texture atlas that contains a collection of [TexturePackerSprite]s.
+///
+/// This class provides methods to load and query sprites from a texture atlas
+/// created by TexturePacker or similar tools.
 class TexturePackerAtlas {
-  /// The sprites contained in this atlas.
+  /// List of all sprites contained in this atlas.
   final List<TexturePackerSprite> sprites;
 
+  /// Creates a new [TexturePackerAtlas] with the given [sprites].
   TexturePackerAtlas(this.sprites);
 
-  /// Loads all the sprites from the passed in [TextureAtlasData] and constructs
-  /// a new instance.
+  /// Creates a [TexturePackerAtlas] from parsed atlas data.
   ///
-  /// Use [whiteList] to filter the sprites to be loaded. This can
-  /// significantly reduce the memory usage if only a few sprites are needed.
-  /// If the list is empty, all sprites will be loaded. The filter is case
-  /// sensitive and will load all sprites in a directory if you wish.
-  ///
-  /// If [useOriginalSize] is true, the sprites loaded will use original size
-  /// instead of the packed size. For animation sprites, load with origin size
-  /// is recommended for a smooth result.
+  /// [atlasData] - The parsed atlas data containing pages and regions
+  /// [whiteList] - Optional list of sprite names to include.
+  ///               If empty, all sprites are included
+  /// [useOriginalSize] - Use original sprite dimensions before packing or not.
   factory TexturePackerAtlas.fromAtlas(
     TextureAtlasData atlasData, {
     List<String> whiteList = const [],
@@ -36,23 +37,33 @@ class TexturePackerAtlas {
     return TexturePackerAtlas(
       atlasData.regions
           .where(
-            (e) =>
-                whiteList.isEmpty ||
-                whiteList.any((key) => e.name.contains(key)),
+            (e) {
+              return whiteList.isEmpty ||
+                  whiteList.any((key) => e.name.contains(key));
+            },
+          )
+          .map(
+            (e) => TexturePackerSprite(
+              e,
+              useOriginalSize: useOriginalSize,
+            ),
           )
-          .map((e) => TexturePackerSprite(e, useOriginalSize: useOriginalSize))
           .toList(),
     );
   }
 
-  /// Loads all the sprites from the atlas that resides on the [path] and
-  /// returns a new [TexturePackerAtlas].
-  /// If [fromStorage] is true, the atlas will be loaded from the device's
-  /// storage instead of the assets folder.
+  /// Loads a texture atlas from a file path.
+  ///
+  /// [path] - The path to the atlas file
+  /// [fromStorage] - Load from device storage (true) or assets (false)
+  /// [useOriginalSize] - Use original sprite dimensions before packing or not.
+  /// [images] - Optional Images cache to use for loading textures
+  /// [assetsPrefix] - Prefix for asset paths (default: 'images')
+  /// [assets] - Optional AssetsCache to use for loading assets
+  /// [whiteList] - Optional list of sprite names to include.
+  ///               If empty, all sprites are included
   ///
-  /// If [useOriginalSize] is true, the sprites loaded will use original size
-  /// instead of the packed size. For animation sprites, load with origin size
-  /// is recommended for a smooth result.
+  /// Returns a [Future] that completes with the loaded [TexturePackerAtlas].
   static Future<TexturePackerAtlas> load(
     String path, {
     bool fromStorage = false,
@@ -82,10 +93,14 @@ class TexturePackerAtlas {
     );
   }
 
-  /// Loads the atlas that resides on the [path] and
-  /// returns a new [TextureAtlasData].
-  /// If [fromStorage] is true, the atlas will be loaded from the device's
-  /// storage instead of the assets folder.
+  /// Loads atlas data without creating a [TexturePackerAtlas] instance.
+  ///
+  /// [path] - The path to the atlas file
+  /// [fromStorage] - Load from device storage (true) or assets (false)
+  /// [images] - Optional Images cache to use for loading textures
+  /// [assetsPrefix] - Prefix for asset paths (default: 'images')
+  ///
+  /// Returns a [Future] that completes with the raw [TextureAtlasData].
   static Future<TextureAtlasData> loadAtlas(
     String path, {
     bool fromStorage = false,
@@ -99,33 +114,54 @@ class TexturePackerAtlas {
     }
   }
 
-  /// Returns the first region found with the specified name. This method uses
-  /// string comparison to find the region, so the result should be cached
-  /// rather than calling this method multiple times.
+  /// Finds a sprite by its name.
+  ///
+  /// [name] - The name of the sprite to find
+  ///
+  /// Returns the first [TexturePackerSprite] with the given name
+  /// or null if not found.
   TexturePackerSprite? findSpriteByName(String name) {
-    return sprites.firstWhereOrNull((e) => e.name == name);
+    return sprites.firstWhereOrNull(
+      (e) => e.region.name == name,
+    );
   }
 
-  /// Returns the first region found with the specified name and index.
-  /// This method uses string comparison to find the region, so the result
-  /// should be cached rather than calling this method multiple times.
+  /// Finds a sprite by its name and index.
+  ///
+  /// [name] - The name of the sprite to find
+  /// [index] - The index of the sprite to find
+  ///
+  /// Returns the [TexturePackerSprite] with the given name and index
+  /// or null if not found.
   TexturePackerSprite? findSpriteByNameIndex(String name, int index) {
     return sprites.firstWhereOrNull(
-      (sprite) => sprite.name == name && sprite.index == index,
+      (sprite) => sprite.region.name == name && sprite.region.index == index,
     );
   }
 
-  /// Returns all regions with the specified name, ordered by smallest to
-  /// largest index. This method uses string comparison to find the regions, so
-  /// the result should be cached rather than calling this method multiple
-  /// times.
+  /// Finds all sprites with the given name.
+  ///
+  /// [name] - The name of the sprites to find
+  ///
+  /// Returns a list of all [TexturePackerSprite]s with the given name.
   List<TexturePackerSprite> findSpritesByName(String name) {
-    return sprites.where((sprite) => sprite.name == name).toList();
+    return sprites
+        .where(
+          (sprite) => sprite.region.name == name,
+        )
+        .toList();
   }
 }
 
-/// Loads images from the assets folder.
-/// Uses the [path] to find the image directory.
+/// Loads texture atlas data from application assets.
+///
+/// [path] - The path to the atlas file
+/// [assetsPrefix] - Prefix for asset paths
+/// [images] - Optional Images cache to use for loading textures
+/// [assets] - Optional AssetsCache to use for loading assets
+///
+/// Returns a [Future] that completes with the loaded [TextureAtlasData].
+/// Throws an [Exception] if loading fails.
 Future<TextureAtlasData> _fromAssets(
   String path, {
   required String assetsPrefix,
@@ -142,16 +178,19 @@ Future<TextureAtlasData> _fromAssets(
     );
   } on Exception catch (e, stack) {
     Error.throwWithStackTrace(
-      Exception(
-        'Error loading $assetsPrefix$path from assets: $e',
-      ),
+      Exception('Error loading $assetsPrefix$path from assets: $e'),
       stack,
     );
   }
 }
 
-/// Loads images from the device's storage.
-/// Uses the [path] to find the image directory.
+/// Loads texture atlas data from device storage.
+///
+/// [path] - The path to the atlas file
+/// [images] - Optional Images cache to use for loading textures
+///
+/// Returns a [Future] that completes with the loaded [TextureAtlasData].
+/// Throws an [Exception] if loading fails.
 Future<TextureAtlasData> _fromStorage(
   String path, {
   Images? images,
@@ -170,11 +209,15 @@ Future<TextureAtlasData> _fromStorage(
   }
 }
 
-/// Parses the atlas file and loads the images.
-/// Uses the [path] to find the image directory.
-/// Atlas will be loaded from the device's storage if [fromStorage] is true.
-/// Otherwise, it will be loaded from the assets folder.
-/// Returns a [TextureAtlasData] containing the pages and regions.
+/// Parses a texture atlas file and returns the atlas data.
+///
+/// [path] - The path to the atlas file
+/// [fromStorage] - Whether to load from device storage (true) or assets (false)
+/// [images] - Optional Images cache to use for loading textures
+/// [assets] - Optional AssetsCache to use for loading assets
+/// [assetsPrefix] - Prefix for asset paths (required when fromStorage is false)
+///
+/// Returns a [Future] that completes with the parsed [TextureAtlasData].
 Future<TextureAtlasData> _parse(
   String path, {
   required bool fromStorage,
@@ -184,173 +227,249 @@ Future<TextureAtlasData> _parse(
 }) async {
   final pages = <Page>[];
   final regions = <Region>[];
-  final String fileAsString;
+  var hasIndexes = false;
+
+  final fileContent = fromStorage
+      ? await XFile(path).readAsString()
+      : await (assets ?? Flame.assets).readFile('${assetsPrefix!}/$path');
+
+  final lines = LineSplitter.split(
+    fileContent,
+  ).where((line) => line.trim().isNotEmpty);
+
+  final lineQueue = ListQueue<String>.from(lines);
+  images ??= Flame.images;
+
+  while (lineQueue.isNotEmpty) {
+    final page = await _parsePage(lineQueue, path, fromStorage, images);
+    pages.add(page);
+
+    // Parse regions for this page until we hit another page or end of file
+    while (lineQueue.isNotEmpty) {
+      final line = lineQueue.first.trim();
+
+      // Check if this line looks like a texture file (has file extension)
+      if (_isTextureFile(line)) {
+        break; // This is a new page, break out of region parsing
+      }
+
+      final region = _parseRegion(lineQueue, page);
+
+      if (region.index != -1) {
+        hasIndexes = true;
+      }
+
+      regions.add(region);
+    }
+  }
+
+  if (hasIndexes) {
+    regions.sort(
+      (a, b) {
+        final i1 = a.index == -1 ? 0x7FFFFFFF : a.index;
+        final i2 = b.index == -1 ? 0x7FFFFFFF : b.index;
+        return i1 - i2;
+      },
+    );
+  }
+
+  return (pages: pages, regions: regions);
+}
+
+/// Checks if a line represents a texture file (page) rather than a region name.
+///
+/// [line] - The line to check
+///
+/// Returns true if the line looks like a texture file path.
+bool _isTextureFile(String line) {
+  final trimmed = line.trim();
+
+  // Check for common image file extensions
+  const imageExtensions = ['.png', '.jpg', '.jpeg', '.bmp', '.tga', '.webp'];
+
+  for (final extension in imageExtensions) {
+    if (trimmed.toLowerCase().endsWith(extension)) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+/// Parses a page definition from the atlas file.
+///
+/// [lineQueue] - Queue of remaining lines to parse
+/// [path] - The path to the atlas file
+/// [fromStorage] - Whether loading from device storage
+/// [images] - Images cache to use for loading textures
+///
+/// Returns a [Future] that completes with the parsed [Page].
+Future<Page> _parsePage(
+  ListQueue<String> lineQueue,
+  String path,
+  bool fromStorage,
+  Images images,
+) async {
+  final page = Page();
+  page.textureFile = lineQueue.removeFirst();
+
+  final parentPath = (path.split('/')..removeLast()).join('/');
+  final texturePath = parentPath.isEmpty
+      ? page.textureFile
+      : '$parentPath/${page.textureFile}';
 
   if (fromStorage) {
-    fileAsString = await XFile(path).readAsString();
+    final bytes = await XFile(texturePath).readAsBytes();
+    final image = await decodeImageFromList(bytes);
+    images.add(texturePath, image);
+    page.texture = images.fromCache(texturePath);
   } else {
-    assert(
-      assetsPrefix != null,
-      'When reading from storage, the assetsPrefix needs to be provided.',
-    );
-    fileAsString = await (assets ?? Flame.assets).readFile(
-      '$assetsPrefix/$path',
-    );
+    page.texture = await images.load(texturePath);
   }
 
-  final iterator = LineSplitter.split(fileAsString).iterator;
-  var line = iterator.moveNextAndGet();
-  var hasIndexes = false;
+  _parsePageProperties(lineQueue, page);
+
+  return page;
+}
 
-  while (true) {
-    if (line == null) {
+/// Parses page properties from the atlas file.
+///
+/// [lineQueue] - Queue of remaining lines to parse
+/// [page] - The page to populate with properties
+void _parsePageProperties(ListQueue<String> lineQueue, Page page) {
+  while (lineQueue.isNotEmpty) {
+    final line = lineQueue.first;
+    final (:count, :entry) = _readEntry(line);
+
+    if (count == 0) {
       break;
     }
 
-    if (line.isEmpty) {
-      line = iterator.moveNextAndGet();
+    switch (entry[0]) {
+      case 'size':
+        page.width = int.parse(entry[1]);
+        page.height = int.parse(entry[2]);
+      case 'filter':
+        page.minFilter = entry[1];
+        page.magFilter = entry[2];
+      case 'format':
+        page.format = entry[1];
+      case 'repeat':
+        page.repeat = entry[1];
+      default:
+        // Unknown property, consume and stop
+        break;
     }
 
-    Page? page;
+    lineQueue.removeFirst();
+  }
+}
 
-    while (true) {
-      if (line == null) {
-        break;
-      }
+/// Parses a region definition from the atlas file.
+///
+/// [lineQueue] - Queue of remaining lines to parse
+/// [page] - The page this region belongs to
+///
+/// Returns the parsed [Region].
+Region _parseRegion(ListQueue<String> lineQueue, Page page) {
+  final name = lineQueue.removeFirst().trim();
+  final values = <String, List<String>>{};
+
+  while (lineQueue.isNotEmpty) {
+    final line = lineQueue.first.trim();
+
+    // If this looks like a texture file, stop parsing this region
+    if (_isTextureFile(line)) {
+      break;
+    }
 
-      if (line.isEmpty) {
-        page = null;
-        line = iterator.moveNextAndGet();
-      } else if (page == null) {
-        page = Page();
-        page.textureFile = line;
-        final parentPath = (path.split('/')..removeLast()).join('/');
-        final texturePath = parentPath.isEmpty ? line : '$parentPath/$line';
-
-        images ??= Flame.images;
-        if (fromStorage) {
-          try {
-            final textureFile = XFile(texturePath);
-            final bytes = await textureFile.readAsBytes();
-            final decodedBytes = await decodeImageFromList(bytes);
-            images.add(texturePath, decodedBytes);
-            page.texture = images.fromCache(texturePath);
-          } on Exception catch (e, stack) {
-            Error.throwWithStackTrace(
-              Exception('Could not add storage file to Flame cache. $e'),
-              stack,
-            );
-          }
-        } else {
-          page.texture = await images.load(texturePath);
-        }
-
-        while (true) {
-          line = iterator.moveNextAndGet();
-          if (line == null) {
-            break;
-          }
-          final (:count, :entry) = _readEntry(line);
-          if (count == 0) {
-            break;
-          }
-          switch (entry[0]) {
-            case 'size':
-              page.width = int.parse(entry[1]);
-              page.height = int.parse(entry[2]);
-            case 'filter':
-              page.minFilter = entry[1];
-              page.magFilter = entry[2];
-            case 'format':
-              page.format = entry[1];
-            case 'repeat':
-              page.repeat = entry[1];
-          }
-        }
-        pages.add(page);
-      } else {
-        final region = Region();
-        region.page = page;
-        region.name = line.trim();
-        while (true) {
-          line = iterator.moveNextAndGet();
-          if (line == null) {
-            break;
-          }
-          final (:count, :entry) = _readEntry(line);
-          if (count == 0) {
-            break;
-          }
-          switch (entry[0]) {
-            case 'xy':
-              region.left = double.parse(entry[1]);
-              region.top = double.parse(entry[2]);
-            case 'size':
-              region.width = double.parse(entry[1]);
-              region.height = double.parse(entry[2]);
-            case 'bounds':
-              region.left = double.parse(entry[1]);
-              region.top = double.parse(entry[2]);
-              region.width = double.parse(entry[3]);
-              region.height = double.parse(entry[4]);
-            case 'offset':
-              region.offsetX = double.parse(entry[1]);
-              region.offsetY = double.parse(entry[2]);
-            case 'orig':
-              region.originalWidth = double.parse(entry[1]);
-              region.originalHeight = double.parse(entry[2]);
-            case 'offsets':
-              region.offsetX = double.parse(entry[1]);
-              region.offsetY = double.parse(entry[2]);
-              region.originalWidth = double.parse(entry[3]);
-              region.originalHeight = double.parse(entry[4]);
-            case 'rotate':
-              final value = entry[1];
-
-              if (value == 'true') {
-                region.degrees = 90;
-              } else if (value == 'false') {
-                region.degrees = 0;
-              } else {
-                region.degrees = int.parse(value);
-              }
-
-              region.rotate = region.degrees == 90;
-            case 'index':
-              region.index = int.parse(entry[1]);
-
-              if (region.index != -1) {
-                hasIndexes = true;
-              }
-          }
-        }
-        if (region.originalWidth == 0 && region.originalHeight == 0) {
-          region.originalWidth = region.width;
-          region.originalHeight = region.height;
-        }
-
-        regions.add(region);
-      }
+    final (:count, :entry) = _readEntry(line);
+
+    if (count == 0) {
+      break;
     }
+
+    values[entry[0]] = entry.sublist(1);
+    lineQueue.removeFirst();
   }
 
-  if (hasIndexes) {
-    regions.sort((region1, region2) {
-      var i1 = region1.index;
-      if (i1 == -1) {
-        i1 = double.maxFinite.toInt();
-      }
-      var i2 = region2.index;
-      if (i2 == -1) {
-        i2 = double.maxFinite.toInt();
-      }
-      return i1 - i2;
-    });
+  final xy = values['xy'];
+  final size = values['size'];
+  final bounds = values['bounds'];
+  final offset = values['offset'];
+  final orig = values['orig'];
+  final offsets = values['offsets'];
+  final rotate = values['rotate'];
+  final index = values['index'];
+
+  final offsetOrNull = offsets ?? offset;
+
+  final offsetX = offsetOrNull != null ? double.parse(offsetOrNull[0]) : 0.0;
+  final offsetY = offsetOrNull != null ? double.parse(offsetOrNull[1]) : 0.0;
+
+  final originalWidth = offsets != null
+      ? double.parse(offsets[2])
+      : (orig != null ? double.parse(orig[0]) : 0.0);
+
+  final finalOriginalWidth = originalWidth == 0.0 ? null : originalWidth;
+
+  final originalHeight = offsets != null
+      ? double.parse(offsets[3])
+      : (orig != null ? double.parse(orig[1]) : 0.0);
+
+  final finalOriginalHeight = originalHeight == 0.0 ? null : originalHeight;
+
+  return Region(
+    page: page,
+    name: name,
+    left: bounds != null
+        ? double.parse(bounds[0])
+        : (xy != null ? double.parse(xy[0]) : 0.0),
+    top: bounds != null
+        ? double.parse(bounds[1])
+        : (xy != null ? double.parse(xy[1]) : 0.0),
+    width: bounds != null
+        ? double.parse(bounds[2])
+        : (size != null ? double.parse(size[0]) : 0.0),
+    height: bounds != null
+        ? double.parse(bounds[3])
+        : (size != null ? double.parse(size[1]) : 0.0),
+    offsetX: offsetX,
+    offsetY: offsetY,
+    originalWidth: finalOriginalWidth,
+    originalHeight: finalOriginalHeight,
+    degrees: _parseDegrees(rotate?.first),
+    rotate: _parseDegrees(rotate?.first) == 90,
+    index: index != null ? int.parse(index[0]) : -1,
+  );
+}
+
+/// Parses rotation degrees from a string value.
+///
+/// [value] - The string value to parse ('true', 'false', or numeric string)
+///
+/// Returns the rotation in degrees (0, 90, or parsed integer value).
+int _parseDegrees(String? value) {
+  if (value == null) {
+    return 0;
   }
 
-  return (pages: pages, regions: regions);
+  if (value == 'true') {
+    return 90;
+  }
+
+  if (value == 'false') {
+    return 0;
+  }
+
+  return int.parse(value);
 }
 
+/// Parses a single entry line from the atlas file.
+///
+/// [line] - The line to parse
+///
+/// Returns a record containing the count of parsed values and the entry list.
 ({int count, List<String> entry}) _readEntry(String line) {
   final trimmedLine = line.trim();
 
@@ -384,14 +503,9 @@ Future<TextureAtlasData> _parse(
   }
 }
 
+/// Type definition for texture atlas data containing pages and regions.
+///
+/// This is a record type with two fields:
+/// - [List<Page> pages]: List of texture pages
+/// - [List<Region> regions]: List of sprite regions
 typedef TextureAtlasData = ({List<Page> pages, List<Region> regions});
-
-extension _IteratorExtension on Iterator<String> {
-  String? moveNextAndGet() {
-    if (moveNext()) {
-      return current;
-    }
-
-    return null;
-  }
-}
diff --git a/packages/flame_texturepacker/lib/src/texture_packer_sprite.dart b/packages/flame_texturepacker/lib/src/texture_packer_sprite.dart
index cc27f3b5236..44e4a52f101 100644
--- a/packages/flame_texturepacker/lib/src/texture_packer_sprite.dart
+++ b/packages/flame_texturepacker/lib/src/texture_packer_sprite.dart
@@ -5,25 +5,15 @@ import 'package:flame/components.dart';
 import 'package:flame/game.dart';
 import 'package:flame/rendering.dart';
 import 'package:flame_texturepacker/src/model/region.dart';
+import 'package:flutter/material.dart';
 
 /// {@template _texture_packer_sprite}
 /// A [Sprite] extracted from a texture packer file.
 /// {@endtemplate}
 class TexturePackerSprite extends Sprite {
   /// {@macro _texture_packer_sprite}
-  TexturePackerSprite(Region region, {this.useOriginalSize = true})
-    : _region = region,
-      name = region.name,
-      index = region.index,
-      offsetX = region.offsetX,
-      offsetY = region.offsetY,
-      packedWidth = region.width,
-      packedHeight = region.height,
-      originalWidth = region.originalWidth,
-      originalHeight = region.originalHeight,
-      rotate = region.rotate,
-      degrees = region.degrees,
-      super(
+  TexturePackerSprite(this.region, {this.useOriginalSize = true})
+    : super(
         region.page.texture,
         srcPosition: Vector2(region.left, region.top),
         srcSize: Vector2(
@@ -34,77 +24,39 @@ class TexturePackerSprite extends Sprite {
     if (region.rotate) {
       final transform = Transform2D()..angle = math.pi / 2;
       _decorator = Transform2DDecorator(transform);
+    } else {
+      _decorator = null;
     }
   }
 
   /// Region object for [clone] function, don't modify this object properties.
-  final Region _region;
+  final Region region;
 
-  /// If true, use [originalWidth] and [originalHeight] as size; otherwise use
-  /// [packedWidth] and [packedHeight] as size.
+  /// If true, use [Region.originalWidth] and [Region.originalHeight] as size;
+  /// otherwise use [Region.width] and [Region.height] as size.
   final bool useOriginalSize;
 
-  /// The number at the end of the original image file name, or -1 if none.
-  ///
-  /// When sprites are packed, if the original file name ends with a number, it
-  /// is stored as the index and is not considered as part of the sprite's name.
-  /// This is useful for keeping animation frames in order.
-  final int index;
-
-  /// The name of the original image file, without the file's extension.
-  /// If the name ends with an underscore followed by only numbers, that part is
-  /// excluded: underscores denote special instructions to the texture packer.
-  final String name;
-
-  /// The offset from the left of the original image to the left of the packed
-  /// image, after whitespace was removed for packing.
-  final double offsetX;
-
-  /// The offset from the bottom of the original image to the bottom of the
-  /// packed image, after whitespace was removed for packing.
-  final double offsetY;
-
-  /// The width of the image, after whitespace was removed for packing.
-  final double packedWidth;
-
-  /// The height of the image, after whitespace was removed for packing.
-  final double packedHeight;
-
-  /// The width of the image, before whitespace was removed and rotation was
-  /// applied for packing.
-  final double originalWidth;
-
-  /// The height of the image, before whitespace was removed for packing.
-  final double originalHeight;
-
-  /// If true, the region has been rotated 90 degrees counter clockwise.
-  final bool rotate;
-
-  /// The degrees the region has been rotated, counter clockwise between 0 and
-  /// 359. Most atlas region handling deals only with 0 or 90 degree rotation
-  /// (enough to handle rectangles).
-  /// More advanced texture packing may support other rotations (eg, for tightly
-  /// packing polygons).
-  final int degrees;
-
-  /// The [degrees] field (angle) represented as radians.
-  double get angle => radians(degrees.toDouble());
+  /// The [Region.degrees] field (angle) represented as radians.
+  double get angle => radians(region.degrees.toDouble());
 
   /// Clone current object with new value for argument [useOriginalSize].
   TexturePackerSprite clone({bool useOriginalSize = true}) =>
-      TexturePackerSprite(_region, useOriginalSize: useOriginalSize);
+      TexturePackerSprite(region, useOriginalSize: useOriginalSize);
 
   Vector2 get _srcSize => Vector2(src.width, src.height);
 
   Vector2 get _srcSizeRotated => Vector2(src.height, src.width);
 
-  Vector2 get _srcSizeRender => rotate ? _srcSizeRotated : _srcSize;
+  Vector2 get _srcSizeRender => region.rotate ? _srcSizeRotated : _srcSize;
 
-  Vector2 get _offset => Vector2(offsetX, offsetY);
+  Vector2 get _offset => Vector2(region.offsetX, region.offsetY);
 
-  Vector2 get _packedSize => Vector2(packedWidth, packedHeight);
+  Vector2 get _packedSize => Vector2(region.width, region.height);
 
-  Vector2 get _originalSize => Vector2(originalWidth, originalHeight);
+  Vector2 get _originalSize => Vector2(
+    region.originalWidth,
+    region.originalHeight,
+  );
 
   Vector2 get offset => useOriginalSize ? _offset : Vector2.zero();
 
@@ -121,7 +73,7 @@ class TexturePackerSprite extends Sprite {
     final actualSize = Vector2.copy(size ?? originalSize)
       ..divide(originalSize)
       ..multiply(_packedSize);
-    if (rotate) {
+    if (region.rotate) {
       actualSize.setValues(actualSize.y, actualSize.x);
     }
     src = srcPosition.toPositionedRect(actualSize);
@@ -132,7 +84,7 @@ class TexturePackerSprite extends Sprite {
     src = (position ?? Vector2.zero()).toPositionedRect(_srcSize);
   }
 
-  Decorator? _decorator;
+  late final Decorator? _decorator;
 
   // Used to avoid the creation of new Vector2 objects in render.
   static final _tmpRenderPosition = Vector2.zero();
@@ -181,7 +133,7 @@ class TexturePackerSprite extends Sprite {
       ..multiply(_tmpRenderScale)
       ..add(_tmpRenderPosition);
 
-    if (!rotate) {
+    if (!region.rotate) {
       // Calculate and render for non-rotated image, must call function render
       // from super class with anchor = Anchor.topLeft, because we already
       // calculated size and position based on Anchor.topLeft to rendered by


Test patch:
diff --git a/packages/flame_texturepacker/test/legacy_format_test.dart b/packages/flame_texturepacker/test/legacy_format_test.dart
index 050688f01ec..f7b32ef2168 100644
--- a/packages/flame_texturepacker/test/legacy_format_test.dart
+++ b/packages/flame_texturepacker/test/legacy_format_test.dart
@@ -50,16 +50,16 @@ void main() {
       );
 
       final jumpSprite = atlas.findSpriteByName('robot_jump')!;
-      expect(jumpSprite.rotate, true);
-      expect(jumpSprite.name, 'robot_jump');
+      expect(jumpSprite.region.rotate, true);
+      expect(jumpSprite.region.name, 'robot_jump');
+      expect(jumpSprite.region.height, 256);
+      expect(jumpSprite.region.width, 192);
+      expect(jumpSprite.region.index, -1);
+      expect(jumpSprite.region.offsetY, 0);
+      expect(jumpSprite.region.offsetX, 0);
+      expect(jumpSprite.region.originalWidth, 192);
+      expect(jumpSprite.region.originalHeight, 256);
       expect(jumpSprite.srcPosition, Vector2(196, 260));
-      expect(jumpSprite.packedHeight, 256);
-      expect(jumpSprite.packedWidth, 192);
-      expect(jumpSprite.index, -1);
-      expect(jumpSprite.offsetY, 0);
-      expect(jumpSprite.offsetX, 0);
-      expect(jumpSprite.originalWidth, 192);
-      expect(jumpSprite.originalHeight, 256);
       expect(jumpSprite.src, const Rect.fromLTWH(196, 260, 256, 192));
       expect(jumpSprite.srcSize, Vector2(192, 256));
       expect(jumpSprite.originalSize, Vector2(192, 256));
@@ -67,16 +67,16 @@ void main() {
       expect(jumpSprite.angle, math.pi / 2);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, false);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, false);
+      expect(walkSprite.region.name, 'robot_walk');
+      expect(walkSprite.region.height, 256);
+      expect(walkSprite.region.width, 192);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 0);
+      expect(walkSprite.region.offsetX, 0);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.srcPosition, Vector2(2, 196));
-      expect(walkSprite.packedHeight, 256);
-      expect(walkSprite.packedWidth, 192);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 0);
-      expect(walkSprite.offsetX, 0);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(2, 196, 192, 256));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -91,16 +91,16 @@ void main() {
       );
 
       final idleSprite = atlas.findSpriteByName('robot_idle')!;
-      expect(idleSprite.rotate, false);
-      expect(idleSprite.name, 'robot_idle');
+      expect(idleSprite.region.rotate, false);
+      expect(idleSprite.region.name, 'robot_idle');
+      expect(idleSprite.region.height, 182);
+      expect(idleSprite.region.width, 130);
+      expect(idleSprite.region.index, -1);
+      expect(idleSprite.region.offsetY, 0);
+      expect(idleSprite.region.offsetX, 31);
+      expect(idleSprite.region.originalWidth, 192);
+      expect(idleSprite.region.originalHeight, 256);
       expect(idleSprite.srcPosition, Vector2(0, 310));
-      expect(idleSprite.packedHeight, 182);
-      expect(idleSprite.packedWidth, 130);
-      expect(idleSprite.index, -1);
-      expect(idleSprite.offsetY, 0);
-      expect(idleSprite.offsetX, 31);
-      expect(idleSprite.originalWidth, 192);
-      expect(idleSprite.originalHeight, 256);
       expect(idleSprite.src, const Rect.fromLTWH(0, 310, 130, 182));
       expect(idleSprite.srcSize, Vector2(192, 256));
       expect(idleSprite.originalSize, Vector2(192, 256));
@@ -115,16 +115,16 @@ void main() {
       expect(idleSpritePackedSize.angle, 0);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, true);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, true);
+      expect(walkSprite.region.name, 'robot_walk');
+      expect(walkSprite.region.height, 183);
+      expect(walkSprite.region.width, 150);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 2);
+      expect(walkSprite.region.offsetX, 14);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.srcPosition, Vector2(0, 0));
-      expect(walkSprite.packedHeight, 183);
-      expect(walkSprite.packedWidth, 150);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 2);
-      expect(walkSprite.offsetX, 14);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(0, 0, 183, 150));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -186,16 +186,16 @@ void main() {
       );
 
       final jumpSprite = atlas.findSpriteByName('robot_jump')!;
-      expect(jumpSprite.rotate, false);
-      expect(jumpSprite.name, 'robot_jump');
+      expect(jumpSprite.region.rotate, false);
+      expect(jumpSprite.region.name, 'robot_jump');
+      expect(jumpSprite.region.height, 256);
+      expect(jumpSprite.region.width, 192);
+      expect(jumpSprite.region.index, -1);
+      expect(jumpSprite.region.offsetY, 0);
+      expect(jumpSprite.region.offsetX, 0);
+      expect(jumpSprite.region.originalWidth, 192);
+      expect(jumpSprite.region.originalHeight, 256);
       expect(jumpSprite.srcPosition, Vector2(196, 518));
-      expect(jumpSprite.packedHeight, 256);
-      expect(jumpSprite.packedWidth, 192);
-      expect(jumpSprite.index, -1);
-      expect(jumpSprite.offsetY, 0);
-      expect(jumpSprite.offsetX, 0);
-      expect(jumpSprite.originalWidth, 192);
-      expect(jumpSprite.originalHeight, 256);
       expect(jumpSprite.src, const Rect.fromLTWH(196, 518, 192, 256));
       expect(jumpSprite.srcSize, Vector2(192, 256));
       expect(jumpSprite.originalSize, Vector2(192, 256));
@@ -203,16 +203,16 @@ void main() {
       expect(jumpSprite.angle, 0);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, false);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, false);
+      expect(walkSprite.region.name, 'robot_walk');
+      expect(walkSprite.region.height, 256);
+      expect(walkSprite.region.width, 192);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 0);
+      expect(walkSprite.region.offsetX, 0);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.srcPosition, Vector2(2, 518));
-      expect(walkSprite.packedHeight, 256);
-      expect(walkSprite.packedWidth, 192);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 0);
-      expect(walkSprite.offsetX, 0);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(2, 518, 192, 256));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -227,16 +227,16 @@ void main() {
       );
 
       final idleSprite = atlas.findSpriteByName('robot_idle')!;
-      expect(idleSprite.rotate, false);
-      expect(idleSprite.name, 'robot_idle');
+      expect(idleSprite.region.rotate, false);
+      expect(idleSprite.region.name, 'robot_idle');
+      expect(idleSprite.region.height, 182);
+      expect(idleSprite.region.width, 130);
+      expect(idleSprite.region.index, -1);
+      expect(idleSprite.region.offsetY, 0);
+      expect(idleSprite.region.offsetX, 31);
+      expect(idleSprite.region.originalWidth, 192);
+      expect(idleSprite.region.originalHeight, 256);
       expect(idleSprite.srcPosition, Vector2(0, 160));
-      expect(idleSprite.packedHeight, 182);
-      expect(idleSprite.packedWidth, 130);
-      expect(idleSprite.index, -1);
-      expect(idleSprite.offsetY, 0);
-      expect(idleSprite.offsetX, 31);
-      expect(idleSprite.originalWidth, 192);
-      expect(idleSprite.originalHeight, 256);
       expect(idleSprite.src, const Rect.fromLTWH(0, 160, 130, 182));
       expect(idleSprite.srcSize, Vector2(192, 256));
       expect(idleSprite.originalSize, Vector2(192, 256));
@@ -251,16 +251,16 @@ void main() {
       expect(idleSpritePackedSize.angle, 0);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, true);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, true);
+      expect(walkSprite.region.name, 'robot_walk');
+      expect(walkSprite.region.height, 183);
+      expect(walkSprite.region.width, 150);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 2);
+      expect(walkSprite.region.offsetX, 14);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.srcPosition, Vector2(191, 367));
-      expect(walkSprite.packedHeight, 183);
-      expect(walkSprite.packedWidth, 150);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 2);
-      expect(walkSprite.offsetX, 14);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(191, 367, 183, 150));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -290,7 +290,7 @@ void main() {
         size: Vector2(384, 512),
       );
 
-      expect(sprite.rotate, false);
+      expect(sprite.region.rotate, false);
       expect(sprite.offset, Vector2.zero());
       expect(sprite.originalSize, Vector2(192, 256));
       expect(sprite.srcSize, Vector2(192, 256));
@@ -311,7 +311,7 @@ void main() {
         size: Vector2(384, 512),
       );
 
-      expect(sprite.rotate, true);
+      expect(sprite.region.rotate, true);
       expect(sprite.offset, Vector2(14, 2));
       expect(sprite.originalSize, Vector2(192, 256));
       expect(sprite.srcSize, Vector2(192, 256));
diff --git a/packages/flame_texturepacker/test/new_format_test.dart b/packages/flame_texturepacker/test/new_format_test.dart
index f77ae76aa3b..2066010c52f 100644
--- a/packages/flame_texturepacker/test/new_format_test.dart
+++ b/packages/flame_texturepacker/test/new_format_test.dart
@@ -58,16 +58,16 @@ void main() {
       );
 
       final jumpSprite = atlas.findSpriteByName('robot_jump')!;
-      expect(jumpSprite.rotate, true);
-      expect(jumpSprite.name, 'robot_jump');
+      expect(jumpSprite.region.rotate, true);
+      expect(jumpSprite.region.name, 'robot_jump');
       expect(jumpSprite.srcPosition, Vector2(196, 260));
-      expect(jumpSprite.packedHeight, 256);
-      expect(jumpSprite.packedWidth, 192);
-      expect(jumpSprite.index, -1);
-      expect(jumpSprite.offsetY, 0);
-      expect(jumpSprite.offsetX, 0);
-      expect(jumpSprite.originalWidth, 192);
-      expect(jumpSprite.originalHeight, 256);
+      expect(jumpSprite.region.height, 256);
+      expect(jumpSprite.region.width, 192);
+      expect(jumpSprite.region.index, -1);
+      expect(jumpSprite.region.offsetY, 0);
+      expect(jumpSprite.region.offsetX, 0);
+      expect(jumpSprite.region.originalWidth, 192);
+      expect(jumpSprite.region.originalHeight, 256);
       expect(jumpSprite.src, const Rect.fromLTWH(196, 260, 256, 192));
       expect(jumpSprite.srcSize, Vector2(192, 256));
       expect(jumpSprite.originalSize, Vector2(192, 256));
@@ -75,16 +75,16 @@ void main() {
       expect(jumpSprite.angle, math.pi / 2);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, false);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, false);
+      expect(walkSprite.region.name, 'robot_walk');
       expect(walkSprite.srcPosition, Vector2(2, 196));
-      expect(walkSprite.packedHeight, 256);
-      expect(walkSprite.packedWidth, 192);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 0);
-      expect(walkSprite.offsetX, 0);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
+      expect(walkSprite.region.height, 256);
+      expect(walkSprite.region.width, 192);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 0);
+      expect(walkSprite.region.offsetX, 0);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(2, 196, 192, 256));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -98,16 +98,16 @@ void main() {
       );
 
       final idleSprite = atlas.findSpriteByName('robot_idle')!;
-      expect(idleSprite.rotate, false);
-      expect(idleSprite.name, 'robot_idle');
+      expect(idleSprite.region.rotate, false);
+      expect(idleSprite.region.name, 'robot_idle');
+      expect(idleSprite.region.height, 182);
+      expect(idleSprite.region.width, 130);
+      expect(idleSprite.region.index, -1);
+      expect(idleSprite.region.offsetY, 0);
+      expect(idleSprite.region.offsetX, 31);
+      expect(idleSprite.region.originalWidth, 192);
+      expect(idleSprite.region.originalHeight, 256);
       expect(idleSprite.srcPosition, Vector2(0, 310));
-      expect(idleSprite.packedHeight, 182);
-      expect(idleSprite.packedWidth, 130);
-      expect(idleSprite.index, -1);
-      expect(idleSprite.offsetY, 0);
-      expect(idleSprite.offsetX, 31);
-      expect(idleSprite.originalWidth, 192);
-      expect(idleSprite.originalHeight, 256);
       expect(idleSprite.src, const Rect.fromLTWH(0, 310, 130, 182));
       expect(idleSprite.srcSize, Vector2(192, 256));
       expect(idleSprite.originalSize, Vector2(192, 256));
@@ -122,16 +122,16 @@ void main() {
       expect(idleSpritePackedSize.angle, 0);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, true);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, true);
+      expect(walkSprite.region.name, 'robot_walk');
+      expect(walkSprite.region.height, 183);
+      expect(walkSprite.region.width, 150);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 2);
+      expect(walkSprite.region.offsetX, 14);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.srcPosition, Vector2(0, 0));
-      expect(walkSprite.packedHeight, 183);
-      expect(walkSprite.packedWidth, 150);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 2);
-      expect(walkSprite.offsetX, 14);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(0, 0, 183, 150));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -161,7 +161,7 @@ void main() {
         size: Vector2(384, 512),
       );
 
-      expect(sprite.rotate, true);
+      expect(sprite.region.rotate, true);
       expect(sprite.offset, Vector2.zero());
       expect(sprite.originalSize, Vector2(192, 256));
       expect(sprite.srcSize, Vector2(192, 256));
@@ -244,16 +244,16 @@ void main() {
       );
 
       final jumpSprite = atlas.findSpriteByName('robot_jump')!;
-      expect(jumpSprite.rotate, false);
-      expect(jumpSprite.name, 'robot_jump');
+      expect(jumpSprite.region.rotate, false);
+      expect(jumpSprite.region.name, 'robot_jump');
+      expect(jumpSprite.region.height, 256);
+      expect(jumpSprite.region.width, 192);
+      expect(jumpSprite.region.index, -1);
+      expect(jumpSprite.region.offsetY, 0);
+      expect(jumpSprite.region.offsetX, 0);
+      expect(jumpSprite.region.originalWidth, 192);
+      expect(jumpSprite.region.originalHeight, 256);
       expect(jumpSprite.srcPosition, Vector2(196, 518));
-      expect(jumpSprite.packedHeight, 256);
-      expect(jumpSprite.packedWidth, 192);
-      expect(jumpSprite.index, -1);
-      expect(jumpSprite.offsetY, 0);
-      expect(jumpSprite.offsetX, 0);
-      expect(jumpSprite.originalWidth, 192);
-      expect(jumpSprite.originalHeight, 256);
       expect(jumpSprite.src, const Rect.fromLTWH(196, 518, 192, 256));
       expect(jumpSprite.srcSize, Vector2(192, 256));
       expect(jumpSprite.originalSize, Vector2(192, 256));
@@ -261,16 +261,16 @@ void main() {
       expect(jumpSprite.angle, 0);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, false);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, false);
+      expect(walkSprite.region.name, 'robot_walk');
+      expect(walkSprite.region.height, 256);
+      expect(walkSprite.region.width, 192);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 0);
+      expect(walkSprite.region.offsetX, 0);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.srcPosition, Vector2(2, 518));
-      expect(walkSprite.packedHeight, 256);
-      expect(walkSprite.packedWidth, 192);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 0);
-      expect(walkSprite.offsetX, 0);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(2, 518, 192, 256));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -284,16 +284,16 @@ void main() {
       );
 
       final idleSprite = atlas.findSpriteByName('robot_idle')!;
-      expect(idleSprite.rotate, false);
-      expect(idleSprite.name, 'robot_idle');
+      expect(idleSprite.region.rotate, false);
+      expect(idleSprite.region.name, 'robot_idle');
+      expect(idleSprite.region.height, 182);
+      expect(idleSprite.region.width, 130);
+      expect(idleSprite.region.index, -1);
+      expect(idleSprite.region.offsetY, 0);
+      expect(idleSprite.region.offsetX, 31);
+      expect(idleSprite.region.originalWidth, 192);
+      expect(idleSprite.region.originalHeight, 256);
       expect(idleSprite.srcPosition, Vector2(0, 160));
-      expect(idleSprite.packedHeight, 182);
-      expect(idleSprite.packedWidth, 130);
-      expect(idleSprite.index, -1);
-      expect(idleSprite.offsetY, 0);
-      expect(idleSprite.offsetX, 31);
-      expect(idleSprite.originalWidth, 192);
-      expect(idleSprite.originalHeight, 256);
       expect(idleSprite.src, const Rect.fromLTWH(0, 160, 130, 182));
       expect(idleSprite.srcSize, Vector2(192, 256));
       expect(idleSprite.originalSize, Vector2(192, 256));
@@ -308,16 +308,16 @@ void main() {
       expect(idleSpritePackedSize.angle, 0);
 
       final walkSprite = atlas.findSpriteByName('robot_walk')!;
-      expect(walkSprite.rotate, true);
-      expect(walkSprite.name, 'robot_walk');
+      expect(walkSprite.region.rotate, true);
+      expect(walkSprite.region.name, 'robot_walk');
+      expect(walkSprite.region.height, 183);
+      expect(walkSprite.region.width, 150);
+      expect(walkSprite.region.index, 0);
+      expect(walkSprite.region.offsetY, 2);
+      expect(walkSprite.region.offsetX, 14);
+      expect(walkSprite.region.originalWidth, 192);
+      expect(walkSprite.region.originalHeight, 256);
       expect(walkSprite.srcPosition, Vector2(191, 367));
-      expect(walkSprite.packedHeight, 183);
-      expect(walkSprite.packedWidth, 150);
-      expect(walkSprite.index, 0);
-      expect(walkSprite.offsetY, 2);
-      expect(walkSprite.offsetX, 14);
-      expect(walkSprite.originalWidth, 192);
-      expect(walkSprite.originalHeight, 256);
       expect(walkSprite.src, const Rect.fromLTWH(191, 367, 183, 150));
       expect(walkSprite.srcSize, Vector2(192, 256));
       expect(walkSprite.originalSize, Vector2(192, 256));
@@ -347,7 +347,7 @@ void main() {
         size: Vector2(384, 512),
       );
 
-      expect(sprite.rotate, true);
+      expect(sprite.region.rotate, true);
       expect(sprite.offset, Vector2(14, 2));
       expect(sprite.originalSize, Vector2(192, 256));
       expect(sprite.srcSize, Vector2(192, 256));
diff --git a/packages/flame_texturepacker/test/region_test.dart b/packages/flame_texturepacker/test/region_test.dart
new file mode 100644
index 00000000000..a399b9504f7
--- /dev/null
+++ b/packages/flame_texturepacker/test/region_test.dart
@@ -0,0 +1,103 @@
+import 'package:flame_texturepacker/src/model/page.dart';
+import 'package:flame_texturepacker/src/model/region.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('Region', () {
+    late Page dummyPage;
+
+    setUp(() {
+      dummyPage = Page();
+    });
+
+    test('encodes and decodes all fields correctly', () {
+      final region = Region(
+        page: dummyPage,
+        name: 'test_region',
+        left: 123,
+        top: 456,
+        width: 789,
+        height: 321,
+        offsetX: 50,
+        offsetY: 75,
+        originalWidth: 800,
+        originalHeight: 600,
+        degrees: 90,
+        rotate: true,
+        index: 3,
+      );
+
+      expect(region.name, 'test_region');
+      expect(region.page, dummyPage);
+
+      expect(region.left, 123);
+      expect(region.top, 456);
+      expect(region.width, 789);
+      expect(region.height, 321);
+      expect(region.offsetX, 50);
+      expect(region.offsetY, 75);
+      expect(region.originalWidth, 800);
+      expect(region.originalHeight, 600);
+
+      expect(region.degrees, 90);
+      expect(region.rotate, isTrue);
+      expect(region.index, 3);
+    });
+
+    test('default values decode correctly', () {
+      final region = Region(
+        page: dummyPage,
+        name: 'default_region',
+      );
+
+      expect(region.name, 'default_region');
+      expect(region.page, dummyPage);
+
+      expect(region.left, 0);
+      expect(region.top, 0);
+      expect(region.width, 0);
+      expect(region.height, 0);
+      expect(region.offsetX, 0);
+      expect(region.offsetY, 0);
+      expect(region.originalWidth, 0);
+      expect(region.originalHeight, 0);
+
+      expect(region.degrees, 0);
+      expect(region.rotate, isFalse);
+      expect(region.index, -1);
+    });
+
+    test('handles max values correctly', () {
+      const max = 4095;
+
+      final region = Region(
+        page: dummyPage,
+        name: 'max_region',
+        left: max.toDouble(),
+        top: max.toDouble(),
+        width: max.toDouble(),
+        height: max.toDouble(),
+        offsetX: max.toDouble(),
+        offsetY: max.toDouble(),
+        originalWidth: max.toDouble(),
+        originalHeight: max.toDouble(),
+        degrees: 359,
+        rotate: true,
+        index: 999999,
+      );
+
+      expect(region.left, max.toDouble());
+      expect(region.top, max.toDouble());
+      expect(region.width, max.toDouble());
+      expect(region.height, max.toDouble());
+      expect(region.offsetX, max.toDouble());
+      expect(region.offsetY, max.toDouble());
+      expect(region.originalWidth, max.toDouble());
+      expect(region.originalHeight, max.toDouble());
+
+      expect(region.degrees, 359);
+      expect(region.rotate, isTrue);
+      expect(region.index, 999999);
+    });
+  });
+}