@openfairygui/functions 0.2.0-alpha.34 → 0.2.0-alpha.35

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.
@@ -1,11 +1,13 @@
1
1
  import { type Document, type ILogger, ProjectType } from '@openfairygui/core';
2
2
  import type { AtlasOptions } from '../../atlas.js';
3
+ import { resolveCodeGenerationSettings } from '../../codegen.js';
3
4
  import { publish } from '../../publish.js';
4
5
  import type {
5
6
  PublishFileSystem,
6
7
  PublishOutputFileSystem,
7
8
  PublishSourceFileSystem,
8
9
  } from '../../publish/contracts.js';
10
+ import { resolvePublishOptions } from '../../publish/options.js';
9
11
  import { assertBrowserImageSupport, createBrowserImageEncoder } from './raster.js';
10
12
 
11
13
  export type BrowserPublishProjectType = 'layabox';
@@ -42,6 +44,9 @@ export interface BrowserPublishOptions {
42
44
  export interface BrowserPublishDiagnostic {
43
45
  level: 'debug' | 'info' | 'warning' | 'error';
44
46
  message: string;
47
+ code?: 'unsupported_publish_setting' | 'publish_failed';
48
+ setting?: string;
49
+ path?: string;
45
50
  }
46
51
 
47
52
  export interface BrowserPublishedFile {
@@ -103,6 +108,10 @@ function toResult(
103
108
  };
104
109
  }
105
110
 
111
+ function unsupportedSetting(setting: string, path: string, message: string): BrowserPublishDiagnostic {
112
+ return { level: 'error', code: 'unsupported_publish_setting', setting, path, message };
113
+ }
114
+
106
115
  /**
107
116
  * Publish a loaded FairyGUI project to browser-provided storage.
108
117
  *
@@ -121,16 +130,43 @@ export async function publishBrowser(options: BrowserPublishOptions): Promise<Br
121
130
  if (options.projectType !== 'layabox') {
122
131
  throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
123
132
  }
124
- assertBrowserImageSupport();
125
133
  root.setProjectType(ProjectType.LayaBox);
134
+ const resolved = resolvePublishOptions(options.document, {
135
+ compressed: options.compressed,
136
+ packages: options.packages,
137
+ atlas: options.atlas,
138
+ });
139
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolved.fileExtension)) {
140
+ diagnostics.push(unsupportedSetting(
141
+ 'fileExtension',
142
+ 'settings.publish.fileExtension',
143
+ `publishBrowser: unsupported fileExtension "${resolved.fileExtension}".`,
144
+ ));
145
+ return toResult(false, files, diagnostics);
146
+ }
147
+ const selectedPackageNames = options.packages?.length ? new Set(options.packages) : null;
148
+ const selectedPackages = root.listPackages().filter((pkg) => !selectedPackageNames || selectedPackageNames.has(pkg.getName()));
149
+ if (resolveCodeGenerationSettings(options.document).allowGenCode) {
150
+ const packageIndex = selectedPackages.findIndex((pkg) => pkg.getGenCode());
151
+ if (packageIndex >= 0) {
152
+ const pkg = selectedPackages[packageIndex]!;
153
+ diagnostics.push(unsupportedSetting(
154
+ 'codeGeneration',
155
+ `packages[${root.listPackages().indexOf(pkg)}].publish.genCode`,
156
+ `publishBrowser: code generation requested by package "${pkg.getName()}" is not supported.`,
157
+ ));
158
+ return toResult(false, files, diagnostics);
159
+ }
160
+ }
161
+ assertBrowserImageSupport();
126
162
  const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
127
163
  const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), 'assets');
128
164
 
129
165
  await options.document.transform(
130
166
  publish({
131
167
  output: options.output,
132
- compressed: options.compressed,
133
- fileExtension: 'fui',
168
+ compressed: resolved.compressed,
169
+ fileExtension: resolved.fileExtension,
134
170
  packages: options.packages,
135
171
  branch: options.branch,
136
172
  basePath: sourceAssetsPath,
@@ -149,6 +185,7 @@ export async function publishBrowser(options: BrowserPublishOptions): Promise<Br
149
185
  } catch (error) {
150
186
  diagnostics.push({
151
187
  level: 'error',
188
+ code: 'publish_failed',
152
189
  message: error instanceof Error ? error.message : String(error),
153
190
  });
154
191
  return toResult(false, files, diagnostics);
@@ -21,7 +21,7 @@ import {
21
21
  } from '../publish/package-context.js';
22
22
  import type { ExtrasMap } from '../shared-types.js';
23
23
  import { parseFnt } from './font.js';
24
- import { extractJtaFrames } from './jta.js';
24
+ import { prepareJtaForPublish, type PreparedJtaData } from './jta.js';
25
25
 
26
26
  /** Trim info for a single image. */
27
27
  interface TrimInfo {
@@ -181,6 +181,36 @@ export interface PagedAtlasGroup {
181
181
  inputs: InputItem[];
182
182
  }
183
183
 
184
+ export function resolveMovieClipSourcePath(resource: MovieClipResource, pkg: Package, basePath: string): string {
185
+ const fileName = `${resource.getName()}.jta`;
186
+ const resourcePath = resource.getPath() ?? '/';
187
+ return `${basePath}/${pkg.getName()}${resourcePath}${fileName}`;
188
+ }
189
+
190
+ export async function prepareMovieClipResource(
191
+ resource: MovieClipResource,
192
+ pkg: Package,
193
+ encoder: AtlasRasterBackend | undefined,
194
+ basePath: string,
195
+ readFileRaw: (path: string) => Promise<Uint8Array>,
196
+ ): Promise<PreparedJtaData> {
197
+ const filePath = resolveMovieClipSourcePath(resource, pkg, basePath);
198
+ let raw: Uint8Array;
199
+ try {
200
+ raw = await readFileRaw(filePath);
201
+ } catch {
202
+ throw new Error(`atlas: Could not read MovieClip "${filePath}".`);
203
+ }
204
+
205
+ try {
206
+ return await prepareJtaForPublish(raw, encoder, filePath);
207
+ } catch (error) {
208
+ if (error instanceof Error && error.message.startsWith('atlas:')) throw error;
209
+ const detail = error instanceof Error ? ` ${error.message}` : '';
210
+ throw new Error(`atlas: Could not parse MovieClip "${filePath}".${detail}`);
211
+ }
212
+ }
213
+
184
214
  /** Collect a single ImageResource into the inputs array. */
185
215
  export async function collectImage(
186
216
  resource: ImageResource,
@@ -285,130 +315,62 @@ export async function collectMovieClipFrames(
285
315
  }
286
316
 
287
317
  const mcId = resource.getId();
288
- const mcName = resource.getName() + '.jta';
289
- const mcPath = resource.getPath() ?? '/';
290
- const filePath = `${options.basePath}/${pkg.getName()}${mcPath}${mcName}`;
318
+ const filePath = resolveMovieClipSourcePath(resource, pkg, options.basePath);
291
319
 
292
320
  try {
293
- const raw = await options.readFileRaw(filePath);
294
- const jta = extractJtaFrames(raw);
295
- if (jta.frames.length === 0) return;
296
-
297
- const frameMetas = jta.meta?.frames ?? [];
321
+ const jta =
322
+ options.preparedMovieClips?.get(resource) ??
323
+ (await prepareMovieClipResource(resource, pkg, encoder, options.basePath, options.readFileRaw));
298
324
  for (const frame of resource.listFrames()) {
299
325
  resource.removeFrame(frame);
300
326
  }
301
327
  resource
302
- .setInterval(jta.meta?.interval ?? 100)
303
- .setSwing(jta.meta?.swing ?? false)
304
- .setRepeatDelay(jta.meta?.repeatDelay ?? 0);
305
-
306
- if (frameMetas.length > 0) {
307
- const firstFrameIndexByTextureIndex = new Map<number, number>();
308
- for (let frameIndex = 0; frameIndex < frameMetas.length; frameIndex += 1) {
309
- const meta = frameMetas[frameIndex];
310
- const textureIndex = Number.isFinite(meta.textureIndex) ? meta.textureIndex : frameIndex;
311
- if (!firstFrameIndexByTextureIndex.has(textureIndex)) {
312
- firstFrameIndexByTextureIndex.set(textureIndex, frameIndex);
313
- }
314
- }
315
-
316
- const spriteIdByTextureIndex = new Map<number, string>();
317
- for (let textureIndex = 0; textureIndex < jta.frames.length; textureIndex += 1) {
318
- const exportFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
319
- if (exportFrameIndex === undefined) continue;
320
- const itemId = `${mcId}_${exportFrameIndex}`;
321
- const input = await createMovieClipFrameInput(
322
- jta.frames[textureIndex],
323
- itemId,
328
+ .setInterval(jta.meta.interval)
329
+ .setSwing(jta.meta.swing)
330
+ .setRepeatDelay(jta.meta.repeatDelay);
331
+ const spriteIdByTextureIndex = new Map<number, string>();
332
+ for (const texture of jta.referencedTextures) {
333
+ if (texture.width <= 0 || texture.height <= 0) continue;
334
+ const itemId = `${mcId}_${texture.firstFrameIndex}`;
335
+ inputs.push({
336
+ id: itemId,
337
+ width: texture.width,
338
+ height: texture.height,
339
+ originalWidth: texture.width,
340
+ originalHeight: texture.height,
341
+ offsetX: 0,
342
+ offsetY: 0,
324
343
  resource,
325
- encoder,
326
- options.strictOutput,
327
- );
328
- if (!input) continue;
329
- inputs.push(input);
330
- spriteIdByTextureIndex.set(textureIndex, itemId);
331
- }
344
+ trimBuffer: texture.buffer,
345
+ sourceKind: 'movieclip-frame',
346
+ });
347
+ spriteIdByTextureIndex.set(texture.textureIndex, itemId);
348
+ }
332
349
 
333
- for (let frameIndex = 0; frameIndex < frameMetas.length; frameIndex += 1) {
334
- const meta = frameMetas[frameIndex];
335
- const textureIndex = Number.isFinite(meta.textureIndex) ? meta.textureIndex : frameIndex;
336
- const frame = doc.createMovieFrame(`${mcId}_${frameIndex}`);
337
- frame
338
- .setRectX(meta.offsetX)
339
- .setRectY(meta.offsetY)
340
- .setRectWidth(meta.width)
341
- .setRectHeight(meta.height)
342
- .setAddDelay(meta.addDelay)
343
- .setSpriteId(spriteIdByTextureIndex.get(textureIndex) ?? '');
344
- resource.addFrame(frame);
345
- }
346
- } else {
347
- for (let frameIndex = 0; frameIndex < jta.frames.length; frameIndex += 1) {
348
- const itemId = `${mcId}_${frameIndex}`;
349
- const input = await createMovieClipFrameInput(
350
- jta.frames[frameIndex],
351
- itemId,
352
- resource,
353
- encoder,
354
- options.strictOutput,
355
- );
356
- if (!input) continue;
357
- inputs.push(input);
358
- const frame = doc.createMovieFrame(itemId);
359
- frame
360
- .setRectX(0)
361
- .setRectY(0)
362
- .setRectWidth(input.originalWidth)
363
- .setRectHeight(input.originalHeight)
364
- .setAddDelay(0)
365
- .setSpriteId(itemId);
366
- resource.addFrame(frame);
367
- }
350
+ for (let frameIndex = 0; frameIndex < jta.meta.frames.length; frameIndex += 1) {
351
+ const meta = jta.meta.frames[frameIndex]!;
352
+ const frame = doc.createMovieFrame(`${mcId}_${frameIndex}`);
353
+ frame
354
+ .setRectX(meta.offsetX)
355
+ .setRectY(meta.offsetY)
356
+ .setRectWidth(meta.width)
357
+ .setRectHeight(meta.height)
358
+ .setAddDelay(meta.addDelay)
359
+ .setSpriteId(meta.textureIndex === -1 ? '' : (spriteIdByTextureIndex.get(meta.textureIndex) ?? ''));
360
+ resource.addFrame(frame);
368
361
  }
369
362
 
370
- if ((jta.meta?.width ?? 0) > 0 && (jta.meta?.height ?? 0) > 0) {
371
- resource.setWidth(jta.meta?.width ?? 0);
372
- resource.setHeight(jta.meta?.height ?? 0);
363
+ if (jta.meta.width > 0 && jta.meta.height > 0) {
364
+ resource.setWidth(jta.meta.width);
365
+ resource.setHeight(jta.meta.height);
373
366
  }
374
- } catch {
375
- const message = `atlas: Could not parse MovieClip "${filePath}".`;
376
- if (options.strictOutput) throw new Error(message);
367
+ } catch (error) {
368
+ const message = error instanceof Error ? error.message : `atlas: Could not parse MovieClip "${filePath}".`;
369
+ if (options.strictOutput) throw error;
377
370
  logger.warn(`${message} Skipping frames.`);
378
371
  }
379
372
  }
380
373
 
381
- async function createMovieClipFrameInput(
382
- buffer: Uint8Array,
383
- itemId: string,
384
- resource: MovieClipResource,
385
- encoder: AtlasRasterBackend | undefined,
386
- strictOutput: boolean,
387
- ): Promise<InputItem | null> {
388
- if (!encoder || buffer.length === 0) return null;
389
- try {
390
- const meta = await encoder(buffer).metadata();
391
- const width = meta.width ?? 0;
392
- const height = meta.height ?? 0;
393
- if (width <= 0 || height <= 0) return null;
394
- return {
395
- id: itemId,
396
- width,
397
- height,
398
- originalWidth: width,
399
- originalHeight: height,
400
- offsetX: 0,
401
- offsetY: 0,
402
- resource,
403
- trimBuffer: buffer,
404
- sourceKind: 'movieclip-frame',
405
- };
406
- } catch {
407
- if (strictOutput) throw new Error(`atlas: Could not decode MovieClip frame "${itemId}".`);
408
- return null;
409
- }
410
- }
411
-
412
374
  /** Collect a Bitmap Font's texture image, packed under the font's ID. */
413
375
  export async function collectFontTexture(
414
376
  doc: Document,
package/src/atlas/jta.ts CHANGED
@@ -1,3 +1,11 @@
1
+ import {
2
+ deriveMovieClipModel,
3
+ parseJta,
4
+ probeRasterImage,
5
+ type RasterImageFormat,
6
+ } from '@openfairygui/core';
7
+ import type { AtlasRasterBackend } from '../publish/contracts.js';
8
+
1
9
  export interface JtaFrameMeta {
2
10
  addDelay: number;
3
11
  offsetX: number;
@@ -18,194 +26,132 @@ export interface JtaMeta {
18
26
 
19
27
  export interface ExtractedJtaData {
20
28
  frames: Uint8Array[];
21
- meta?: JtaMeta;
29
+ meta: JtaMeta;
22
30
  }
23
31
 
24
- const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
25
-
26
- export function extractJtaFrames(data: Uint8Array): ExtractedJtaData {
27
- const frames: Uint8Array[] = [];
28
- let offset = 0;
29
- let firstPngOffset = -1;
30
-
31
- while (offset < data.length) {
32
- const signatureIndex = findPngSignature(data, offset);
33
- if (signatureIndex === -1) break;
34
- if (firstPngOffset === -1) firstPngOffset = signatureIndex;
35
- const end = findPngEnd(data, signatureIndex);
36
- if (end === -1) break;
37
- frames.push(data.subarray(signatureIndex, end));
38
- offset = end;
39
- }
32
+ export interface PreparedJtaTexture {
33
+ textureIndex: number;
34
+ firstFrameIndex: number;
35
+ /** PNG bytes validated during preflight and reused by atlas compositing. */
36
+ buffer: Uint8Array;
37
+ width: number;
38
+ height: number;
39
+ }
40
40
 
41
- if (firstPngOffset === -1 || frames.length === 0) {
42
- return { frames: [] };
43
- }
41
+ export interface PreparedJtaData extends ExtractedJtaData {
42
+ referencedTextures: PreparedJtaTexture[];
43
+ }
44
44
 
45
+ export function extractJtaFrames(data: Uint8Array): ExtractedJtaData {
46
+ const parsed = parseJta(data);
47
+ const derived = deriveMovieClipModel(parsed);
45
48
  return {
46
- frames,
47
- meta: parseJtaHeader(data, firstPngOffset, frames.length),
49
+ frames: parsed.textures.map((texture) => texture.raw),
50
+ meta: {
51
+ interval: derived.interval,
52
+ repeatDelay: derived.repeatDelay,
53
+ swing: derived.swing,
54
+ width: derived.dimensions.width,
55
+ height: derived.dimensions.height,
56
+ frames: derived.frames.map((frame) => ({
57
+ addDelay: frame.addDelay,
58
+ offsetX: frame.rectX,
59
+ offsetY: frame.rectY,
60
+ width: frame.rectWidth,
61
+ height: frame.rectHeight,
62
+ textureIndex: frame.textureIndex,
63
+ })),
64
+ },
48
65
  };
49
66
  }
50
67
 
51
- function findPngSignature(data: Uint8Array, fromIndex: number): number {
52
- for (let index = fromIndex; index <= data.length - PNG_SIGNATURE.length; index += 1) {
53
- let matched = true;
54
- for (let signatureIndex = 0; signatureIndex < PNG_SIGNATURE.length; signatureIndex += 1) {
55
- if (data[index + signatureIndex] !== PNG_SIGNATURE[signatureIndex]) {
56
- matched = false;
57
- break;
58
- }
59
- }
60
- if (matched) return index;
68
+ function detectSupportedRasterFormat(data: Uint8Array): RasterImageFormat | null {
69
+ if (
70
+ data.length >= 8 &&
71
+ data[0] === 0x89 &&
72
+ data[1] === 0x50 &&
73
+ data[2] === 0x4e &&
74
+ data[3] === 0x47 &&
75
+ data[4] === 0x0d &&
76
+ data[5] === 0x0a &&
77
+ data[6] === 0x1a &&
78
+ data[7] === 0x0a
79
+ ) {
80
+ return 'png';
61
81
  }
62
- return -1;
82
+ if (data.length >= 2 && data[0] === 0xff && data[1] === 0xd8) return 'jpeg';
83
+ return null;
63
84
  }
64
85
 
65
- function findPngEnd(data: Uint8Array, start: number): number {
66
- let position = start + PNG_SIGNATURE.length;
67
- while (position + 8 <= data.length) {
68
- const length = readUint32BE(data, position);
69
- position += 8;
70
- if (position + length + 4 > data.length) return -1;
71
- const isEnd =
72
- data[position - 4] === 0x49 &&
73
- data[position - 3] === 0x45 &&
74
- data[position - 2] === 0x4e &&
75
- data[position - 1] === 0x44;
76
- position += length + 4;
77
- if (isEnd) return position;
78
- }
79
- return -1;
86
+ function couldNotDecode(filePath: string, frameIndex: number, textureIndex: number): Error {
87
+ return new Error(
88
+ `atlas: Could not decode MovieClip "${filePath}" frame ${frameIndex} (texture ${textureIndex}).`,
89
+ );
80
90
  }
81
91
 
82
- function parseJtaHeader(data: Uint8Array, firstPngOffset: number, frameCount: number): JtaMeta | undefined {
83
- if (data.length < 10) return undefined;
84
-
85
- const state = { offset: 0 };
86
- const end = Math.min(firstPngOffset, data.length);
87
- const mark = readUtfBE(data, state, end);
88
- if (!mark) return undefined;
89
-
90
- const version = readInt32BEAt(data, state, end);
91
- if (version == null) return undefined;
92
-
93
- const fpsRaw = readInt8At(data, state, end);
94
- if (fpsRaw == null) return undefined;
95
- const fps = fpsRaw > 0 ? fpsRaw : 24;
96
-
97
- if (state.offset + 3 > end) return undefined;
98
- state.offset += 3;
99
-
100
- if (version < 102) return undefined;
101
-
102
- readUint16BEAt(data, state, end);
103
- readUint16BEAt(data, state, end);
104
- const width = readUint16BEAt(data, state, end);
105
- const height = readUint16BEAt(data, state, end);
106
- if (width == null || height == null) return undefined;
92
+ export async function prepareJtaForPublish(
93
+ data: Uint8Array,
94
+ encoder: AtlasRasterBackend | undefined,
95
+ filePath: string,
96
+ ): Promise<PreparedJtaData> {
97
+ const extracted = extractJtaFrames(data);
98
+ const firstFrameIndexByTextureIndex = new Map<number, number>();
99
+
100
+ for (let frameIndex = 0; frameIndex < extracted.meta.frames.length; frameIndex += 1) {
101
+ const textureIndex = extracted.meta.frames[frameIndex]!.textureIndex;
102
+ if (textureIndex >= 0 && !firstFrameIndexByTextureIndex.has(textureIndex)) {
103
+ firstFrameIndexByTextureIndex.set(textureIndex, frameIndex);
104
+ }
105
+ }
107
106
 
108
- const speedRaw = readUint8At(data, state, end);
109
- const repeatDelayRaw = readUint8At(data, state, end);
110
- const swingRaw = readInt8At(data, state, end);
111
- const frameTableCount = readInt16BEAt(data, state, end);
112
- if (speedRaw == null || repeatDelayRaw == null || swingRaw == null || frameTableCount == null) return undefined;
107
+ const referencedTextures: PreparedJtaTexture[] = [];
108
+ for (let textureIndex = 0; textureIndex < extracted.frames.length; textureIndex += 1) {
109
+ const firstFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
110
+ if (firstFrameIndex === undefined) continue;
111
+ const raw = extracted.frames[textureIndex]!;
112
+ if (raw.byteLength === 0) {
113
+ throw new Error(
114
+ `atlas: MovieClip "${filePath}" frame ${firstFrameIndex} references empty texture ${textureIndex}.`,
115
+ );
116
+ }
117
+ const detectedFormat = detectSupportedRasterFormat(raw);
118
+ if (!detectedFormat) {
119
+ throw new Error(
120
+ `atlas: MovieClip "${filePath}" frame ${firstFrameIndex} (texture ${textureIndex}) uses an unsupported ` +
121
+ 'raster format; only PNG and JPEG are supported.',
122
+ );
123
+ }
124
+ const imageInfo = probeRasterImage(raw);
125
+ if (!imageInfo || imageInfo.format !== detectedFormat) {
126
+ throw couldNotDecode(filePath, firstFrameIndex, textureIndex);
127
+ }
113
128
 
114
- const frames: JtaFrameMeta[] = [];
115
- for (let index = 0; index < frameTableCount; index += 1) {
116
- const delayRaw = readInt16BEAt(data, state, end);
117
- const offsetX = readInt16BEAt(data, state, end);
118
- const offsetY = readInt16BEAt(data, state, end);
119
- const frameWidth = readInt16BEAt(data, state, end);
120
- const frameHeight = readInt16BEAt(data, state, end);
121
- const textureIndex = readInt16BEAt(data, state, end);
122
- if (
123
- delayRaw == null ||
124
- offsetX == null ||
125
- offsetY == null ||
126
- frameWidth == null ||
127
- frameHeight == null ||
128
- textureIndex == null
129
- ) {
130
- break;
129
+ let buffer = raw;
130
+ if (encoder) {
131
+ try {
132
+ buffer = await encoder(raw).png().toBuffer();
133
+ } catch {
134
+ throw couldNotDecode(filePath, firstFrameIndex, textureIndex);
135
+ }
136
+ const normalizedInfo = probeRasterImage(buffer);
137
+ if (
138
+ !normalizedInfo ||
139
+ normalizedInfo.format !== 'png' ||
140
+ normalizedInfo.width !== imageInfo.width ||
141
+ normalizedInfo.height !== imageInfo.height
142
+ ) {
143
+ throw couldNotDecode(filePath, firstFrameIndex, textureIndex);
144
+ }
131
145
  }
132
- frames.push({
133
- addDelay: Math.trunc((1000 / fps) * delayRaw),
134
- offsetX,
135
- offsetY,
136
- width: frameWidth,
137
- height: frameHeight,
146
+
147
+ referencedTextures.push({
138
148
  textureIndex,
149
+ firstFrameIndex,
150
+ buffer,
151
+ width: imageInfo.width,
152
+ height: imageInfo.height,
139
153
  });
140
154
  }
141
155
 
142
- return {
143
- interval: Math.trunc((1000 / fps) * (speedRaw || 1)),
144
- repeatDelay: Math.trunc((1000 / fps) * repeatDelayRaw),
145
- swing: swingRaw === 1,
146
- width,
147
- height,
148
- frames: frames.length === 0 && frameCount > 0 ? [] : frames,
149
- };
150
- }
151
-
152
- function readUtfBE(data: Uint8Array, state: { offset: number }, end: number): string | null {
153
- const length = readUint16BEAt(data, state, end);
154
- if (length == null || state.offset + length > end) return null;
155
- const value = new TextDecoder().decode(data.subarray(state.offset, state.offset + length));
156
- state.offset += length;
157
- return value;
158
- }
159
-
160
- function readUint8At(data: Uint8Array, state: { offset: number }, end: number): number | null {
161
- if (state.offset + 1 > end) return null;
162
- const value = data[state.offset];
163
- state.offset += 1;
164
- return value ?? 0;
165
- }
166
-
167
- function readInt8At(data: Uint8Array, state: { offset: number }, end: number): number | null {
168
- if (state.offset + 1 > end) return null;
169
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
170
- const value = view.getInt8(state.offset);
171
- state.offset += 1;
172
- return value;
173
- }
174
-
175
- function readUint16BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
176
- if (state.offset + 2 > end) return null;
177
- const value = readUint16BE(data, state.offset);
178
- state.offset += 2;
179
- return value;
180
- }
181
-
182
- function readInt16BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
183
- if (state.offset + 2 > end) return null;
184
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
185
- const value = view.getInt16(state.offset, false);
186
- state.offset += 2;
187
- return value;
188
- }
189
-
190
- function readInt32BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
191
- if (state.offset + 4 > end) return null;
192
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
193
- const value = view.getInt32(state.offset, false);
194
- state.offset += 4;
195
- return value;
196
- }
197
-
198
- function readUint16BE(data: Uint8Array, offset: number): number {
199
- if (offset + 1 >= data.length) return 0;
200
- return (data[offset] << 8) | data[offset + 1];
201
- }
202
-
203
- function readUint32BE(data: Uint8Array, offset: number): number {
204
- if (offset + 3 >= data.length) return 0;
205
- return (
206
- data[offset] * 0x1000000 +
207
- ((data[offset + 1] ?? 0) << 16) +
208
- ((data[offset + 2] ?? 0) << 8) +
209
- (data[offset + 3] ?? 0)
210
- );
156
+ return { ...extracted, referencedTextures };
211
157
  }
package/src/atlas.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  type Document,
4
4
  GearType,
5
5
  type Package,
6
+ type MovieClipResource,
6
7
  type Transform,
7
8
  TransitionActionType,
8
9
  } from '@openfairygui/core';
@@ -27,6 +28,7 @@ import {
27
28
  type PackageResource,
28
29
  } from './atlas/inputs.js';
29
30
  import { emitAtlasInputs, sortResourcesByOrder } from './atlas/packing.js';
31
+ import type { PreparedJtaData } from './atlas/jta.js';
30
32
 
31
33
  export interface AtlasOptions {
32
34
  /**
@@ -106,6 +108,12 @@ export interface AtlasOptions {
106
108
  */
107
109
  readFileRaw?: (path: string) => Promise<Uint8Array>;
108
110
 
111
+ /**
112
+ * MovieClip parse/decode results prepared by publish() before output begins.
113
+ * @internal
114
+ */
115
+ preparedMovieClips?: ReadonlyMap<MovieClipResource, PreparedJtaData>;
116
+
109
117
  /**
110
118
  * Keep original input order when MaxRects tie-break scores are equal.
111
119
  * This is an internal publish detail used to mirror editor/CLI behavior.
@@ -133,7 +141,10 @@ export interface AtlasOptions {
133
141
  }
134
142
 
135
143
  const ATLAS_DEFAULTS: Required<
136
- Omit<AtlasOptions, 'packages' | 'encoder' | 'basePath' | 'outputPath' | 'mkdir' | 'readFileRaw'>
144
+ Omit<
145
+ AtlasOptions,
146
+ 'packages' | 'encoder' | 'basePath' | 'outputPath' | 'mkdir' | 'readFileRaw' | 'preparedMovieClips'
147
+ >
137
148
  > = {
138
149
  maxSize: 2048,
139
150
  fast: true,