@flighthq/spritesheet-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 (75) hide show
  1. package/dist/asepriteParse.d.ts +18 -0
  2. package/dist/asepriteParse.d.ts.map +1 -0
  3. package/dist/asepriteParse.js +87 -0
  4. package/dist/asepriteParse.js.map +1 -0
  5. package/dist/asepriteSchema.d.ts +64 -0
  6. package/dist/asepriteSchema.d.ts.map +1 -0
  7. package/dist/asepriteSchema.js +6 -0
  8. package/dist/asepriteSchema.js.map +1 -0
  9. package/dist/asepriteSerialize.d.ts +14 -0
  10. package/dist/asepriteSerialize.d.ts.map +1 -0
  11. package/dist/asepriteSerialize.js +86 -0
  12. package/dist/asepriteSerialize.js.map +1 -0
  13. package/dist/cocosPlistParse.d.ts +15 -0
  14. package/dist/cocosPlistParse.d.ts.map +1 -0
  15. package/dist/cocosPlistParse.js +157 -0
  16. package/dist/cocosPlistParse.js.map +1 -0
  17. package/dist/cocosPlistSchema.d.ts +31 -0
  18. package/dist/cocosPlistSchema.d.ts.map +1 -0
  19. package/dist/cocosPlistSchema.js +6 -0
  20. package/dist/cocosPlistSchema.js.map +1 -0
  21. package/dist/cocosPlistSerialize.d.ts +8 -0
  22. package/dist/cocosPlistSerialize.d.ts.map +1 -0
  23. package/dist/cocosPlistSerialize.js +86 -0
  24. package/dist/cocosPlistSerialize.js.map +1 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +16 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/libgdxAtlasParse.d.ts +13 -0
  30. package/dist/libgdxAtlasParse.d.ts.map +1 -0
  31. package/dist/libgdxAtlasParse.js +209 -0
  32. package/dist/libgdxAtlasParse.js.map +1 -0
  33. package/dist/libgdxAtlasSchema.d.ts +38 -0
  34. package/dist/libgdxAtlasSchema.d.ts.map +1 -0
  35. package/dist/libgdxAtlasSchema.js +6 -0
  36. package/dist/libgdxAtlasSchema.js.map +1 -0
  37. package/dist/spritesheetDetect.d.ts +41 -0
  38. package/dist/spritesheetDetect.d.ts.map +1 -0
  39. package/dist/spritesheetDetect.js +101 -0
  40. package/dist/spritesheetDetect.js.map +1 -0
  41. package/dist/starlingParse.d.ts +20 -0
  42. package/dist/starlingParse.d.ts.map +1 -0
  43. package/dist/starlingParse.js +138 -0
  44. package/dist/starlingParse.js.map +1 -0
  45. package/dist/starlingSchema.d.ts +32 -0
  46. package/dist/starlingSchema.d.ts.map +1 -0
  47. package/dist/starlingSchema.js +6 -0
  48. package/dist/starlingSchema.js.map +1 -0
  49. package/dist/starlingSerialize.d.ts +8 -0
  50. package/dist/starlingSerialize.d.ts.map +1 -0
  51. package/dist/starlingSerialize.js +67 -0
  52. package/dist/starlingSerialize.js.map +1 -0
  53. package/dist/texturePackerParse.d.ts +15 -0
  54. package/dist/texturePackerParse.d.ts.map +1 -0
  55. package/dist/texturePackerParse.js +70 -0
  56. package/dist/texturePackerParse.js.map +1 -0
  57. package/dist/texturePackerSchema.d.ts +52 -0
  58. package/dist/texturePackerSchema.d.ts.map +1 -0
  59. package/dist/texturePackerSchema.js +5 -0
  60. package/dist/texturePackerSchema.js.map +1 -0
  61. package/dist/texturePackerSerialize.d.ts +14 -0
  62. package/dist/texturePackerSerialize.d.ts.map +1 -0
  63. package/dist/texturePackerSerialize.js +74 -0
  64. package/dist/texturePackerSerialize.js.map +1 -0
  65. package/package.json +41 -0
  66. package/src/asepriteParse.test.ts +235 -0
  67. package/src/asepriteSerialize.test.ts +218 -0
  68. package/src/cocosPlistParse.test.ts +131 -0
  69. package/src/cocosPlistSerialize.test.ts +56 -0
  70. package/src/libgdxAtlasParse.test.ts +100 -0
  71. package/src/spritesheetDetect.test.ts +217 -0
  72. package/src/starlingParse.test.ts +176 -0
  73. package/src/starlingSerialize.test.ts +117 -0
  74. package/src/texturePackerParse.test.ts +237 -0
  75. package/src/texturePackerSerialize.test.ts +200 -0
@@ -0,0 +1,235 @@
1
+ import { parseAsepriteSpritesheet, parseAsepriteSpritesheetDocument } from './asepriteParse';
2
+
3
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
4
+
5
+ const HASH_JSON = JSON.stringify({
6
+ frames: {
7
+ 'sprite 0.aseprite': {
8
+ frame: { x: 0, y: 0, w: 32, h: 32 },
9
+ rotated: false,
10
+ trimmed: false,
11
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
12
+ sourceSize: { w: 32, h: 32 },
13
+ duration: 100,
14
+ },
15
+ 'sprite 1.aseprite': {
16
+ frame: { x: 32, y: 0, w: 32, h: 32 },
17
+ rotated: false,
18
+ trimmed: false,
19
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
20
+ sourceSize: { w: 32, h: 32 },
21
+ duration: 150,
22
+ },
23
+ 'sprite 2.aseprite': {
24
+ frame: { x: 64, y: 0, w: 30, h: 28 },
25
+ rotated: false,
26
+ trimmed: true,
27
+ spriteSourceSize: { x: 1, y: 2, w: 30, h: 28 },
28
+ sourceSize: { w: 32, h: 32 },
29
+ duration: 200,
30
+ },
31
+ 'sprite 3.aseprite': {
32
+ frame: { x: 96, y: 0, w: 32, h: 32 },
33
+ rotated: false,
34
+ trimmed: false,
35
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
36
+ sourceSize: { w: 32, h: 32 },
37
+ duration: 100,
38
+ },
39
+ },
40
+ meta: {
41
+ app: 'https://www.aseprite.org/',
42
+ version: '1.3',
43
+ image: 'sprite.png',
44
+ format: 'RGBA8888',
45
+ size: { w: 128, h: 32 },
46
+ scale: '1',
47
+ frameTags: [
48
+ { name: 'run', from: 0, to: 1, direction: 'forward' },
49
+ { name: 'jump', from: 2, to: 3, direction: 'reverse' },
50
+ ],
51
+ layers: [{ name: 'Layer 1', opacity: 255, blendMode: 'normal' }],
52
+ },
53
+ });
54
+
55
+ const ARRAY_JSON = JSON.stringify({
56
+ frames: [
57
+ {
58
+ filename: 'anim 0.aseprite',
59
+ frame: { x: 0, y: 0, w: 16, h: 16 },
60
+ rotated: false,
61
+ trimmed: false,
62
+ spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
63
+ sourceSize: { w: 16, h: 16 },
64
+ duration: 80,
65
+ },
66
+ {
67
+ filename: 'anim 1.aseprite',
68
+ frame: { x: 16, y: 0, w: 16, h: 16 },
69
+ rotated: false,
70
+ trimmed: false,
71
+ spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
72
+ sourceSize: { w: 16, h: 16 },
73
+ duration: 80,
74
+ },
75
+ ],
76
+ meta: {
77
+ app: 'https://www.aseprite.org/',
78
+ version: '1.3',
79
+ image: 'anim.png',
80
+ format: 'RGBA8888',
81
+ size: { w: 32, h: 16 },
82
+ scale: '1',
83
+ frameTags: [{ name: 'idle', from: 0, to: 1, direction: 'pingpong' }],
84
+ },
85
+ });
86
+
87
+ const NO_TAGS_JSON = JSON.stringify({
88
+ frames: {
89
+ 'solo 0.aseprite': {
90
+ frame: { x: 0, y: 0, w: 8, h: 8 },
91
+ rotated: false,
92
+ trimmed: false,
93
+ spriteSourceSize: { x: 0, y: 0, w: 8, h: 8 },
94
+ sourceSize: { w: 8, h: 8 },
95
+ duration: 100,
96
+ },
97
+ },
98
+ meta: {
99
+ app: 'https://www.aseprite.org/',
100
+ version: '1.3',
101
+ image: 'solo.png',
102
+ format: 'RGBA8888',
103
+ size: { w: 8, h: 8 },
104
+ scale: '1',
105
+ frameTags: [],
106
+ },
107
+ });
108
+
109
+ describe('parseAsepriteSpritesheet', () => {
110
+ it('returns a SpritesheetData (not a Parsed object)', () => {
111
+ const result = parseAsepriteSpritesheet(HASH_JSON);
112
+ expect(typeof result.frames).toBe('object');
113
+ expect((result as unknown as Record<string, unknown>).document).toBeUndefined();
114
+ });
115
+
116
+ it('parses frame count from Hash variant', () => {
117
+ expect(parseAsepriteSpritesheet(HASH_JSON).frames).toHaveLength(4);
118
+ });
119
+
120
+ it('parses frame count from Array variant', () => {
121
+ expect(parseAsepriteSpritesheet(ARRAY_JSON).frames).toHaveLength(2);
122
+ });
123
+
124
+ it('maps frame name from Hash key', () => {
125
+ const data = parseAsepriteSpritesheet(HASH_JSON);
126
+ expect(data.frames[0].name).toBe('sprite 0.aseprite');
127
+ expect(data.frames[2].name).toBe('sprite 2.aseprite');
128
+ });
129
+
130
+ it('maps frame name from Array filename field', () => {
131
+ const data = parseAsepriteSpritesheet(ARRAY_JSON);
132
+ expect(data.frames[0].name).toBe('anim 0.aseprite');
133
+ expect(data.frames[1].name).toBe('anim 1.aseprite');
134
+ });
135
+
136
+ it('maps atlas position and size', () => {
137
+ const data = parseAsepriteSpritesheet(HASH_JSON);
138
+ expect(data.frames[0].x).toBe(0);
139
+ expect(data.frames[0].y).toBe(0);
140
+ expect(data.frames[0].width).toBe(32);
141
+ expect(data.frames[0].height).toBe(32);
142
+ });
143
+
144
+ it('maps trim offset', () => {
145
+ const data = parseAsepriteSpritesheet(HASH_JSON);
146
+ const trimmed = data.frames[2];
147
+ expect(trimmed.offsetX).toBe(1);
148
+ expect(trimmed.offsetY).toBe(2);
149
+ expect(trimmed.sourceWidth).toBe(32);
150
+ expect(trimmed.sourceHeight).toBe(32);
151
+ });
152
+
153
+ it('has null pivots (Aseprite format has no pivot data)', () => {
154
+ const data = parseAsepriteSpritesheet(HASH_JSON);
155
+ expect(data.frames[0].pivotX).toBeNull();
156
+ expect(data.frames[0].pivotY).toBeNull();
157
+ });
158
+
159
+ it('maps image file and atlas size', () => {
160
+ const data = parseAsepriteSpritesheet(HASH_JSON);
161
+ expect(data.imageFile).toBe('sprite.png');
162
+ expect(data.imageWidth).toBe(128);
163
+ expect(data.imageHeight).toBe(32);
164
+ });
165
+
166
+ it('parses animations from frameTags', () => {
167
+ const data = parseAsepriteSpritesheet(HASH_JSON);
168
+ expect(data.animations).toHaveLength(2);
169
+ expect(data.animations[0].name).toBe('run');
170
+ expect(data.animations[1].name).toBe('jump');
171
+ });
172
+
173
+ it('maps animation direction', () => {
174
+ const data = parseAsepriteSpritesheet(HASH_JSON);
175
+ expect(data.animations[0].direction).toBe('forward');
176
+ expect(data.animations[1].direction).toBe('reverse');
177
+ });
178
+
179
+ it('maps animation frameNames via tag from/to indices', () => {
180
+ const data = parseAsepriteSpritesheet(HASH_JSON);
181
+ expect(data.animations[0].frameNames).toEqual(['sprite 0.aseprite', 'sprite 1.aseprite']);
182
+ expect(data.animations[1].frameNames).toEqual(['sprite 2.aseprite', 'sprite 3.aseprite']);
183
+ });
184
+
185
+ it('sets frameDurations when per-frame durations vary', () => {
186
+ const data = parseAsepriteSpritesheet(HASH_JSON);
187
+ const run = data.animations[0];
188
+ // frames 0 and 1 have durations 100 and 150 — they differ
189
+ expect(run.frameDurations).toEqual([100, 150]);
190
+ expect(run.frameDuration).toBe(100);
191
+ });
192
+
193
+ it('sets frameDurations to null when all frame durations are uniform', () => {
194
+ const data = parseAsepriteSpritesheet(ARRAY_JSON);
195
+ const idle = data.animations[0];
196
+ // both frames have duration 80
197
+ expect(idle.frameDurations).toBeNull();
198
+ expect(idle.frameDuration).toBe(80);
199
+ });
200
+
201
+ it('produces empty animations when frameTags array is empty', () => {
202
+ expect(parseAsepriteSpritesheet(NO_TAGS_JSON).animations).toHaveLength(0);
203
+ });
204
+
205
+ it('maps pingpong direction from Array variant', () => {
206
+ const data = parseAsepriteSpritesheet(ARRAY_JSON);
207
+ expect(data.animations[0].direction).toBe('pingpong');
208
+ });
209
+ });
210
+
211
+ describe('parseAsepriteSpritesheetDocument', () => {
212
+ it('returns the same data as parseAsepriteSpritesheet', () => {
213
+ const parsed = parseAsepriteSpritesheet(HASH_JSON);
214
+ const { data } = parseAsepriteSpritesheetDocument(HASH_JSON);
215
+ expect(data.frames.length).toBe(parsed.frames.length);
216
+ expect(data.imageFile).toBe(parsed.imageFile);
217
+ });
218
+
219
+ it('preserves the original Hash document', () => {
220
+ const { document } = parseAsepriteSpritesheetDocument(HASH_JSON);
221
+ expect(Array.isArray(document.frames)).toBe(false);
222
+ expect((document.frames as Record<string, unknown>)['sprite 0.aseprite']).toBeDefined();
223
+ });
224
+
225
+ it('preserves the original Array document', () => {
226
+ const { document } = parseAsepriteSpritesheetDocument(ARRAY_JSON);
227
+ expect(Array.isArray(document.frames)).toBe(true);
228
+ });
229
+
230
+ it('preserves layers in document meta', () => {
231
+ const { document } = parseAsepriteSpritesheetDocument(HASH_JSON);
232
+ expect(document.meta.layers).toHaveLength(1);
233
+ expect(document.meta.layers![0].name).toBe('Layer 1');
234
+ });
235
+ });
@@ -0,0 +1,218 @@
1
+ import { parseAsepriteSpritesheet, parseAsepriteSpritesheetDocument } from './asepriteParse';
2
+ import { serializeAsepriteSpritesheet } from './asepriteSerialize';
3
+
4
+ const HASH_JSON = JSON.stringify({
5
+ frames: {
6
+ 'frame0.aseprite': {
7
+ duration: 100,
8
+ frame: { h: 32, w: 32, x: 0, y: 0 },
9
+ rotated: false,
10
+ sourceSize: { h: 32, w: 32 },
11
+ spriteSourceSize: { h: 32, w: 32, x: 0, y: 0 },
12
+ trimmed: false,
13
+ },
14
+ },
15
+ meta: {
16
+ app: 'https://www.aseprite.org/',
17
+ format: 'RGBA8888',
18
+ frameTags: [],
19
+ image: 'sprite.png',
20
+ scale: '1',
21
+ size: { h: 32, w: 64 },
22
+ version: '1.3',
23
+ },
24
+ });
25
+
26
+ const ROUNDTRIP_HASH_JSON = JSON.stringify({
27
+ frames: {
28
+ 'sprite 0.aseprite': {
29
+ frame: { x: 0, y: 0, w: 32, h: 32 },
30
+ rotated: false,
31
+ trimmed: false,
32
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
33
+ sourceSize: { w: 32, h: 32 },
34
+ duration: 100,
35
+ },
36
+ 'sprite 1.aseprite': {
37
+ frame: { x: 32, y: 0, w: 32, h: 32 },
38
+ rotated: false,
39
+ trimmed: false,
40
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
41
+ sourceSize: { w: 32, h: 32 },
42
+ duration: 150,
43
+ },
44
+ 'sprite 2.aseprite': {
45
+ frame: { x: 64, y: 0, w: 30, h: 28 },
46
+ rotated: false,
47
+ trimmed: true,
48
+ spriteSourceSize: { x: 1, y: 2, w: 30, h: 28 },
49
+ sourceSize: { w: 32, h: 32 },
50
+ duration: 200,
51
+ },
52
+ 'sprite 3.aseprite': {
53
+ frame: { x: 96, y: 0, w: 32, h: 32 },
54
+ rotated: false,
55
+ trimmed: false,
56
+ spriteSourceSize: { x: 0, y: 0, w: 32, h: 32 },
57
+ sourceSize: { w: 32, h: 32 },
58
+ duration: 100,
59
+ },
60
+ },
61
+ meta: {
62
+ app: 'https://www.aseprite.org/',
63
+ version: '1.3',
64
+ image: 'sprite.png',
65
+ format: 'RGBA8888',
66
+ size: { w: 128, h: 32 },
67
+ scale: '1',
68
+ frameTags: [
69
+ { name: 'run', from: 0, to: 1, direction: 'forward' },
70
+ { name: 'jump', from: 2, to: 3, direction: 'reverse' },
71
+ ],
72
+ layers: [{ name: 'Layer 1', opacity: 255, blendMode: 'normal' }],
73
+ },
74
+ });
75
+
76
+ const ROUNDTRIP_ARRAY_JSON = JSON.stringify({
77
+ frames: [
78
+ {
79
+ filename: 'anim 0.aseprite',
80
+ frame: { x: 0, y: 0, w: 16, h: 16 },
81
+ rotated: false,
82
+ trimmed: false,
83
+ spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
84
+ sourceSize: { w: 16, h: 16 },
85
+ duration: 80,
86
+ },
87
+ {
88
+ filename: 'anim 1.aseprite',
89
+ frame: { x: 16, y: 0, w: 16, h: 16 },
90
+ rotated: false,
91
+ trimmed: false,
92
+ spriteSourceSize: { x: 0, y: 0, w: 16, h: 16 },
93
+ sourceSize: { w: 16, h: 16 },
94
+ duration: 80,
95
+ },
96
+ ],
97
+ meta: {
98
+ app: 'https://www.aseprite.org/',
99
+ version: '1.3',
100
+ image: 'anim.png',
101
+ format: 'RGBA8888',
102
+ size: { w: 32, h: 16 },
103
+ scale: '1',
104
+ frameTags: [{ name: 'idle', from: 0, to: 1, direction: 'pingpong' }],
105
+ },
106
+ });
107
+
108
+ const ROUNDTRIP_NO_TAGS_JSON = JSON.stringify({
109
+ frames: {
110
+ 'solo 0.aseprite': {
111
+ frame: { x: 0, y: 0, w: 8, h: 8 },
112
+ rotated: false,
113
+ trimmed: false,
114
+ spriteSourceSize: { x: 0, y: 0, w: 8, h: 8 },
115
+ sourceSize: { w: 8, h: 8 },
116
+ duration: 100,
117
+ },
118
+ },
119
+ meta: {
120
+ app: 'https://www.aseprite.org/',
121
+ version: '1.3',
122
+ image: 'solo.png',
123
+ format: 'RGBA8888',
124
+ size: { w: 8, h: 8 },
125
+ scale: '1',
126
+ frameTags: [],
127
+ },
128
+ });
129
+
130
+ describe('serializeAsepriteSpritesheet', () => {
131
+ it('produces valid JSON that can be reparsed', () => {
132
+ const data = parseAsepriteSpritesheet(HASH_JSON);
133
+ const serialized = serializeAsepriteSpritesheet(data);
134
+ const reparsed = JSON.parse(serialized) as Record<string, unknown>;
135
+ expect(reparsed).toHaveProperty('frames');
136
+ expect(reparsed).toHaveProperty('meta');
137
+ });
138
+
139
+ it('preserves frame count', () => {
140
+ const data = parseAsepriteSpritesheet(HASH_JSON);
141
+ const serialized = serializeAsepriteSpritesheet(data);
142
+ const reparsed = parseAsepriteSpritesheet(serialized);
143
+ expect(reparsed.frames).toHaveLength(data.frames.length);
144
+ });
145
+
146
+ it('serializes array variant when option is set', () => {
147
+ const data = parseAsepriteSpritesheet(HASH_JSON);
148
+ const serialized = serializeAsepriteSpritesheet(data, undefined, { variant: 'array' });
149
+ const parsed = JSON.parse(serialized) as { frames: unknown[] };
150
+ expect(Array.isArray(parsed.frames)).toBe(true);
151
+ });
152
+
153
+ it('round-trips frame names', () => {
154
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
155
+ const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
156
+ expect(data2.frames.map((f) => f.name)).toEqual(data.frames.map((f) => f.name));
157
+ });
158
+
159
+ it('round-trips frame positions', () => {
160
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
161
+ const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
162
+ expect(data2.frames[0].x).toBe(data.frames[0].x);
163
+ expect(data2.frames[0].width).toBe(data.frames[0].width);
164
+ expect(data2.frames[2].offsetX).toBe(data.frames[2].offsetX);
165
+ });
166
+
167
+ it('round-trips variable per-frame durations', () => {
168
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
169
+ const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
170
+ expect(data2.animations[0].frameDurations).toEqual([100, 150]);
171
+ });
172
+
173
+ it('round-trips uniform durations as null frameDurations', () => {
174
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_ARRAY_JSON);
175
+ const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
176
+ expect(data2.animations[0].frameDurations).toBeNull();
177
+ expect(data2.animations[0].frameDuration).toBe(80);
178
+ });
179
+
180
+ it('round-trips animation names and directions', () => {
181
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
182
+ const data2 = parseAsepriteSpritesheet(serializeAsepriteSpritesheet(data, document));
183
+ expect(data2.animations[0].name).toBe('run');
184
+ expect(data2.animations[1].name).toBe('jump');
185
+ expect(data2.animations[1].direction).toBe('reverse');
186
+ });
187
+
188
+ it('preserves layer metadata through the document', () => {
189
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
190
+ const json2 = serializeAsepriteSpritesheet(data, document);
191
+ const { document: doc2 } = parseAsepriteSpritesheetDocument(json2);
192
+ expect(doc2.meta.layers).toHaveLength(1);
193
+ });
194
+
195
+ it('emits array variant when existing is array', () => {
196
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_ARRAY_JSON);
197
+ const parsed = JSON.parse(serializeAsepriteSpritesheet(data, document));
198
+ expect(Array.isArray(parsed.frames)).toBe(true);
199
+ expect(parsed.frames[0].filename).toBeDefined();
200
+ });
201
+
202
+ it('emits hash variant by default', () => {
203
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
204
+ const parsed = JSON.parse(serializeAsepriteSpritesheet(data, document));
205
+ expect(Array.isArray(parsed.frames)).toBe(false);
206
+ });
207
+
208
+ it('respects variant override option', () => {
209
+ const { data, document } = parseAsepriteSpritesheetDocument(ROUNDTRIP_HASH_JSON);
210
+ const parsed = JSON.parse(serializeAsepriteSpritesheet(data, document, { variant: 'array' }));
211
+ expect(Array.isArray(parsed.frames)).toBe(true);
212
+ });
213
+
214
+ it('produces valid JSON', () => {
215
+ const data = parseAsepriteSpritesheet(ROUNDTRIP_NO_TAGS_JSON);
216
+ expect(() => JSON.parse(serializeAsepriteSpritesheet(data))).not.toThrow();
217
+ });
218
+ });
@@ -0,0 +1,131 @@
1
+ import { parseCocosPlistSpritesheet, parseCocosPlistSpritesheetDocument } from './cocosPlistParse';
2
+
3
+ const MINIMAL_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
4
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
5
+ <plist version="1.0">
6
+ <dict>
7
+ <key>frames</key>
8
+ <dict>
9
+ <key>hero.png</key>
10
+ <dict>
11
+ <key>frame</key>
12
+ <string>{{0,0},{64,64}}</string>
13
+ <key>spriteOffset</key>
14
+ <string>{0,0}</string>
15
+ <key>spriteSize</key>
16
+ <string>{64,64}</string>
17
+ <key>spriteSourceSize</key>
18
+ <string>{64,64}</string>
19
+ <key>spriteTrimmed</key>
20
+ <false/>
21
+ <key>textureRotated</key>
22
+ <false/>
23
+ </dict>
24
+ <key>coin.png</key>
25
+ <dict>
26
+ <key>frame</key>
27
+ <string>{{64,0},{32,32}}</string>
28
+ <key>spriteOffset</key>
29
+ <string>{0,0}</string>
30
+ <key>spriteSize</key>
31
+ <string>{32,32}</string>
32
+ <key>spriteSourceSize</key>
33
+ <string>{32,32}</string>
34
+ <key>spriteTrimmed</key>
35
+ <false/>
36
+ <key>textureRotated</key>
37
+ <false/>
38
+ </dict>
39
+ </dict>
40
+ <key>metadata</key>
41
+ <dict>
42
+ <key>format</key>
43
+ <integer>3</integer>
44
+ <key>size</key>
45
+ <string>{256,128}</string>
46
+ <key>textureFileName</key>
47
+ <string>atlas.png</string>
48
+ </dict>
49
+ </dict>
50
+ </plist>`;
51
+
52
+ const ROTATED_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
53
+ <plist version="1.0">
54
+ <dict>
55
+ <key>frames</key>
56
+ <dict>
57
+ <key>rotated.png</key>
58
+ <dict>
59
+ <key>frame</key>
60
+ <string>{{0,0},{32,64}}</string>
61
+ <key>spriteOffset</key>
62
+ <string>{0,0}</string>
63
+ <key>spriteSize</key>
64
+ <string>{64,32}</string>
65
+ <key>spriteSourceSize</key>
66
+ <string>{64,32}</string>
67
+ <key>spriteTrimmed</key>
68
+ <false/>
69
+ <key>textureRotated</key>
70
+ <true/>
71
+ </dict>
72
+ </dict>
73
+ <key>metadata</key>
74
+ <dict>
75
+ <key>format</key>
76
+ <integer>3</integer>
77
+ <key>size</key>
78
+ <string>{128,128}</string>
79
+ <key>textureFileName</key>
80
+ <string>rotated.png</string>
81
+ </dict>
82
+ </dict>
83
+ </plist>`;
84
+
85
+ describe('parseCocosPlistSpritesheet', () => {
86
+ it('parses frame names', () => {
87
+ const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
88
+ const names = data.frames.map((f) => f.name).sort();
89
+ expect(names).toContain('hero.png');
90
+ expect(names).toContain('coin.png');
91
+ });
92
+
93
+ it('parses frame positions', () => {
94
+ const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
95
+ const hero = data.frames.find((f) => f.name === 'hero.png');
96
+ expect(hero?.x).toBe(0);
97
+ expect(hero?.y).toBe(0);
98
+ expect(hero?.width).toBe(64);
99
+ expect(hero?.height).toBe(64);
100
+ });
101
+
102
+ it('parses atlas image file and dimensions', () => {
103
+ const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
104
+ expect(data.imageFile).toBe('atlas.png');
105
+ expect(data.imageWidth).toBe(256);
106
+ expect(data.imageHeight).toBe(128);
107
+ });
108
+
109
+ it('handles rotated frames with swapped dimensions', () => {
110
+ const data = parseCocosPlistSpritesheet(ROTATED_PLIST);
111
+ const frame = data.frames.find((f) => f.name === 'rotated.png');
112
+ expect(frame?.rotated).toBe(true);
113
+ // Rotated: atlas rect is 32x64, so in logical orientation it is 64x32
114
+ expect(frame?.width).toBe(64);
115
+ expect(frame?.height).toBe(32);
116
+ });
117
+
118
+ it('produces empty animations array', () => {
119
+ const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
120
+ expect(data.animations).toHaveLength(0);
121
+ });
122
+ });
123
+
124
+ describe('parseCocosPlistSpritesheetDocument', () => {
125
+ it('returns both data and document', () => {
126
+ const result = parseCocosPlistSpritesheetDocument(MINIMAL_PLIST);
127
+ expect(result.data.frames.length).toBeGreaterThan(0);
128
+ expect(result.document.metadata.textureFileName).toBe('atlas.png');
129
+ expect(result.document.metadata.format).toBe(3);
130
+ });
131
+ });
@@ -0,0 +1,56 @@
1
+ import { parseCocosPlistSpritesheet } from './cocosPlistParse';
2
+ import { serializeCocosPlistSpritesheet } from './cocosPlistSerialize';
3
+
4
+ const MINIMAL_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
5
+ <plist version="1.0">
6
+ <dict>
7
+ <key>frames</key>
8
+ <dict>
9
+ <key>hero.png</key>
10
+ <dict>
11
+ <key>frame</key>
12
+ <string>{{0,0},{64,64}}</string>
13
+ <key>spriteOffset</key>
14
+ <string>{0,0}</string>
15
+ <key>spriteSize</key>
16
+ <string>{64,64}</string>
17
+ <key>spriteSourceSize</key>
18
+ <string>{64,64}</string>
19
+ <key>spriteTrimmed</key>
20
+ <false/>
21
+ <key>textureRotated</key>
22
+ <false/>
23
+ </dict>
24
+ </dict>
25
+ <key>metadata</key>
26
+ <dict>
27
+ <key>format</key>
28
+ <integer>3</integer>
29
+ <key>size</key>
30
+ <string>{128,64}</string>
31
+ <key>textureFileName</key>
32
+ <string>atlas.png</string>
33
+ </dict>
34
+ </dict>
35
+ </plist>`;
36
+
37
+ describe('serializeCocosPlistSpritesheet', () => {
38
+ it('produces XML with a plist root element', () => {
39
+ const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
40
+ const serialized = serializeCocosPlistSpritesheet(data);
41
+ expect(serialized).toContain('<plist');
42
+ expect(serialized).toContain('</plist>');
43
+ });
44
+ it('preserves frame count on round-trip', () => {
45
+ const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
46
+ const serialized = serializeCocosPlistSpritesheet(data);
47
+ const reparsed = parseCocosPlistSpritesheet(serialized);
48
+ expect(reparsed.frames).toHaveLength(data.frames.length);
49
+ });
50
+ it('preserves image file on round-trip', () => {
51
+ const data = parseCocosPlistSpritesheet(MINIMAL_PLIST);
52
+ const serialized = serializeCocosPlistSpritesheet(data);
53
+ const reparsed = parseCocosPlistSpritesheet(serialized);
54
+ expect(reparsed.imageFile).toBe(data.imageFile);
55
+ });
56
+ });