@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.
- package/README.md +3 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +2 -2
- package/dist/node.cjs +2 -2
- package/dist/node.js +2 -2
- package/dist/{publish-BJk8UzRP.js → publish--8vagaSA.js} +135 -210
- package/dist/{publish-DCP0AYx2.cjs → publish-Bv7E2fZE.cjs} +139 -208
- package/dist/{restore-C98ugT2H.js → restore-CknfGMJM.js} +9 -1
- package/dist/{restore-BUaCK8ui.cjs → restore-iTgeWr7-.cjs} +9 -1
- package/dist/web.cjs +33 -4
- package/dist/web.d.cts +3 -0
- package/dist/web.d.ts +3 -0
- package/dist/web.js +33 -4
- package/package.json +2 -2
- package/src/adapters/web/publish.ts +40 -3
- package/src/atlas/inputs.ts +72 -110
- package/src/atlas/jta.ts +116 -170
- package/src/atlas.ts +12 -1
- package/src/publish.ts +45 -2
- package/src/restore.ts +10 -0
|
@@ -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:
|
|
133
|
-
fileExtension:
|
|
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);
|
package/src/atlas/inputs.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
303
|
-
.setSwing(jta.meta
|
|
304
|
-
.setRepeatDelay(jta.meta
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 (
|
|
371
|
-
resource.setWidth(jta.meta
|
|
372
|
-
resource.setHeight(jta.meta
|
|
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
|
|
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
|
|
29
|
+
meta: JtaMeta;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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:
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
82
|
+
if (data.length >= 2 && data[0] === 0xff && data[1] === 0xd8) return 'jpeg';
|
|
83
|
+
return null;
|
|
63
84
|
}
|
|
64
85
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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<
|
|
144
|
+
Omit<
|
|
145
|
+
AtlasOptions,
|
|
146
|
+
'packages' | 'encoder' | 'basePath' | 'outputPath' | 'mkdir' | 'readFileRaw' | 'preparedMovieClips'
|
|
147
|
+
>
|
|
137
148
|
> = {
|
|
138
149
|
maxSize: 2048,
|
|
139
150
|
fast: true,
|