Skip to content

Commit b0c4900

Browse files
committed
fix(texturepacker): improve path resolution and region parsing #3848
1 parent 81d2f61 commit b0c4900

File tree

3 files changed

+157
-6
lines changed

3 files changed

+157
-6
lines changed

packages/flame_texturepacker/lib/src/texture_packer_atlas.dart

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,31 @@ Future<TextureAtlasData> _parse(
262262
}
263263
}
264264

265+
var currentPackage = package;
266+
var finalPath = fullPath;
267+
268+
// Check for package path in the full path (which may already include assetsPrefix)
269+
const packageKeyword = 'packages/';
270+
final packageIndex = finalPath.indexOf(packageKeyword);
271+
if (packageIndex != -1) {
272+
final subPath = finalPath.substring(packageIndex + packageKeyword.length);
273+
final parts = subPath.split('/');
274+
if (parts.length > 1) {
275+
currentPackage ??= parts[0];
276+
// Clean the path by removing everything up to and including the package name
277+
finalPath = parts.sublist(1).join('/');
278+
}
279+
}
280+
281+
// AssetsCache also prepends its own prefix (usually 'assets/')
282+
final assetsCachePrefix = (assets ?? Flame.assets).prefix;
283+
if (finalPath.startsWith(assetsCachePrefix)) {
284+
finalPath = finalPath.replaceFirst(assetsCachePrefix, '');
285+
}
286+
265287
fileContent = await (assets ?? Flame.assets).readFile(
266-
fullPath,
267-
package: package,
288+
finalPath,
289+
package: currentPackage,
268290
);
269291
}
270292

@@ -293,6 +315,30 @@ Future<TextureAtlasData> _parse(
293315

294316
// Check if this line looks like a texture file (has file extension)
295317
if (_isTextureFile(line)) {
318+
// Peek at the next line to see if it's a region or a new page.
319+
// Regions are followed by properties like 'bounds:', 'rotate:', 'xy:', 'offsets:'.
320+
// Pages are followed by 'size:', 'format:', 'filter:', 'repeat:'.
321+
if (lineQueue.length > 1) {
322+
final nextLine = lineQueue.elementAt(1).trim();
323+
final isRegionProperty =
324+
nextLine.startsWith('bounds:') ||
325+
nextLine.startsWith('rotate:') ||
326+
nextLine.startsWith('xy:') ||
327+
nextLine.startsWith('offsets:') ||
328+
nextLine.startsWith('orig:') ||
329+
nextLine.startsWith('offset:') ||
330+
nextLine.startsWith('index:');
331+
332+
if (isRegionProperty) {
333+
// This is a region name that happens to have a file extension.
334+
final region = _parseRegion(lineQueue, page);
335+
if (region.index != -1) {
336+
hasIndexes = true;
337+
}
338+
regions.add(region);
339+
continue;
340+
}
341+
}
296342
break; // This is a new page, break out of region parsing
297343
}
298344

@@ -388,7 +434,29 @@ Future<Page> _parsePage(
388434
}
389435
}
390436

391-
page.texture = await images.load(fullTexturePath, package: package);
437+
var currentPackage = package;
438+
var finalTexturePath = fullTexturePath;
439+
440+
const packageKeyword = 'packages/';
441+
final packageIndex = finalTexturePath.indexOf(packageKeyword);
442+
if (packageIndex != -1) {
443+
final subPath = finalTexturePath.substring(
444+
packageIndex + packageKeyword.length,
445+
);
446+
final parts = subPath.split('/');
447+
if (parts.length > 1) {
448+
currentPackage ??= parts[0];
449+
finalTexturePath = parts.sublist(1).join('/');
450+
}
451+
}
452+
453+
// Images cache also prepends its own prefix (usually 'assets/images/')
454+
final imagesPrefix = images.prefix;
455+
if (finalTexturePath.startsWith(imagesPrefix)) {
456+
finalTexturePath = finalTexturePath.replaceFirst(imagesPrefix, '');
457+
}
458+
459+
page.texture = await images.load(finalTexturePath, package: currentPackage);
392460
}
393461

394462
_parsePageProperties(lineQueue, page);
@@ -436,7 +504,29 @@ void _parsePageProperties(ListQueue<String> lineQueue, Page page) {
436504
///
437505
/// Returns the parsed [Region].
438506
Region _parseRegion(ListQueue<String> lineQueue, Page page) {
439-
final name = lineQueue.removeFirst().trim();
507+
var name = lineQueue.removeFirst().trim();
508+
var extractedIndex = -1;
509+
510+
// Attempt to extract index and clean name if it follows patterns like
511+
// 'name_01.png' or 'name_01'
512+
final extensionMatch = RegExp(
513+
r'\.(png|jpg|jpeg|bmp|tga|webp)$',
514+
caseSensitive: false,
515+
).firstMatch(name);
516+
if (extensionMatch != null) {
517+
name = name.substring(0, extensionMatch.start);
518+
}
519+
520+
final indexMatch = RegExp(r'_(\d+)$').firstMatch(name);
521+
if (indexMatch != null) {
522+
try {
523+
extractedIndex = int.parse(indexMatch.group(1)!);
524+
name = name.substring(0, indexMatch.start);
525+
} catch (_) {
526+
// Ignore parsing errors for very large numbers
527+
}
528+
}
529+
440530
final values = <String, List<String>>{};
441531

442532
while (lineQueue.isNotEmpty) {
@@ -504,7 +594,7 @@ Region _parseRegion(ListQueue<String> lineQueue, Page page) {
504594
originalHeight: finalOriginalHeight,
505595
degrees: _parseDegrees(rotate?.first),
506596
rotate: _parseDegrees(rotate?.first) == 90,
507-
index: index != null ? int.parse(index[0]) : -1,
597+
index: index != null ? int.parse(index[0]) : extractedIndex,
508598
);
509599
}
510600

packages/flame_texturepacker/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ environment:
1818
dependencies:
1919
collection: ^1.17.1
2020
cross_file: ^0.3.4+2
21-
flame: ^1.36.0
21+
flame: ^1.35.0
2222
flutter:
2323
sdk: flutter
2424

packages/flame_texturepacker/test/atlas_path_resolution_test.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ sprite1
4444
when(
4545
() => images.load(any(), package: any(named: 'package')),
4646
).thenAnswer((_) async => FakeImage());
47+
48+
when(() => images.prefix).thenReturn('assets/images/');
4749
});
4850

4951
test('should resolve paths correctly with leading slashes', () async {
@@ -124,5 +126,64 @@ sprite1
124126
() => images.load('images/test.png', package: 'my_package'),
125127
).called(1);
126128
});
129+
130+
test(
131+
'should auto-detect package from path if package parameter is null',
132+
() async {
133+
final assets = AssetsCache(bundle: bundle);
134+
135+
await TexturePackerAtlas.load(
136+
'packages/custom_package/assets/images/atlas_name.atlas',
137+
assets: assets,
138+
images: images,
139+
);
140+
141+
// Verify bundle call extracted 'custom_package' and cleaned the path
142+
verify(
143+
() => bundle.loadString(
144+
'packages/custom_package/assets/images/atlas_name.atlas',
145+
cache: any(named: 'cache'),
146+
),
147+
).called(1);
148+
149+
// Verify images.load also uses the extracted package
150+
verify(
151+
() => images.load('test.png', package: 'custom_package'),
152+
).called(1);
153+
},
154+
);
155+
156+
test(
157+
'should correctly parse region names with .png and extracted indexes',
158+
() async {
159+
final assets = AssetsCache(bundle: bundle);
160+
final complexAtlasContent = '''
161+
knight.png
162+
size: 64, 64
163+
filter: Nearest, Nearest
164+
repeat: none
165+
knight_walk_01.png
166+
bounds: 0, 0, 32, 32
167+
knight_walk_02.png
168+
bounds: 32, 0, 32, 32
169+
''';
170+
171+
when(
172+
() => bundle.loadString(any(), cache: any(named: 'cache')),
173+
).thenAnswer((_) async => complexAtlasContent);
174+
175+
final atlas = await TexturePackerAtlas.load(
176+
'knight.atlas',
177+
assets: assets,
178+
images: images,
179+
);
180+
181+
expect(atlas.sprites.length, 2);
182+
expect(atlas.sprites[0].region.name, 'knight_walk');
183+
expect(atlas.sprites[0].region.index, 1);
184+
expect(atlas.sprites[1].region.name, 'knight_walk');
185+
expect(atlas.sprites[1].region.index, 2);
186+
},
187+
);
127188
});
128189
}

0 commit comments

Comments
 (0)