@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.
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/textureAtlasAsepriteParse.d.ts +5 -0
- package/dist/textureAtlasAsepriteParse.d.ts.map +1 -0
- package/dist/textureAtlasAsepriteParse.js +42 -0
- package/dist/textureAtlasAsepriteParse.js.map +1 -0
- package/dist/textureAtlasAsepriteSchema.d.ts +53 -0
- package/dist/textureAtlasAsepriteSchema.d.ts.map +1 -0
- package/dist/textureAtlasAsepriteSchema.js +6 -0
- package/dist/textureAtlasAsepriteSchema.js.map +1 -0
- package/dist/textureAtlasDetect.d.ts +16 -0
- package/dist/textureAtlasDetect.d.ts.map +1 -0
- package/dist/textureAtlasDetect.js +71 -0
- package/dist/textureAtlasDetect.js.map +1 -0
- package/dist/textureAtlasLibgdxParse.d.ts +3 -0
- package/dist/textureAtlasLibgdxParse.d.ts.map +1 -0
- package/dist/textureAtlasLibgdxParse.js +114 -0
- package/dist/textureAtlasLibgdxParse.js.map +1 -0
- package/dist/textureAtlasPackerParse.d.ts +9 -0
- package/dist/textureAtlasPackerParse.d.ts.map +1 -0
- package/dist/textureAtlasPackerParse.js +57 -0
- package/dist/textureAtlasPackerParse.js.map +1 -0
- package/dist/textureAtlasPackerSchema.d.ts +52 -0
- package/dist/textureAtlasPackerSchema.d.ts.map +1 -0
- package/dist/textureAtlasPackerSchema.js +5 -0
- package/dist/textureAtlasPackerSchema.js.map +1 -0
- package/dist/textureAtlasStarlingParse.d.ts +14 -0
- package/dist/textureAtlasStarlingParse.d.ts.map +1 -0
- package/dist/textureAtlasStarlingParse.js +50 -0
- package/dist/textureAtlasStarlingParse.js.map +1 -0
- package/package.json +39 -0
- package/src/textureAtlasAsepriteParse.test.ts +131 -0
- package/src/textureAtlasDetect.test.ts +98 -0
- package/src/textureAtlasLibgdxParse.test.ts +114 -0
- package/src/textureAtlasPackerParse.test.ts +206 -0
- 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
|
+
});
|