@flighthq/textureatlas-formats 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/index.d.ts +8 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +8 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/textureAtlasAsepriteParse.d.ts +5 -0
  6. package/dist/textureAtlasAsepriteParse.d.ts.map +1 -0
  7. package/dist/textureAtlasAsepriteParse.js +42 -0
  8. package/dist/textureAtlasAsepriteParse.js.map +1 -0
  9. package/dist/textureAtlasAsepriteSchema.d.ts +53 -0
  10. package/dist/textureAtlasAsepriteSchema.d.ts.map +1 -0
  11. package/dist/textureAtlasAsepriteSchema.js +6 -0
  12. package/dist/textureAtlasAsepriteSchema.js.map +1 -0
  13. package/dist/textureAtlasDetect.d.ts +16 -0
  14. package/dist/textureAtlasDetect.d.ts.map +1 -0
  15. package/dist/textureAtlasDetect.js +71 -0
  16. package/dist/textureAtlasDetect.js.map +1 -0
  17. package/dist/textureAtlasLibgdxParse.d.ts +3 -0
  18. package/dist/textureAtlasLibgdxParse.d.ts.map +1 -0
  19. package/dist/textureAtlasLibgdxParse.js +114 -0
  20. package/dist/textureAtlasLibgdxParse.js.map +1 -0
  21. package/dist/textureAtlasPackerParse.d.ts +9 -0
  22. package/dist/textureAtlasPackerParse.d.ts.map +1 -0
  23. package/dist/textureAtlasPackerParse.js +57 -0
  24. package/dist/textureAtlasPackerParse.js.map +1 -0
  25. package/dist/textureAtlasPackerSchema.d.ts +52 -0
  26. package/dist/textureAtlasPackerSchema.d.ts.map +1 -0
  27. package/dist/textureAtlasPackerSchema.js +5 -0
  28. package/dist/textureAtlasPackerSchema.js.map +1 -0
  29. package/dist/textureAtlasStarlingParse.d.ts +14 -0
  30. package/dist/textureAtlasStarlingParse.d.ts.map +1 -0
  31. package/dist/textureAtlasStarlingParse.js +50 -0
  32. package/dist/textureAtlasStarlingParse.js.map +1 -0
  33. package/package.json +39 -0
  34. package/src/textureAtlasAsepriteParse.test.ts +131 -0
  35. package/src/textureAtlasDetect.test.ts +98 -0
  36. package/src/textureAtlasLibgdxParse.test.ts +114 -0
  37. package/src/textureAtlasPackerParse.test.ts +206 -0
  38. package/src/textureAtlasStarlingParse.test.ts +92 -0
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@flighthq/textureatlas-formats",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/textureatlas": "0.1.0",
31
+ "@flighthq/types": "0.1.0",
32
+ "@flighthq/xml": "0.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^5.3.0"
36
+ },
37
+ "description": "Texture-atlas descriptor format parsers (TexturePacker / Starling / LibGDX / Aseprite -> TextureAtlas)",
38
+ "sideEffects": false
39
+ }
@@ -0,0 +1,131 @@
1
+ import { createTextureAtlas } from '@flighthq/textureatlas';
2
+
3
+ import { parseTextureAtlasAsepriteDocument, parseTextureAtlasAsepriteJson } from './textureAtlasAsepriteParse';
4
+ import type { TextureAtlasAsepriteArrayDocument, TextureAtlasAsepriteHashDocument } from './textureAtlasAsepriteSchema';
5
+
6
+ const HASH_DOC: TextureAtlasAsepriteHashDocument = {
7
+ frames: {
8
+ 'hero_idle.png': {
9
+ duration: 100,
10
+ frame: { x: 0, y: 0, w: 64, h: 64 },
11
+ rotated: false,
12
+ sourceSize: { w: 64, h: 64 },
13
+ spriteSourceSize: { x: 0, y: 0, w: 64, h: 64 },
14
+ trimmed: false,
15
+ },
16
+ 'hero_walk.png': {
17
+ duration: 80,
18
+ frame: { x: 64, y: 0, w: 48, h: 56 },
19
+ rotated: false,
20
+ sourceSize: { w: 64, h: 64 },
21
+ spriteSourceSize: { x: 8, y: 4, w: 48, h: 56 },
22
+ trimmed: true,
23
+ },
24
+ },
25
+ meta: {
26
+ app: 'https://www.aseprite.org/',
27
+ format: 'RGBA8888',
28
+ image: 'atlas.png',
29
+ scale: 1,
30
+ size: { w: 256, h: 256 },
31
+ version: '1.3',
32
+ },
33
+ };
34
+
35
+ const ARRAY_DOC: TextureAtlasAsepriteArrayDocument = {
36
+ frames: [
37
+ {
38
+ duration: 100,
39
+ filename: 'tile_0.png',
40
+ frame: { x: 0, y: 0, w: 32, h: 32 },
41
+ rotated: false,
42
+ sourceSize: { w: 32, h: 32 },
43
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
44
+ trimmed: false,
45
+ },
46
+ ],
47
+ meta: {
48
+ app: 'https://www.aseprite.org/',
49
+ format: 'RGBA8888',
50
+ image: 'tiles.png',
51
+ scale: '1',
52
+ size: { w: 32, h: 32 },
53
+ version: '1.3',
54
+ },
55
+ };
56
+
57
+ describe('parseTextureAtlasAsepriteDocument', () => {
58
+ it('populates regions from a hash-format document', () => {
59
+ const atlas = createTextureAtlas();
60
+ parseTextureAtlasAsepriteDocument(HASH_DOC, atlas);
61
+ expect(atlas.regions).toHaveLength(2);
62
+ });
63
+ it('populates regions from an array-format document', () => {
64
+ const atlas = createTextureAtlas();
65
+ parseTextureAtlasAsepriteDocument(ARRAY_DOC, atlas);
66
+ expect(atlas.regions).toHaveLength(1);
67
+ expect(atlas.regions[0].name).toBe('tile_0.png');
68
+ });
69
+ it('assigns sequential ids starting at 0', () => {
70
+ const atlas = createTextureAtlas();
71
+ parseTextureAtlasAsepriteDocument(ARRAY_DOC, atlas);
72
+ expect(atlas.regions[0].id).toBe(0);
73
+ });
74
+ it('sets x/y/width/height from frame', () => {
75
+ const atlas = createTextureAtlas();
76
+ parseTextureAtlasAsepriteDocument(ARRAY_DOC, atlas);
77
+ expect(atlas.regions[0].x).toBe(0);
78
+ expect(atlas.regions[0].y).toBe(0);
79
+ expect(atlas.regions[0].width).toBe(32);
80
+ expect(atlas.regions[0].height).toBe(32);
81
+ });
82
+ it('sets trimmed fields when frame is trimmed', () => {
83
+ const atlas = createTextureAtlas();
84
+ parseTextureAtlasAsepriteDocument(HASH_DOC, atlas);
85
+ const walk = atlas.regions.find((r) => r.name === 'hero_walk.png')!;
86
+ expect(walk.trimmed).toBe(true);
87
+ expect(walk.originalWidth).toBe(64);
88
+ expect(walk.originalHeight).toBe(64);
89
+ expect(walk.sourceX).toBe(8);
90
+ expect(walk.sourceY).toBe(4);
91
+ });
92
+ it('sets null originalWidth/Height when not trimmed', () => {
93
+ const atlas = createTextureAtlas();
94
+ parseTextureAtlasAsepriteDocument(HASH_DOC, atlas);
95
+ const idle = atlas.regions.find((r) => r.name === 'hero_idle.png')!;
96
+ expect(idle.trimmed).toBe(false);
97
+ expect(idle.originalWidth).toBeNull();
98
+ expect(idle.originalHeight).toBeNull();
99
+ });
100
+ it('sets null pivot (Aseprite does not export pivot)', () => {
101
+ const atlas = createTextureAtlas();
102
+ parseTextureAtlasAsepriteDocument(ARRAY_DOC, atlas);
103
+ expect(atlas.regions[0].pivotX).toBeNull();
104
+ expect(atlas.regions[0].pivotY).toBeNull();
105
+ });
106
+ it('clears existing regions before parsing', () => {
107
+ const atlas = createTextureAtlas();
108
+ parseTextureAtlasAsepriteDocument(ARRAY_DOC, atlas);
109
+ parseTextureAtlasAsepriteDocument(ARRAY_DOC, atlas);
110
+ expect(atlas.regions).toHaveLength(1);
111
+ });
112
+ it('returns the atlas for chaining', () => {
113
+ const atlas = createTextureAtlas();
114
+ const result = parseTextureAtlasAsepriteDocument(ARRAY_DOC, atlas);
115
+ expect(result).toBe(atlas);
116
+ });
117
+ });
118
+
119
+ describe('parseTextureAtlasAsepriteJson', () => {
120
+ it('parses a JSON string and populates atlas regions', () => {
121
+ const atlas = createTextureAtlas();
122
+ parseTextureAtlasAsepriteJson(JSON.stringify(ARRAY_DOC), atlas);
123
+ expect(atlas.regions).toHaveLength(1);
124
+ expect(atlas.regions[0].name).toBe('tile_0.png');
125
+ });
126
+ it('returns the atlas for chaining', () => {
127
+ const atlas = createTextureAtlas();
128
+ const result = parseTextureAtlasAsepriteJson(JSON.stringify(ARRAY_DOC), atlas);
129
+ expect(result).toBe(atlas);
130
+ });
131
+ });
@@ -0,0 +1,98 @@
1
+ import {
2
+ TextureAtlasFormatKindAseprite,
3
+ TextureAtlasFormatKindLibgdxAtlas,
4
+ TextureAtlasFormatKindStarling,
5
+ TextureAtlasFormatKindTexturePacker,
6
+ } from '@flighthq/types';
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ import { detectTextureAtlasFormat } from './textureAtlasDetect';
10
+
11
+ const STARLING_XML = `<?xml version="1.0" encoding="UTF-8"?>
12
+ <TextureAtlas imagePath="atlas.png">
13
+ <SubTexture name="hero_idle" x="0" y="0" width="64" height="64"/>
14
+ </TextureAtlas>`;
15
+
16
+ const LIBGDX_ATLAS = `
17
+ atlas.png
18
+ size: 256,256
19
+ format: RGBA8888
20
+ filter: Nearest,Nearest
21
+ repeat: none
22
+ hero_idle
23
+ rotate: false
24
+ xy: 0, 0
25
+ size: 64, 64
26
+ orig: 64, 64
27
+ offset: 0, 0
28
+ index: -1`;
29
+
30
+ const ASEPRITE_JSON = JSON.stringify({
31
+ frames: {
32
+ hero_idle: {
33
+ frame: { x: 0, y: 0, w: 64, h: 64 },
34
+ rotated: false,
35
+ trimmed: false,
36
+ spriteSourceSize: { x: 0, y: 0, w: 64, h: 64 },
37
+ sourceSize: { w: 64, h: 64 },
38
+ duration: 100,
39
+ },
40
+ },
41
+ meta: { app: 'https://www.aseprite.org/', image: 'atlas.png', size: { w: 64, h: 64 }, scale: 1 },
42
+ });
43
+
44
+ const TEXTUREPACKER_JSON = JSON.stringify({
45
+ frames: {
46
+ hero_idle: {
47
+ frame: { x: 0, y: 0, w: 64, h: 64 },
48
+ rotated: false,
49
+ trimmed: false,
50
+ spriteSourceSize: { x: 0, y: 0, w: 64, h: 64 },
51
+ sourceSize: { w: 64, h: 64 },
52
+ },
53
+ },
54
+ meta: { app: 'https://www.codeandweb.com/texturepacker', image: 'atlas.png', size: { w: 64, h: 64 }, scale: 1 },
55
+ });
56
+
57
+ describe('detectTextureAtlasFormat', () => {
58
+ it('detects Starling XML by its <TextureAtlas> root', () => {
59
+ expect(detectTextureAtlasFormat(STARLING_XML)).toBe(TextureAtlasFormatKindStarling);
60
+ });
61
+
62
+ it('detects a libGDX / Spine text atlas by its header and region blocks', () => {
63
+ expect(detectTextureAtlasFormat(LIBGDX_ATLAS)).toBe(TextureAtlasFormatKindLibgdxAtlas);
64
+ });
65
+
66
+ it('detects Aseprite JSON by its meta.app string', () => {
67
+ expect(detectTextureAtlasFormat(ASEPRITE_JSON)).toBe(TextureAtlasFormatKindAseprite);
68
+ });
69
+
70
+ it('detects TexturePacker JSON by its meta.app string', () => {
71
+ expect(detectTextureAtlasFormat(TEXTUREPACKER_JSON)).toBe(TextureAtlasFormatKindTexturePacker);
72
+ });
73
+
74
+ it('detects an array-shaped Aseprite document', () => {
75
+ const arrayDoc = JSON.stringify({
76
+ frames: [{ filename: 'a', frame: { x: 0, y: 0, w: 1, h: 1 }, duration: 50 }],
77
+ meta: { app: 'aseprite' },
78
+ });
79
+ expect(detectTextureAtlasFormat(arrayDoc)).toBe(TextureAtlasFormatKindAseprite);
80
+ });
81
+
82
+ it('falls back to the per-frame duration field when meta.app is unrecognised', () => {
83
+ const asepriteNoApp = JSON.stringify({ frames: { a: { duration: 100 } }, meta: { app: 'unknown' } });
84
+ const packerNoApp = JSON.stringify({ frames: { a: { rotated: false } }, meta: { app: 'unknown' } });
85
+ expect(detectTextureAtlasFormat(asepriteNoApp)).toBe(TextureAtlasFormatKindAseprite);
86
+ expect(detectTextureAtlasFormat(packerNoApp)).toBe(TextureAtlasFormatKindTexturePacker);
87
+ });
88
+
89
+ it('returns null for unrecognised, empty, or malformed input', () => {
90
+ expect(detectTextureAtlasFormat('')).toBeNull();
91
+ expect(detectTextureAtlasFormat(' ')).toBeNull();
92
+ expect(detectTextureAtlasFormat('not a known format')).toBeNull();
93
+ expect(detectTextureAtlasFormat('{ not json')).toBeNull();
94
+ expect(detectTextureAtlasFormat('<plist><dict></dict></plist>')).toBeNull();
95
+ expect(detectTextureAtlasFormat('{"meta":{"app":"aseprite"}}')).toBeNull();
96
+ expect(detectTextureAtlasFormat('[1,2,3]')).toBeNull();
97
+ });
98
+ });
@@ -0,0 +1,114 @@
1
+ import { createTextureAtlas } from '@flighthq/textureatlas';
2
+
3
+ import { parseTextureAtlasLibgdxAtlas } from './textureAtlasLibgdxParse';
4
+
5
+ const SIMPLE_ATLAS = `
6
+ atlas.png
7
+ size: 256,256
8
+ format: RGBA8888
9
+ filter: Nearest,Nearest
10
+ repeat: none
11
+ hero_idle
12
+ rotate: false
13
+ xy: 0, 0
14
+ size: 64, 64
15
+ orig: 64, 64
16
+ offset: 0, 0
17
+ index: -1
18
+ hero_walk
19
+ rotate: false
20
+ xy: 64, 0
21
+ size: 48, 56
22
+ orig: 64, 64
23
+ offset: 8, 4
24
+ index: -1
25
+ hero_run
26
+ rotate: true
27
+ xy: 112, 0
28
+ size: 32, 48
29
+ orig: 48, 32
30
+ offset: 0, 0
31
+ index: -1
32
+ `;
33
+
34
+ describe('parseTextureAtlasLibgdxAtlas', () => {
35
+ it('populates regions from a libGDX atlas string', () => {
36
+ const atlas = createTextureAtlas();
37
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
38
+ expect(atlas.regions).toHaveLength(3);
39
+ });
40
+ it('assigns sequential ids starting at 0', () => {
41
+ const atlas = createTextureAtlas();
42
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
43
+ expect(atlas.regions[0].id).toBe(0);
44
+ expect(atlas.regions[2].id).toBe(2);
45
+ });
46
+ it('sets region name', () => {
47
+ const atlas = createTextureAtlas();
48
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
49
+ expect(atlas.regions[0].name).toBe('hero_idle');
50
+ expect(atlas.regions[1].name).toBe('hero_walk');
51
+ });
52
+ it('sets x/y/width/height from xy and size', () => {
53
+ const atlas = createTextureAtlas();
54
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
55
+ expect(atlas.regions[0].x).toBe(0);
56
+ expect(atlas.regions[0].y).toBe(0);
57
+ expect(atlas.regions[0].width).toBe(64);
58
+ expect(atlas.regions[0].height).toBe(64);
59
+ expect(atlas.regions[1].x).toBe(64);
60
+ expect(atlas.regions[1].width).toBe(48);
61
+ });
62
+ it('sets trimmed fields when orig differs from size', () => {
63
+ const atlas = createTextureAtlas();
64
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
65
+ const walk = atlas.regions[1];
66
+ expect(walk.trimmed).toBe(true);
67
+ expect(walk.originalWidth).toBe(64);
68
+ expect(walk.originalHeight).toBe(64);
69
+ expect(walk.sourceX).toBe(8);
70
+ expect(walk.sourceY).toBe(4);
71
+ });
72
+ it('marks non-trimmed regions correctly', () => {
73
+ const atlas = createTextureAtlas();
74
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
75
+ expect(atlas.regions[0].trimmed).toBe(false);
76
+ expect(atlas.regions[0].originalWidth).toBeNull();
77
+ });
78
+ it('sets rotated on rotated regions', () => {
79
+ const atlas = createTextureAtlas();
80
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
81
+ expect(atlas.regions[2].rotated).toBe(true);
82
+ expect(atlas.regions[0].rotated).toBe(false);
83
+ });
84
+ it('appends index to name when index >= 0', () => {
85
+ const indexedAtlas = `
86
+ atlas.png
87
+ size: 256,256
88
+ format: RGBA8888
89
+ filter: Nearest,Nearest
90
+ repeat: none
91
+ hero_walk
92
+ rotate: false
93
+ xy: 0, 0
94
+ size: 64, 64
95
+ orig: 64, 64
96
+ offset: 0, 0
97
+ index: 1
98
+ `;
99
+ const atlas = createTextureAtlas();
100
+ parseTextureAtlasLibgdxAtlas(indexedAtlas, atlas);
101
+ expect(atlas.regions[0].name).toBe('hero_walk_1');
102
+ });
103
+ it('clears existing regions before parsing', () => {
104
+ const atlas = createTextureAtlas();
105
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
106
+ parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
107
+ expect(atlas.regions).toHaveLength(3);
108
+ });
109
+ it('returns the atlas for chaining', () => {
110
+ const atlas = createTextureAtlas();
111
+ const result = parseTextureAtlasLibgdxAtlas(SIMPLE_ATLAS, atlas);
112
+ expect(result).toBe(atlas);
113
+ });
114
+ });
@@ -0,0 +1,206 @@
1
+ import { createTextureAtlas } from '@flighthq/textureatlas';
2
+
3
+ import { parseTextureAtlasPackerDocument, parseTextureAtlasPackerJson } from './textureAtlasPackerParse';
4
+ import type { TextureAtlasPackerArrayDocument, TextureAtlasPackerHashDocument } from './textureAtlasPackerSchema';
5
+
6
+ // Minimal TexturePacker JSON-Hash fixture
7
+ const HASH_FIXTURE: TextureAtlasPackerHashDocument = {
8
+ frames: {
9
+ 'hero_idle_0.png': {
10
+ frame: { x: 0, y: 0, w: 64, h: 64 },
11
+ pivot: { x: 0.5, y: 0.5 },
12
+ rotated: false,
13
+ sourceSize: { w: 64, h: 64 },
14
+ spriteSourceSize: { x: 0, y: 0, w: 64, h: 64 },
15
+ trimmed: false,
16
+ },
17
+ 'hero_walk_0.png': {
18
+ frame: { x: 64, y: 0, w: 48, h: 56 },
19
+ pivot: { x: 0.5, y: 1.0 },
20
+ rotated: false,
21
+ sourceSize: { w: 64, h: 64 },
22
+ spriteSourceSize: { x: 8, y: 4, w: 48, h: 56 },
23
+ trimmed: true,
24
+ },
25
+ },
26
+ meta: {
27
+ app: 'https://www.codeandweb.com/texturepacker',
28
+ format: 'RGBA8888',
29
+ image: 'atlas.png',
30
+ scale: 1,
31
+ size: { w: 256, h: 256 },
32
+ version: '1.0',
33
+ },
34
+ };
35
+
36
+ // Minimal TexturePacker JSON-Array fixture
37
+ const ARRAY_FIXTURE: TextureAtlasPackerArrayDocument = {
38
+ frames: [
39
+ {
40
+ filename: 'tile_0.png',
41
+ frame: { x: 0, y: 0, w: 32, h: 32 },
42
+ rotated: false,
43
+ sourceSize: { w: 32, h: 32 },
44
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
45
+ trimmed: false,
46
+ },
47
+ {
48
+ filename: 'tile_1.png',
49
+ frame: { x: 32, y: 0, w: 32, h: 32 },
50
+ rotated: true,
51
+ sourceSize: { w: 32, h: 32 },
52
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
53
+ trimmed: false,
54
+ },
55
+ ],
56
+ meta: {
57
+ app: 'https://www.codeandweb.com/texturepacker',
58
+ format: 'RGBA8888',
59
+ image: 'tiles.png',
60
+ scale: '1',
61
+ size: { w: 64, h: 32 },
62
+ version: '1.0',
63
+ },
64
+ };
65
+
66
+ describe('parseTextureAtlasPackerDocument', () => {
67
+ it('populates regions from a hash-format document', () => {
68
+ const atlas = createTextureAtlas();
69
+ parseTextureAtlasPackerDocument(HASH_FIXTURE, atlas);
70
+ expect(atlas.regions).toHaveLength(2);
71
+ });
72
+ it('populates regions from an array-format document', () => {
73
+ const atlas = createTextureAtlas();
74
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
75
+ expect(atlas.regions).toHaveLength(2);
76
+ });
77
+ it('sets region name from the frame key (hash format)', () => {
78
+ const atlas = createTextureAtlas();
79
+ parseTextureAtlasPackerDocument(HASH_FIXTURE, atlas);
80
+ const names = atlas.regions.map((r) => r.name);
81
+ expect(names).toContain('hero_idle_0.png');
82
+ expect(names).toContain('hero_walk_0.png');
83
+ });
84
+ it('sets region name from filename field (array format)', () => {
85
+ const atlas = createTextureAtlas();
86
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
87
+ expect(atlas.regions[0].name).toBe('tile_0.png');
88
+ expect(atlas.regions[1].name).toBe('tile_1.png');
89
+ });
90
+ it('assigns sequential ids starting at 0', () => {
91
+ const atlas = createTextureAtlas();
92
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
93
+ expect(atlas.regions[0].id).toBe(0);
94
+ expect(atlas.regions[1].id).toBe(1);
95
+ });
96
+ it('populates x/y/width/height from the frame rect', () => {
97
+ const atlas = createTextureAtlas();
98
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
99
+ expect(atlas.regions[0].x).toBe(0);
100
+ expect(atlas.regions[0].y).toBe(0);
101
+ expect(atlas.regions[0].width).toBe(32);
102
+ expect(atlas.regions[0].height).toBe(32);
103
+ });
104
+ it('sets trimmed fields when the frame is trimmed', () => {
105
+ const atlas = createTextureAtlas();
106
+ parseTextureAtlasPackerDocument(HASH_FIXTURE, atlas);
107
+ const trimmed = atlas.regions.find((r) => r.name === 'hero_walk_0.png');
108
+ expect(trimmed).toBeDefined();
109
+ expect(trimmed!.trimmed).toBe(true);
110
+ expect(trimmed!.sourceX).toBe(8);
111
+ expect(trimmed!.sourceY).toBe(4);
112
+ expect(trimmed!.originalWidth).toBe(64);
113
+ expect(trimmed!.originalHeight).toBe(64);
114
+ });
115
+ it('sets null originalWidth/Height when the frame is not trimmed', () => {
116
+ const atlas = createTextureAtlas();
117
+ parseTextureAtlasPackerDocument(HASH_FIXTURE, atlas);
118
+ const idle = atlas.regions.find((r) => r.name === 'hero_idle_0.png')!;
119
+ expect(idle.trimmed).toBe(false);
120
+ expect(idle.originalWidth).toBeNull();
121
+ expect(idle.originalHeight).toBeNull();
122
+ });
123
+ it('sets pivot when pivot is present', () => {
124
+ const atlas = createTextureAtlas();
125
+ parseTextureAtlasPackerDocument(HASH_FIXTURE, atlas);
126
+ const idle = atlas.regions.find((r) => r.name === 'hero_idle_0.png')!;
127
+ expect(idle.pivotX).toBe(0.5);
128
+ expect(idle.pivotY).toBe(0.5);
129
+ });
130
+ it('sets null pivot when pivot is absent', () => {
131
+ const atlas = createTextureAtlas();
132
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
133
+ expect(atlas.regions[0].pivotX).toBeNull();
134
+ expect(atlas.regions[0].pivotY).toBeNull();
135
+ });
136
+ it('sets rotated on rotated regions', () => {
137
+ const atlas = createTextureAtlas();
138
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
139
+ expect(atlas.regions[0].rotated).toBe(false);
140
+ expect(atlas.regions[1].rotated).toBe(true);
141
+ });
142
+ it('swaps width/height for rotated regions', () => {
143
+ // Rotated 90°: packed w=32,h=32 with rotation → logical w=h, h=w (same here since square)
144
+ const rotatedDoc: TextureAtlasPackerArrayDocument = {
145
+ frames: [
146
+ {
147
+ filename: 'rotated.png',
148
+ frame: { x: 0, y: 0, w: 20, h: 40 },
149
+ rotated: true,
150
+ sourceSize: { w: 40, h: 20 },
151
+ spriteSourceSize: { x: 0, y: 0, w: 40, h: 20 },
152
+ trimmed: false,
153
+ },
154
+ ],
155
+ meta: { app: 'tp', format: 'RGBA8888', image: 'a.png', scale: 1, size: { w: 64, h: 64 }, version: '1.0' },
156
+ };
157
+ const atlas = createTextureAtlas();
158
+ parseTextureAtlasPackerDocument(rotatedDoc, atlas);
159
+ // When rotated: logical width = packed h, logical height = packed w
160
+ expect(atlas.regions[0].width).toBe(40);
161
+ expect(atlas.regions[0].height).toBe(20);
162
+ });
163
+ it('clears existing regions before parsing', () => {
164
+ const atlas = createTextureAtlas();
165
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
166
+ parseTextureAtlasPackerDocument(ARRAY_FIXTURE, atlas);
167
+ expect(atlas.regions).toHaveLength(2);
168
+ });
169
+ it('returns the atlas for chaining', () => {
170
+ const atlas = createTextureAtlas();
171
+ const result = parseTextureAtlasPackerDocument(HASH_FIXTURE, atlas);
172
+ expect(result).toBe(atlas);
173
+ });
174
+ });
175
+
176
+ describe('parseTextureAtlasPackerJson', () => {
177
+ it('parses a JSON string and populates atlas regions', () => {
178
+ const atlas = createTextureAtlas();
179
+ const json = JSON.stringify(ARRAY_FIXTURE);
180
+ parseTextureAtlasPackerJson(json, atlas);
181
+ expect(atlas.regions).toHaveLength(2);
182
+ expect(atlas.regions[0].name).toBe('tile_0.png');
183
+ });
184
+ it('accepts an optional stripPathPrefix option', () => {
185
+ const docWithPaths: TextureAtlasPackerHashDocument = {
186
+ frames: {
187
+ 'sprites/hero.png': {
188
+ frame: { x: 0, y: 0, w: 32, h: 32 },
189
+ rotated: false,
190
+ sourceSize: { w: 32, h: 32 },
191
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
192
+ trimmed: false,
193
+ },
194
+ },
195
+ meta: { app: 'tp', format: 'RGBA8888', image: 'a.png', scale: 1, size: { w: 64, h: 64 }, version: '1.0' },
196
+ };
197
+ const atlas = createTextureAtlas();
198
+ parseTextureAtlasPackerJson(JSON.stringify(docWithPaths), atlas, { stripPathPrefix: true });
199
+ expect(atlas.regions[0].name).toBe('hero.png');
200
+ });
201
+ it('returns the atlas for chaining', () => {
202
+ const atlas = createTextureAtlas();
203
+ const result = parseTextureAtlasPackerJson(JSON.stringify(ARRAY_FIXTURE), atlas);
204
+ expect(result).toBe(atlas);
205
+ });
206
+ });
@@ -0,0 +1,92 @@
1
+ import { createTextureAtlas } from '@flighthq/textureatlas';
2
+
3
+ import { parseTextureAtlasStarlingXml } from './textureAtlasStarlingParse';
4
+
5
+ const SIMPLE_XML = `<?xml version="1.0" encoding="UTF-8"?>
6
+ <TextureAtlas imagePath="atlas.png">
7
+ <SubTexture name="hero_idle" x="0" y="0" width="64" height="64"/>
8
+ <SubTexture name="hero_walk" x="64" y="0" width="48" height="56"
9
+ frameX="-8" frameY="-4" frameWidth="64" frameHeight="64"/>
10
+ <SubTexture name="hero_jump" x="112" y="0" width="40" height="50" rotated="true"/>
11
+ <SubTexture name="coin" x="152" y="0" width="32" height="32" pivotX="16" pivotY="16"/>
12
+ </TextureAtlas>`;
13
+
14
+ describe('parseTextureAtlasStarlingXml', () => {
15
+ it('populates regions from a Starling XML string', () => {
16
+ const atlas = createTextureAtlas();
17
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
18
+ expect(atlas.regions).toHaveLength(4);
19
+ });
20
+ it('assigns sequential ids starting at 0', () => {
21
+ const atlas = createTextureAtlas();
22
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
23
+ expect(atlas.regions[0].id).toBe(0);
24
+ expect(atlas.regions[3].id).toBe(3);
25
+ });
26
+ it('sets region name from the SubTexture name attribute', () => {
27
+ const atlas = createTextureAtlas();
28
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
29
+ expect(atlas.regions[0].name).toBe('hero_idle');
30
+ expect(atlas.regions[1].name).toBe('hero_walk');
31
+ });
32
+ it('sets x/y/width/height from SubTexture attributes', () => {
33
+ const atlas = createTextureAtlas();
34
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
35
+ expect(atlas.regions[0].x).toBe(0);
36
+ expect(atlas.regions[0].y).toBe(0);
37
+ expect(atlas.regions[0].width).toBe(64);
38
+ expect(atlas.regions[0].height).toBe(64);
39
+ });
40
+ it('sets trimmed fields when frameX/frameWidth are present', () => {
41
+ const atlas = createTextureAtlas();
42
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
43
+ const walk = atlas.regions[1];
44
+ expect(walk.trimmed).toBe(true);
45
+ expect(walk.originalWidth).toBe(64);
46
+ expect(walk.originalHeight).toBe(64);
47
+ expect(walk.sourceX).toBe(8); // -frameX = 8
48
+ expect(walk.sourceY).toBe(4); // -frameY = 4
49
+ });
50
+ it('marks non-trimmed regions correctly', () => {
51
+ const atlas = createTextureAtlas();
52
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
53
+ expect(atlas.regions[0].trimmed).toBe(false);
54
+ expect(atlas.regions[0].originalWidth).toBeNull();
55
+ expect(atlas.regions[0].originalHeight).toBeNull();
56
+ });
57
+ it('sets rotated on regions with rotated="true"', () => {
58
+ const atlas = createTextureAtlas();
59
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
60
+ expect(atlas.regions[2].rotated).toBe(true);
61
+ expect(atlas.regions[0].rotated).toBe(false);
62
+ });
63
+ it('sets pivot when pivotX/pivotY attributes are present', () => {
64
+ const atlas = createTextureAtlas();
65
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
66
+ const coin = atlas.regions[3];
67
+ expect(coin.pivotX).toBe(16);
68
+ expect(coin.pivotY).toBe(16);
69
+ });
70
+ it('sets null pivot when pivot attributes are absent', () => {
71
+ const atlas = createTextureAtlas();
72
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
73
+ expect(atlas.regions[0].pivotX).toBeNull();
74
+ expect(atlas.regions[0].pivotY).toBeNull();
75
+ });
76
+ it('clears existing regions before parsing', () => {
77
+ const atlas = createTextureAtlas();
78
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
79
+ parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
80
+ expect(atlas.regions).toHaveLength(4);
81
+ });
82
+ it('returns the atlas for chaining', () => {
83
+ const atlas = createTextureAtlas();
84
+ const result = parseTextureAtlasStarlingXml(SIMPLE_XML, atlas);
85
+ expect(result).toBe(atlas);
86
+ });
87
+ it('returns the atlas unchanged when no SubTexture elements exist', () => {
88
+ const atlas = createTextureAtlas();
89
+ parseTextureAtlasStarlingXml('<TextureAtlas imagePath="a.png"></TextureAtlas>', atlas);
90
+ expect(atlas.regions).toHaveLength(0);
91
+ });
92
+ });