@openfairygui/functions 0.2.0-alpha.8 → 0.2.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/README.md +52 -6
- package/dist/atlas-C6tbl7nn.d.ts +193 -0
- package/dist/atlas-CHsu2Y8i.d.cts +193 -0
- package/dist/index.cjs +17 -3603
- package/dist/index.d.cts +5 -294
- package/dist/index.d.ts +5 -294
- package/dist/index.js +4 -3595
- package/dist/node.cjs +256 -0
- package/dist/node.d.cts +36 -0
- package/dist/node.d.ts +36 -0
- package/dist/node.js +254 -0
- package/dist/publish-CykUJfVa.cjs +3269 -0
- package/dist/publish-DXoaC1Nl.js +3174 -0
- package/dist/restore-BQp01WY3.js +914 -0
- package/dist/restore-BeWaJNjR.d.cts +288 -0
- package/dist/restore-CEywQUHz.cjs +919 -0
- package/dist/restore-Dh0-Nvms.d.ts +288 -0
- package/dist/uam-transaction.cjs +29 -14
- package/dist/uam-transaction.d.cts +2 -1
- package/dist/uam-transaction.d.ts +2 -1
- package/dist/uam-transaction.js +30 -16
- package/dist/web.cjs +440 -0
- package/dist/web.d.cts +44 -0
- package/dist/web.d.ts +44 -0
- package/dist/web.js +439 -0
- package/package.json +29 -4
- package/src/adapters/node/plugins.ts +82 -0
- package/src/adapters/node/publish.ts +130 -0
- package/src/adapters/node/restore.ts +187 -0
- package/src/adapters/web/publish.ts +196 -0
- package/src/adapters/web/raster.ts +421 -0
- package/src/atlas/font.ts +95 -0
- package/src/atlas/inputs.ts +445 -0
- package/src/atlas/jta.ts +157 -0
- package/src/atlas/packing.ts +762 -0
- package/src/atlas.ts +129 -1221
- package/src/codegen.ts +108 -82
- package/src/index.ts +43 -3
- package/src/node.ts +8 -0
- package/src/path-utils.ts +40 -0
- package/src/plugins/types.ts +56 -0
- package/src/publish/contracts.ts +80 -0
- package/src/publish/external-resources.ts +117 -0
- package/src/publish/options.ts +180 -0
- package/src/publish/package-context.ts +608 -0
- package/src/publish/resource-references.ts +210 -0
- package/src/publish.ts +327 -975
- package/src/restore-internals/font.ts +100 -0
- package/src/restore-internals/movie-clip.ts +104 -0
- package/src/restore-internals/output-transaction.ts +124 -0
- package/src/restore.ts +122 -311
- package/src/shared-types.ts +4 -8
- package/src/uam-transaction.ts +34 -17
- package/src/utils.ts +28 -0
- package/src/web.ts +11 -0
package/src/atlas.ts
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
type Document,
|
|
4
|
+
GearType,
|
|
5
|
+
type Package,
|
|
6
|
+
type MovieClipResource,
|
|
7
|
+
type Transform,
|
|
8
|
+
TransitionActionType,
|
|
9
|
+
} from '@openfairygui/core';
|
|
10
|
+
import type { AtlasRasterBackend } from './publish/contracts.js';
|
|
11
|
+
import {
|
|
12
|
+
isComponentResource,
|
|
13
|
+
isFontResource,
|
|
14
|
+
isImageResource,
|
|
15
|
+
isMovieClipResource,
|
|
16
|
+
isSkeletonResource,
|
|
17
|
+
} from './publish/package-context.js';
|
|
18
|
+
import { collectPackageResourceReferences } from './publish/resource-references.js';
|
|
4
19
|
import type { ExtrasMap, HasOptionalSrc, HasOptionalUrl } from './shared-types.js';
|
|
5
20
|
import { createTransform } from './utils.js';
|
|
21
|
+
import {
|
|
22
|
+
collectFontTexture,
|
|
23
|
+
collectImage,
|
|
24
|
+
collectMovieClipFrames,
|
|
25
|
+
isPackableResource,
|
|
26
|
+
resolveFontFileName,
|
|
27
|
+
type InputItem,
|
|
28
|
+
type PackageResource,
|
|
29
|
+
} from './atlas/inputs.js';
|
|
30
|
+
import { emitAtlasInputs, sortResourcesByOrder } from './atlas/packing.js';
|
|
31
|
+
import type { PreparedJtaData } from './atlas/jta.js';
|
|
6
32
|
|
|
7
33
|
export interface AtlasOptions {
|
|
8
34
|
/**
|
|
9
|
-
*
|
|
35
|
+
* Limit atlas generation to specific package names.
|
|
36
|
+
* When omitted, all packages are processed.
|
|
37
|
+
*/
|
|
38
|
+
packages?: string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Raster backend, injected by the host adapter.
|
|
10
42
|
* Required for actual image compositing and trimImage.
|
|
11
43
|
*
|
|
12
44
|
* ```ts
|
|
@@ -14,7 +46,7 @@ export interface AtlasOptions {
|
|
|
14
46
|
* await doc.transform(atlas({ encoder: sharp }));
|
|
15
47
|
* ```
|
|
16
48
|
*/
|
|
17
|
-
encoder?:
|
|
49
|
+
encoder?: AtlasRasterBackend;
|
|
18
50
|
|
|
19
51
|
/** Maximum atlas texture size (width and height). Default: 2048. */
|
|
20
52
|
maxSize?: number;
|
|
@@ -39,7 +71,7 @@ export interface AtlasOptions {
|
|
|
39
71
|
|
|
40
72
|
/**
|
|
41
73
|
* Trim transparent pixels from image edges before packing.
|
|
42
|
-
* Requires
|
|
74
|
+
* Requires a raster backend. Stores offset/originalSize in Sprite nodes.
|
|
43
75
|
* Default: false.
|
|
44
76
|
*/
|
|
45
77
|
trimImage?: boolean;
|
|
@@ -56,6 +88,14 @@ export interface AtlasOptions {
|
|
|
56
88
|
*/
|
|
57
89
|
outputPath?: string;
|
|
58
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Require a complete raster artifact when there are packable inputs.
|
|
93
|
+
* This is used by publish() so a runtime package cannot contain atlas
|
|
94
|
+
* references without the matching PNG output.
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
strictOutput?: boolean;
|
|
98
|
+
|
|
59
99
|
/**
|
|
60
100
|
* Optional mkdir function to ensure output directory exists.
|
|
61
101
|
* If not provided, the outputPath directory must already exist.
|
|
@@ -68,6 +108,12 @@ export interface AtlasOptions {
|
|
|
68
108
|
*/
|
|
69
109
|
readFileRaw?: (path: string) => Promise<Uint8Array>;
|
|
70
110
|
|
|
111
|
+
/**
|
|
112
|
+
* MovieClip parse/decode results prepared by publish() before output begins.
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
preparedMovieClips?: ReadonlyMap<MovieClipResource, PreparedJtaData>;
|
|
116
|
+
|
|
71
117
|
/**
|
|
72
118
|
* Keep original input order when MaxRects tie-break scores are equal.
|
|
73
119
|
* This is an internal publish detail used to mirror editor/CLI behavior.
|
|
@@ -92,10 +138,14 @@ export interface AtlasOptions {
|
|
|
92
138
|
* separate atlas pages/files per branch instead of mixing them with main.
|
|
93
139
|
*/
|
|
94
140
|
separatedAtlasForBranch?: boolean;
|
|
95
|
-
|
|
96
141
|
}
|
|
97
142
|
|
|
98
|
-
const ATLAS_DEFAULTS: Required<
|
|
143
|
+
const ATLAS_DEFAULTS: Required<
|
|
144
|
+
Omit<
|
|
145
|
+
AtlasOptions,
|
|
146
|
+
'packages' | 'encoder' | 'basePath' | 'outputPath' | 'mkdir' | 'readFileRaw' | 'preparedMovieClips'
|
|
147
|
+
>
|
|
148
|
+
> = {
|
|
99
149
|
maxSize: 2048,
|
|
100
150
|
fast: true,
|
|
101
151
|
allowRotation: true,
|
|
@@ -108,34 +158,9 @@ const ATLAS_DEFAULTS: Required<Omit<AtlasOptions, 'encoder' | 'basePath' | 'outp
|
|
|
108
158
|
directSingleImageOutput: false,
|
|
109
159
|
extractAlpha: false,
|
|
110
160
|
separatedAtlasForBranch: false,
|
|
161
|
+
strictOutput: false,
|
|
111
162
|
};
|
|
112
163
|
|
|
113
|
-
/** Trim info for a single image. */
|
|
114
|
-
interface TrimInfo {
|
|
115
|
-
/** Trimmed pixel data (PNG). */
|
|
116
|
-
buffer: Uint8Array;
|
|
117
|
-
/** Trimmed width. */
|
|
118
|
-
width: number;
|
|
119
|
-
/** Trimmed height. */
|
|
120
|
-
height: number;
|
|
121
|
-
/** Offset from original left edge. */
|
|
122
|
-
offsetX: number;
|
|
123
|
-
/** Offset from original top edge. */
|
|
124
|
-
offsetY: number;
|
|
125
|
-
/** Original width before trim. */
|
|
126
|
-
originalWidth: number;
|
|
127
|
-
/** Original height before trim. */
|
|
128
|
-
originalHeight: number;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
type PackageResource = ReturnType<Package['listResources']>[number];
|
|
132
|
-
type PackableResource = ImageResource | MovieClipResource | FontResource;
|
|
133
|
-
type PackInputResource = ImageResource | MovieClipResource;
|
|
134
|
-
|
|
135
|
-
function getPublishedItemId(resource: { getId(): string; getExtras(): ExtrasMap | undefined }): string {
|
|
136
|
-
return ((resource.getExtras() as ImageResourceExtras | undefined) ?? {})._publishedId ?? resource.getId();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
164
|
interface AtlasReferenceItem {
|
|
140
165
|
icon?: string | null;
|
|
141
166
|
url?: string | null;
|
|
@@ -176,80 +201,22 @@ interface ChildWithReferenceUrls extends HasOptionalSrc, HasOptionalUrl {
|
|
|
176
201
|
listGears?(): GearWithAtlasRefs[];
|
|
177
202
|
}
|
|
178
203
|
|
|
179
|
-
interface ImageResourceExtras extends ExtrasMap {
|
|
180
|
-
_fileName?: string;
|
|
181
|
-
_publishedId?: string;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
interface FontSpriteAlias {
|
|
185
|
-
fontId: string;
|
|
186
|
-
textureId: string;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
interface FontResourceExtras extends ExtrasMap {
|
|
190
|
-
_fontSpriteAlias?: FontSpriteAlias;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
204
|
interface PackageAtlasExtras extends ExtrasMap {
|
|
194
205
|
publishedResourceIds?: string[];
|
|
195
206
|
}
|
|
196
207
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
trimOffsetTop?: number;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
interface AtlasEncoderResolvedBuffer {
|
|
213
|
-
data: Uint8Array;
|
|
214
|
-
info: Required<Pick<AtlasEncoderMetadata, 'width' | 'height' | 'channels'>> & AtlasEncoderMetadata;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
interface AtlasCompositeInput {
|
|
218
|
-
input: Uint8Array;
|
|
219
|
-
left: number;
|
|
220
|
-
top: number;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
interface AtlasEncoderPipeline {
|
|
224
|
-
ensureAlpha(): AtlasEncoderPipeline;
|
|
225
|
-
raw(): AtlasEncoderPipeline;
|
|
226
|
-
extract(options: { left: number; top: number; width: number; height: number }): AtlasEncoderPipeline;
|
|
227
|
-
toBuffer(options: { resolveWithObject: true }): Promise<AtlasEncoderResolvedBuffer>;
|
|
228
|
-
toBuffer(options?: { resolveWithObject?: false }): Promise<Uint8Array>;
|
|
229
|
-
toBuffer(options?: { resolveWithObject?: boolean }): Promise<Uint8Array | AtlasEncoderResolvedBuffer>;
|
|
230
|
-
png(): AtlasEncoderPipeline;
|
|
231
|
-
metadata(): Promise<AtlasEncoderMetadata>;
|
|
232
|
-
rotate(angle: number): AtlasEncoderPipeline;
|
|
233
|
-
composite(inputs: AtlasCompositeInput[]): AtlasEncoderPipeline;
|
|
234
|
-
toFile(path: string): Promise<unknown>;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
type AtlasEncoderInput =
|
|
238
|
-
| string
|
|
239
|
-
| Uint8Array
|
|
240
|
-
| {
|
|
241
|
-
create: {
|
|
242
|
-
width: number;
|
|
243
|
-
height: number;
|
|
244
|
-
channels: 4;
|
|
245
|
-
background: { r: number; g: number; b: number; alpha: number };
|
|
246
|
-
};
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
type AtlasEncoder = (input: AtlasEncoderInput) => AtlasEncoderPipeline;
|
|
250
|
-
|
|
251
|
-
function resolveFontFileName(fontName: string): string {
|
|
252
|
-
return /\.fnt$/i.test(fontName) ? fontName : `${fontName}.fnt`;
|
|
208
|
+
function getSelectedSkeletonDependencyImageIds(resources: PackageResource[]): Set<string> {
|
|
209
|
+
const imageIds = new Set<string>();
|
|
210
|
+
const resourcesById = new Map(resources.map((resource) => [resource.getId(), resource] as const));
|
|
211
|
+
for (const resource of resources) {
|
|
212
|
+
if (!isSkeletonResource(resource)) continue;
|
|
213
|
+
for (const requiredId of resource.getRequireIds()) {
|
|
214
|
+
if (!requiredId) continue;
|
|
215
|
+
const required = resourcesById.get(requiredId);
|
|
216
|
+
if (required && isImageResource(required)) imageIds.add(requiredId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return imageIds;
|
|
253
220
|
}
|
|
254
221
|
|
|
255
222
|
async function resolveEditorCompatibleResourceOrder(
|
|
@@ -282,7 +249,9 @@ async function resolveEditorCompatibleResourceOrder(
|
|
|
282
249
|
const imgMatch = line.match(/\bimg=(\w+)/);
|
|
283
250
|
if (imgMatch) await addResource(resourceMap.get(imgMatch[1] ?? ''));
|
|
284
251
|
}
|
|
285
|
-
} catch {
|
|
252
|
+
} catch {
|
|
253
|
+
/* ignore */
|
|
254
|
+
}
|
|
286
255
|
}
|
|
287
256
|
}
|
|
288
257
|
if (isComponentResource(resource)) {
|
|
@@ -371,7 +340,9 @@ async function resolveEditorCompatibleResourceOrder(
|
|
|
371
340
|
]) {
|
|
372
341
|
await addResourceByLocalUiUrl(ref);
|
|
373
342
|
}
|
|
374
|
-
for (const transition of (
|
|
343
|
+
for (const transition of (
|
|
344
|
+
component as Component & { listTransitions?(): TransitionWithAtlasRefs[] }
|
|
345
|
+
).listTransitions?.() ?? []) {
|
|
375
346
|
for (const item of transition.listItems?.() ?? []) {
|
|
376
347
|
const actionType = item.getActionType?.();
|
|
377
348
|
if (actionType !== TransitionActionType.Sound && actionType !== TransitionActionType.Icon) continue;
|
|
@@ -400,7 +371,7 @@ async function resolveEditorCompatibleResourceOrder(
|
|
|
400
371
|
*
|
|
401
372
|
* This transform performs MaxRects bin-packing on all ImageResource items
|
|
402
373
|
* within each package, creating Atlas and Sprite property nodes. When an
|
|
403
|
-
*
|
|
374
|
+
* a raster backend is provided, it also composites the actual PNG files.
|
|
404
375
|
*
|
|
405
376
|
* When `trimImage` is enabled and encoder is available, transparent pixels
|
|
406
377
|
* at image edges are trimmed before packing. The trimmed offset and original
|
|
@@ -423,84 +394,39 @@ export function atlas(_options: AtlasOptions = {}): Transform {
|
|
|
423
394
|
return createTransform('atlas', async (doc: Document): Promise<void> => {
|
|
424
395
|
const root = doc.getRoot();
|
|
425
396
|
const logger = doc.getLogger();
|
|
426
|
-
const encoder = options.encoder
|
|
397
|
+
const encoder = options.encoder;
|
|
427
398
|
const doTrim = options.trimImage && !!encoder && !!options.basePath;
|
|
399
|
+
const packageFilter = options.packages ? new Set(options.packages) : null;
|
|
428
400
|
|
|
429
401
|
for (const pkg of root.listPackages()) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
402
|
+
if (packageFilter && !packageFilter.has(pkg.getName())) continue;
|
|
403
|
+
// Publish annotations select merged resources; only strict output treats an empty selection as explicit.
|
|
404
|
+
const publishedResourceIds = (pkg.getExtras() as PackageAtlasExtras | undefined)?.publishedResourceIds;
|
|
405
|
+
const selectedPublishIds = new Set(publishedResourceIds);
|
|
406
|
+
const hasPublishSelection =
|
|
407
|
+
publishedResourceIds !== undefined && (options.strictOutput || selectedPublishIds.size > 0);
|
|
408
|
+
const allResources =
|
|
409
|
+
hasPublishSelection
|
|
410
|
+
? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId()))
|
|
411
|
+
: pkg.listResources();
|
|
412
|
+
const skeletonDependencyImageIds = getSelectedSkeletonDependencyImageIds(allResources);
|
|
435
413
|
// Process resources in declaration order (matching editor behavior)
|
|
436
414
|
const orderedResources = await resolveEditorCompatibleResourceOrder(pkg, allResources, options);
|
|
437
415
|
const resourceOrder = new Map(orderedResources.map((resource, index) => [resource.getId(), index]));
|
|
438
416
|
const inputOrder = new Map(allResources.map((resource, index) => [resource.getId(), index]));
|
|
439
417
|
const orderedAllResources = sortResourcesByOrder(allResources, resourceOrder, inputOrder);
|
|
440
|
-
const hasPackable = allResources.some((resource) =>
|
|
418
|
+
const hasPackable = allResources.some((resource) => {
|
|
419
|
+
if (isImageResource(resource) && skeletonDependencyImageIds.has(resource.getId())) return false;
|
|
420
|
+
return isPackableResource(resource);
|
|
421
|
+
});
|
|
441
422
|
if (!hasPackable) continue;
|
|
442
423
|
|
|
443
424
|
// Collect packable items in declaration order
|
|
444
425
|
const inputs: InputItem[] = [];
|
|
445
426
|
|
|
446
427
|
// Build set of referenced resource IDs (editor only packs referenced images)
|
|
447
|
-
|
|
448
|
-
const referencedIds = new Set<string>();
|
|
449
|
-
const resourceMap = new Map<string, PackageResource>();
|
|
450
|
-
for (const res of allResources) {
|
|
451
|
-
const id = res.getId();
|
|
452
|
-
if (id) resourceMap.set(id, res);
|
|
453
|
-
}
|
|
454
|
-
function collectRefs(component: Component, visited: Set<string>): void {
|
|
455
|
-
for (const child of component.listChildren()) {
|
|
456
|
-
const refChild = child as ChildWithReferenceUrls;
|
|
457
|
-
const src = refChild.getSrc?.();
|
|
458
|
-
if (src && !visited.has(src)) {
|
|
459
|
-
referencedIds.add(src);
|
|
460
|
-
visited.add(src);
|
|
461
|
-
const srcRes = resourceMap.get(src);
|
|
462
|
-
if (srcRes && isComponentResource(srcRes)) {
|
|
463
|
-
collectRefs(srcRes, visited);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
for (const ref of [
|
|
467
|
-
refChild.getIcon?.(),
|
|
468
|
-
refChild.getSelectedIcon?.(),
|
|
469
|
-
refChild.getFont?.(),
|
|
470
|
-
refChild.getDropdown?.(),
|
|
471
|
-
refChild.getInstanceIcon?.(),
|
|
472
|
-
refChild.getInstanceSelectedIcon?.(),
|
|
473
|
-
refChild.getVtScrollBarRes?.(),
|
|
474
|
-
refChild.getHzScrollBarRes?.(),
|
|
475
|
-
refChild.getHeaderRes?.(),
|
|
476
|
-
refChild.getFooterRes?.(),
|
|
477
|
-
refChild.getUrl?.(),
|
|
478
|
-
]) {
|
|
479
|
-
addUiResourceRef(referencedIds, ref);
|
|
480
|
-
}
|
|
481
|
-
addUiResourceRefsFromText(referencedIds, refChild.getText?.());
|
|
482
|
-
for (const item of refChild.getInstanceComboItems?.() ?? []) {
|
|
483
|
-
addUiResourceRef(referencedIds, item.icon ?? undefined);
|
|
484
|
-
}
|
|
485
|
-
for (const item of refChild.getListItems?.() ?? []) {
|
|
486
|
-
addUiResourceRef(referencedIds, item.icon ?? undefined);
|
|
487
|
-
}
|
|
488
|
-
for (const gear of refChild.listGears?.() ?? []) {
|
|
489
|
-
addUiResourceRefsFromUnknown(referencedIds, gear.getValues?.());
|
|
490
|
-
addUiResourceRefsFromUnknown(referencedIds, gear.getDefaultValue?.());
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
for (const transition of (component as Component & { listTransitions?(): TransitionWithAtlasRefs[] }).listTransitions?.() ?? []) {
|
|
494
|
-
for (const item of transition.listItems?.() ?? []) {
|
|
495
|
-
addUiResourceRefsFromUnknown(referencedIds, item.getStartValue?.());
|
|
496
|
-
addUiResourceRefsFromUnknown(referencedIds, item.getEndValue?.());
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
428
|
+
const referencedIds = collectPackageResourceReferences(pkg).localResourceIds;
|
|
500
429
|
for (const res of orderedAllResources) {
|
|
501
|
-
if (isComponentResource(res)) {
|
|
502
|
-
collectRefs(res, new Set());
|
|
503
|
-
}
|
|
504
430
|
if (isSkeletonResource(res) && referencedIds.has(res.getId())) {
|
|
505
431
|
for (const requiredId of res.getRequireIds()) {
|
|
506
432
|
if (requiredId) referencedIds.add(requiredId);
|
|
@@ -522,7 +448,9 @@ export function atlas(_options: AtlasOptions = {}): Transform {
|
|
|
522
448
|
const match = line.match(/img=(\w+)/);
|
|
523
449
|
if (match) referencedIds.add(match[1]);
|
|
524
450
|
}
|
|
525
|
-
} catch {
|
|
451
|
+
} catch {
|
|
452
|
+
/* .fnt file not found — OK */
|
|
453
|
+
}
|
|
526
454
|
}
|
|
527
455
|
}
|
|
528
456
|
}
|
|
@@ -531,1065 +459,45 @@ export function atlas(_options: AtlasOptions = {}): Transform {
|
|
|
531
459
|
if (isImageResource(res)) {
|
|
532
460
|
// Pack referenced images, plus explicitly exported standalone images.
|
|
533
461
|
const resId = res.getId();
|
|
534
|
-
if (
|
|
535
|
-
|
|
462
|
+
if (skeletonDependencyImageIds.has(resId)) continue;
|
|
463
|
+
if (
|
|
464
|
+
selectedPublishIds.size === 0 &&
|
|
465
|
+
!res.getExported() &&
|
|
466
|
+
referencedIds.size > 0 &&
|
|
467
|
+
!referencedIds.has(resId)
|
|
468
|
+
)
|
|
469
|
+
continue;
|
|
470
|
+
await collectImage(res, pkg, inputs, encoder, options, doTrim, logger);
|
|
536
471
|
} else if (isMovieClipResource(res)) {
|
|
537
472
|
const resId = res.getId();
|
|
538
|
-
if (
|
|
539
|
-
|
|
473
|
+
if (
|
|
474
|
+
selectedPublishIds.size === 0 &&
|
|
475
|
+
!res.getExported() &&
|
|
476
|
+
referencedIds.size > 0 &&
|
|
477
|
+
!referencedIds.has(resId)
|
|
478
|
+
)
|
|
479
|
+
continue;
|
|
480
|
+
await collectMovieClipFrames(doc, res, pkg, inputs, encoder, options, logger);
|
|
540
481
|
} else if (isFontResource(res)) {
|
|
541
482
|
const resId = res.getId();
|
|
542
|
-
if (
|
|
543
|
-
|
|
483
|
+
if (
|
|
484
|
+
selectedPublishIds.size === 0 &&
|
|
485
|
+
!res.getExported() &&
|
|
486
|
+
referencedIds.size > 0 &&
|
|
487
|
+
!referencedIds.has(resId)
|
|
488
|
+
)
|
|
489
|
+
continue;
|
|
490
|
+
await collectFontTexture(doc, res, pkg, options);
|
|
544
491
|
}
|
|
545
492
|
}
|
|
546
493
|
|
|
547
494
|
if (inputs.length === 0) continue;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
for (const group of branchGroups) {
|
|
553
|
-
const directOutput = resolveDirectImageOutput(group.inputs, options);
|
|
554
|
-
if (directOutput) {
|
|
555
|
-
await emitDirectImageOutput(doc, pkg, directOutput, encoder, options, logger, group.branchName, group.branchOrdinal);
|
|
556
|
-
usedDirectOutput = true;
|
|
557
|
-
totalPageCount += 1;
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const hasDuplicatePadding = group.inputs.some((i) => {
|
|
562
|
-
return isImageResource(i.resource) && i.resource.getDuplicatePadding?.() === true;
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
const packer = new MaxRectsPackerCompat({
|
|
566
|
-
pot: options.powerOfTwo,
|
|
567
|
-
mof: !options.powerOfTwo,
|
|
568
|
-
padding: options.padding,
|
|
569
|
-
rotation: options.allowRotation,
|
|
570
|
-
minWidth: 16,
|
|
571
|
-
minHeight: 16,
|
|
572
|
-
maxWidth: options.maxSize,
|
|
573
|
-
maxHeight: options.maxSize,
|
|
574
|
-
square: options.square,
|
|
575
|
-
fast: options.fast,
|
|
576
|
-
edgePadding: false,
|
|
577
|
-
duplicatePadding: hasDuplicatePadding,
|
|
578
|
-
multiPage: options.multiPage,
|
|
579
|
-
preserveInputOrderOnTie: options.preserveInputOrderOnTie,
|
|
580
|
-
});
|
|
581
|
-
const pages = packer.pack(group.inputs.map((input, index) => inputToCompatRect(input, index)));
|
|
582
|
-
if (!pages || pages.length === 0) continue;
|
|
583
|
-
totalPageCount += pages.length;
|
|
584
|
-
|
|
585
|
-
for (let p = 0; p < pages.length; p++) {
|
|
586
|
-
const page = pages[p];
|
|
587
|
-
const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(group.branchOrdinal, p)}`);
|
|
588
|
-
atlasNode.setIndex(resolveAtlasIndex(group.branchOrdinal, p));
|
|
589
|
-
atlasNode.setFile(resolveAtlasOutputFileName(pkg, p, group.branchName));
|
|
590
|
-
atlasNode.setWidth(page.width);
|
|
591
|
-
atlasNode.setHeight(page.height);
|
|
592
|
-
pkg.addAtlas(atlasNode);
|
|
593
|
-
|
|
594
|
-
for (const pr of page.outputRects) {
|
|
595
|
-
const input = group.inputs[pr.index];
|
|
596
|
-
if (!input) continue;
|
|
597
|
-
const packedSize = resolvePackedRectSize(input, pr.width, pr.height, pr.rotated);
|
|
598
|
-
const rotated = pr.rotated;
|
|
599
|
-
const sprite = doc.createSprite();
|
|
600
|
-
sprite.setItemId(input.id);
|
|
601
|
-
sprite.setRectX(pr.x);
|
|
602
|
-
sprite.setRectY(pr.y);
|
|
603
|
-
sprite.setRectWidth(packedSize.width);
|
|
604
|
-
sprite.setRectHeight(packedSize.height);
|
|
605
|
-
sprite.setRotated(rotated);
|
|
606
|
-
sprite.setOffsetX(input.offsetX);
|
|
607
|
-
sprite.setOffsetY(input.offsetY);
|
|
608
|
-
sprite.setOriginalWidth(input.originalWidth);
|
|
609
|
-
sprite.setOriginalHeight(input.originalHeight);
|
|
610
|
-
sprite.setAtlas(atlasNode);
|
|
611
|
-
atlasNode.addSprite(sprite);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
for (const res of allResources) {
|
|
615
|
-
if (!isFontResource(res)) continue;
|
|
616
|
-
const fextras = res.getExtras() as FontResourceExtras;
|
|
617
|
-
const alias = fextras?._fontSpriteAlias;
|
|
618
|
-
if (!alias) continue;
|
|
619
|
-
const imgSprite = page.outputRects.find((result) => group.inputs[result.index]?.id === alias.textureId);
|
|
620
|
-
if (!imgSprite) continue;
|
|
621
|
-
const imgInput = group.inputs[imgSprite.index];
|
|
622
|
-
const fontSprite = doc.createSprite();
|
|
623
|
-
fontSprite.setItemId(alias.fontId);
|
|
624
|
-
fontSprite.setRectX(imgSprite.x);
|
|
625
|
-
fontSprite.setRectY(imgSprite.y);
|
|
626
|
-
fontSprite.setRectWidth(imgSprite.width);
|
|
627
|
-
fontSprite.setRectHeight(imgSprite.height);
|
|
628
|
-
fontSprite.setRotated(imgSprite.rotated);
|
|
629
|
-
if (imgInput) {
|
|
630
|
-
fontSprite.setOffsetX(imgInput.offsetX);
|
|
631
|
-
fontSprite.setOffsetY(imgInput.offsetY);
|
|
632
|
-
fontSprite.setOriginalWidth(imgInput.originalWidth);
|
|
633
|
-
fontSprite.setOriginalHeight(imgInput.originalHeight);
|
|
634
|
-
}
|
|
635
|
-
fontSprite.setAtlas(atlasNode);
|
|
636
|
-
atlasNode.addSprite(fontSprite);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (encoder && options.outputPath) {
|
|
641
|
-
if (options.mkdir) {
|
|
642
|
-
await options.mkdir(options.outputPath);
|
|
643
|
-
}
|
|
644
|
-
for (let p = 0; p < pages.length; p++) {
|
|
645
|
-
const page = pages[p];
|
|
646
|
-
const compositeInputs: Array<{ input: Uint8Array; left: number; top: number }> = [];
|
|
647
|
-
|
|
648
|
-
for (const pr of page.outputRects) {
|
|
649
|
-
const input = group.inputs[pr.index];
|
|
650
|
-
if (!input) continue;
|
|
651
|
-
if (pr.width <= 0 || pr.height <= 0 || input.width <= 0 || input.height <= 0) continue;
|
|
652
|
-
try {
|
|
653
|
-
let imgBuffer: Uint8Array;
|
|
654
|
-
|
|
655
|
-
if (input.trimBuffer) {
|
|
656
|
-
imgBuffer = input.trimBuffer;
|
|
657
|
-
if (imgBuffer.length === 0) continue;
|
|
658
|
-
} else {
|
|
659
|
-
if (!isImageResource(input.resource)) {
|
|
660
|
-
logger.warn(`atlas: Non-image input "${input.id}" is missing inline buffer, skipping compositing.`);
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
const filePath = _resolveImagePath(input.resource, pkg, options.basePath!);
|
|
664
|
-
imgBuffer = await encoder(filePath).toBuffer();
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (pr.rotated) imgBuffer = await encoder(imgBuffer).rotate(270).toBuffer();
|
|
668
|
-
|
|
669
|
-
compositeInputs.push({
|
|
670
|
-
input: imgBuffer,
|
|
671
|
-
left: pr.x,
|
|
672
|
-
top: pr.y,
|
|
673
|
-
});
|
|
674
|
-
} catch {
|
|
675
|
-
logger.warn(`atlas: Could not read image "${input.id}" for compositing.`);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const atlasFileName = resolveAtlasOutputFileName(pkg, p, group.branchName);
|
|
680
|
-
const outputFile = `${options.outputPath}/${atlasFileName}`;
|
|
681
|
-
|
|
682
|
-
await encoder({
|
|
683
|
-
create: {
|
|
684
|
-
width: page.width,
|
|
685
|
-
height: page.height,
|
|
686
|
-
channels: 4 as const,
|
|
687
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
688
|
-
},
|
|
689
|
-
})
|
|
690
|
-
.composite(compositeInputs)
|
|
691
|
-
.png()
|
|
692
|
-
.toFile(outputFile);
|
|
693
|
-
|
|
694
|
-
logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
495
|
+
if (options.strictOutput && (!encoder || !options.basePath || !options.outputPath)) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`atlas: Package "${pkg.getName()}" requires encoder, basePath, and outputPath for complete raster output.`,
|
|
498
|
+
);
|
|
697
499
|
}
|
|
698
|
-
|
|
699
|
-
if (usedDirectOutput) {
|
|
700
|
-
logger.info(`atlas: Direct output for single image package "${pkg.getName()}".`);
|
|
701
|
-
}
|
|
702
|
-
logger.info(`atlas: Packed ${inputs.length} images into ${totalPageCount} atlas(es) for package "${pkg.getName()}".`);
|
|
500
|
+
await emitAtlasInputs({ doc, pkg, allResources, inputs, options, encoder, logger });
|
|
703
501
|
}
|
|
704
502
|
});
|
|
705
503
|
}
|
|
706
|
-
|
|
707
|
-
function buildBranchAtlasGroups(doc: Document, inputs: InputItem[], options: AtlasOptions): BranchAtlasGroup[] {
|
|
708
|
-
if (!options.separatedAtlasForBranch) {
|
|
709
|
-
return [{ branchName: '', branchOrdinal: 0, inputs }];
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const discoveredBranchNames = [...new Set(inputs
|
|
713
|
-
.map((input) => getInputBranchName(input))
|
|
714
|
-
.filter((branchName) => !!branchName))];
|
|
715
|
-
if (discoveredBranchNames.length === 0) {
|
|
716
|
-
return [{ branchName: '', branchOrdinal: 0, inputs }];
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const orderedBranchNames = doc.getRoot().listBranches().filter((branchName) => discoveredBranchNames.includes(branchName));
|
|
720
|
-
for (const branchName of discoveredBranchNames) {
|
|
721
|
-
if (!orderedBranchNames.includes(branchName)) orderedBranchNames.push(branchName);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const groups = new Map<string, InputItem[]>();
|
|
725
|
-
groups.set('', []);
|
|
726
|
-
for (const branchName of orderedBranchNames) {
|
|
727
|
-
groups.set(branchName, []);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
for (const input of inputs) {
|
|
731
|
-
const branchName = getInputBranchName(input);
|
|
732
|
-
const key = groups.has(branchName) ? branchName : '';
|
|
733
|
-
groups.get(key)!.push(input);
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
const orderedKeys = [''];
|
|
737
|
-
for (const branchName of orderedBranchNames) {
|
|
738
|
-
if ((groups.get(branchName)?.length ?? 0) > 0) orderedKeys.push(branchName);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return orderedKeys
|
|
742
|
-
.filter((branchName) => (groups.get(branchName)?.length ?? 0) > 0)
|
|
743
|
-
.map((branchName, index) => ({
|
|
744
|
-
branchName,
|
|
745
|
-
branchOrdinal: index,
|
|
746
|
-
inputs: groups.get(branchName) ?? [],
|
|
747
|
-
}));
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function inputToCompatRect(input: InputItem, index: number): CompatNodeRect {
|
|
751
|
-
const duplicatePadding = isImageResource(input.resource) && input.resource.getDuplicatePadding?.() === true;
|
|
752
|
-
return {
|
|
753
|
-
x: 0,
|
|
754
|
-
y: 0,
|
|
755
|
-
width: input.width,
|
|
756
|
-
height: input.height,
|
|
757
|
-
rotated: false,
|
|
758
|
-
index,
|
|
759
|
-
subIndex: -1,
|
|
760
|
-
flags: duplicatePadding ? COMPAT_NODE_RECT_FLAGS.DUPLICATE_PADDING : 0,
|
|
761
|
-
score1: 0,
|
|
762
|
-
score2: 0,
|
|
763
|
-
sourceKind: input.sourceKind,
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function resolvePackedRectSize(input: InputItem, width: number, height: number, rectRotated: boolean): { width: number; height: number } {
|
|
768
|
-
if (!rectRotated) return { width, height };
|
|
769
|
-
return {
|
|
770
|
-
width: input.height,
|
|
771
|
-
height: input.width,
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function resolveDirectImageOutput(inputs: InputItem[], options: AtlasOptions): InputItem | null {
|
|
776
|
-
if (!options.directSingleImageOutput || options.extractAlpha) return null;
|
|
777
|
-
if (inputs.length !== 1) return null;
|
|
778
|
-
const [input] = inputs;
|
|
779
|
-
if (!input || input.sourceKind !== 'image' || !isImageResource(input.resource)) return null;
|
|
780
|
-
if (input.resource.getDuplicatePadding?.() === true) return null;
|
|
781
|
-
if (input.width !== input.originalWidth || input.height !== input.originalHeight) return null;
|
|
782
|
-
const fileName = resolveImageFileName(input.resource).toLowerCase();
|
|
783
|
-
if (!fileName.endsWith('.png')) return null;
|
|
784
|
-
return input;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function resolveDirectOutputAtlasSize(width: number, height: number, options: AtlasOptions): { width: number; height: number } {
|
|
788
|
-
let resolvedWidth = width;
|
|
789
|
-
let resolvedHeight = height;
|
|
790
|
-
if (options.square) {
|
|
791
|
-
const side = Math.max(resolvedWidth, resolvedHeight);
|
|
792
|
-
resolvedWidth = side;
|
|
793
|
-
resolvedHeight = side;
|
|
794
|
-
}
|
|
795
|
-
if (options.powerOfTwo) {
|
|
796
|
-
resolvedWidth = nextPow2(resolvedWidth);
|
|
797
|
-
resolvedHeight = nextPow2(resolvedHeight);
|
|
798
|
-
}
|
|
799
|
-
return { width: resolvedWidth, height: resolvedHeight };
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
async function emitDirectImageOutput(
|
|
803
|
-
doc: Document,
|
|
804
|
-
pkg: Package,
|
|
805
|
-
input: InputItem,
|
|
806
|
-
encoder: AtlasEncoder | undefined,
|
|
807
|
-
options: AtlasOptions,
|
|
808
|
-
logger: ILogger,
|
|
809
|
-
branchName: string = '',
|
|
810
|
-
branchOrdinal: number = 0,
|
|
811
|
-
): Promise<void> {
|
|
812
|
-
const atlasFileName = resolveAtlasOutputFileName(pkg, 0, branchName);
|
|
813
|
-
const atlasSize = resolveDirectOutputAtlasSize(input.originalWidth, input.originalHeight, options);
|
|
814
|
-
const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(branchOrdinal, 0)}`);
|
|
815
|
-
atlasNode.setIndex(resolveAtlasIndex(branchOrdinal, 0));
|
|
816
|
-
atlasNode.setFile(atlasFileName);
|
|
817
|
-
atlasNode.setWidth(atlasSize.width);
|
|
818
|
-
atlasNode.setHeight(atlasSize.height);
|
|
819
|
-
pkg.addAtlas(atlasNode);
|
|
820
|
-
|
|
821
|
-
const sprite = doc.createSprite();
|
|
822
|
-
sprite.setItemId(input.id);
|
|
823
|
-
sprite.setRectX(0);
|
|
824
|
-
sprite.setRectY(0);
|
|
825
|
-
sprite.setRectWidth(input.originalWidth);
|
|
826
|
-
sprite.setRectHeight(input.originalHeight);
|
|
827
|
-
sprite.setRotated(false);
|
|
828
|
-
sprite.setOffsetX(0);
|
|
829
|
-
sprite.setOffsetY(0);
|
|
830
|
-
sprite.setOriginalWidth(input.originalWidth);
|
|
831
|
-
sprite.setOriginalHeight(input.originalHeight);
|
|
832
|
-
sprite.setAtlas(atlasNode);
|
|
833
|
-
atlasNode.addSprite(sprite);
|
|
834
|
-
|
|
835
|
-
if (!encoder || !options.outputPath || !isImageResource(input.resource) || !options.basePath) return;
|
|
836
|
-
if (options.mkdir) {
|
|
837
|
-
await options.mkdir(options.outputPath);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const outputFile = `${options.outputPath}/${atlasFileName}`;
|
|
841
|
-
const filePath = _resolveImagePath(input.resource, pkg, options.basePath);
|
|
842
|
-
|
|
843
|
-
try {
|
|
844
|
-
if (atlasSize.width === input.originalWidth && atlasSize.height === input.originalHeight) {
|
|
845
|
-
await encoder(filePath).png().toFile(outputFile);
|
|
846
|
-
} else {
|
|
847
|
-
const imageBuffer = await encoder(filePath).png().toBuffer();
|
|
848
|
-
await encoder({
|
|
849
|
-
create: {
|
|
850
|
-
width: atlasSize.width,
|
|
851
|
-
height: atlasSize.height,
|
|
852
|
-
channels: 4 as const,
|
|
853
|
-
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
854
|
-
},
|
|
855
|
-
})
|
|
856
|
-
.composite([{ input: imageBuffer, left: 0, top: 0 }])
|
|
857
|
-
.png()
|
|
858
|
-
.toFile(outputFile);
|
|
859
|
-
}
|
|
860
|
-
} catch {
|
|
861
|
-
logger.warn(`atlas: Could not write direct-output atlas "${atlasFileName}".`);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function getInputBranchName(input: InputItem): string {
|
|
866
|
-
return (input.resource as { getBranch?(): string }).getBranch?.() ?? '';
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function resolveAtlasIndex(branchOrdinal: number, pageIndex: number): number {
|
|
870
|
-
if (branchOrdinal <= 0) return pageIndex;
|
|
871
|
-
return branchOrdinal * 100 + pageIndex;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function resolveAtlasOutputFileName(pkg: Package, pageIndex: number, branchName: string): string {
|
|
875
|
-
const suffix = branchName ? `_${branchName}` : '';
|
|
876
|
-
return `${pkg.getPublishName() || pkg.getName()}_atlas${pageIndex}${suffix}.png`;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function resolveImageFileName(resource: ImageResource): string {
|
|
880
|
-
const extras = resource.getExtras() as ImageResourceExtras;
|
|
881
|
-
return resource.getFileName() || extras._fileName || resource.getName();
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function nextPow2(value: number): number {
|
|
885
|
-
if (value <= 1) return 1;
|
|
886
|
-
return 2 ** Math.ceil(Math.log2(value));
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function sortResourcesByOrder(
|
|
890
|
-
resources: PackageResource[],
|
|
891
|
-
orderMap: Map<string, number>,
|
|
892
|
-
inputOrderMap: Map<string, number>,
|
|
893
|
-
): PackageResource[] {
|
|
894
|
-
const ordered = [...resources];
|
|
895
|
-
ordered.sort((left, right) => {
|
|
896
|
-
const leftId = left.getId();
|
|
897
|
-
const rightId = right.getId();
|
|
898
|
-
const leftOrder = leftId && orderMap.has(leftId) ? (orderMap.get(leftId) ?? Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER;
|
|
899
|
-
const rightOrder = rightId && orderMap.has(rightId) ? (orderMap.get(rightId) ?? Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER;
|
|
900
|
-
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
901
|
-
const leftInputOrder = leftId && inputOrderMap.has(leftId) ? (inputOrderMap.get(leftId) ?? Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER;
|
|
902
|
-
const rightInputOrder = rightId && inputOrderMap.has(rightId) ? (inputOrderMap.get(rightId) ?? Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER;
|
|
903
|
-
if (leftInputOrder !== rightInputOrder) return leftInputOrder - rightInputOrder;
|
|
904
|
-
return (leftId ?? '').localeCompare(rightId ?? '');
|
|
905
|
-
});
|
|
906
|
-
return ordered;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
interface ExtractedJtaFrameMeta {
|
|
910
|
-
addDelay: number;
|
|
911
|
-
offsetX: number;
|
|
912
|
-
offsetY: number;
|
|
913
|
-
width: number;
|
|
914
|
-
height: number;
|
|
915
|
-
textureIndex: number;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
interface ExtractedJtaMeta {
|
|
919
|
-
interval: number;
|
|
920
|
-
repeatDelay: number;
|
|
921
|
-
swing: boolean;
|
|
922
|
-
width: number;
|
|
923
|
-
height: number;
|
|
924
|
-
frames: ExtractedJtaFrameMeta[];
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
interface ExtractedJtaData {
|
|
928
|
-
frames: Uint8Array[];
|
|
929
|
-
meta?: ExtractedJtaMeta;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
/**
|
|
933
|
-
* Trim transparent edges from an image using sharp.
|
|
934
|
-
* Returns the trimmed buffer, dimensions, and offsets.
|
|
935
|
-
* Falls back to the original image if trim fails (e.g. no alpha channel, no transparent edges).
|
|
936
|
-
*/
|
|
937
|
-
async function _trimImage(
|
|
938
|
-
encoder: AtlasEncoder,
|
|
939
|
-
filePath: string,
|
|
940
|
-
originalWidth: number,
|
|
941
|
-
originalHeight: number,
|
|
942
|
-
): Promise<TrimInfo> {
|
|
943
|
-
try {
|
|
944
|
-
const trimResult = await encoder(filePath)
|
|
945
|
-
.ensureAlpha()
|
|
946
|
-
.raw()
|
|
947
|
-
.toBuffer({ resolveWithObject: true });
|
|
948
|
-
if (!isResolvedBuffer(trimResult)) {
|
|
949
|
-
throw new Error('atlas: encoder raw alpha trim did not return resolved metadata.');
|
|
950
|
-
}
|
|
951
|
-
const { data, info } = trimResult;
|
|
952
|
-
const width = info.width;
|
|
953
|
-
const height = info.height;
|
|
954
|
-
const channels = info.channels || 4;
|
|
955
|
-
let minX = width;
|
|
956
|
-
let minY = height;
|
|
957
|
-
let maxX = -1;
|
|
958
|
-
let maxY = -1;
|
|
959
|
-
|
|
960
|
-
for (let y = 0; y < height; y += 1) {
|
|
961
|
-
for (let x = 0; x < width; x += 1) {
|
|
962
|
-
const alphaIndex = (y * width + x) * channels + 3;
|
|
963
|
-
if ((data[alphaIndex] ?? 0) === 0) continue;
|
|
964
|
-
if (x < minX) minX = x;
|
|
965
|
-
if (y < minY) minY = y;
|
|
966
|
-
if (x > maxX) maxX = x;
|
|
967
|
-
if (y > maxY) maxY = y;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (maxX < minX || maxY < minY) {
|
|
972
|
-
return {
|
|
973
|
-
buffer: new Uint8Array(0),
|
|
974
|
-
width: 0,
|
|
975
|
-
height: 0,
|
|
976
|
-
offsetX: 0,
|
|
977
|
-
offsetY: 0,
|
|
978
|
-
originalWidth,
|
|
979
|
-
originalHeight,
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
const trimmedWidth = maxX - minX + 1;
|
|
984
|
-
const trimmedHeight = maxY - minY + 1;
|
|
985
|
-
const buffer = await encoder(filePath)
|
|
986
|
-
.extract({
|
|
987
|
-
left: minX,
|
|
988
|
-
top: minY,
|
|
989
|
-
width: trimmedWidth,
|
|
990
|
-
height: trimmedHeight,
|
|
991
|
-
})
|
|
992
|
-
.toBuffer();
|
|
993
|
-
|
|
994
|
-
return {
|
|
995
|
-
buffer,
|
|
996
|
-
width: trimmedWidth,
|
|
997
|
-
height: trimmedHeight,
|
|
998
|
-
offsetX: minX,
|
|
999
|
-
offsetY: minY,
|
|
1000
|
-
originalWidth,
|
|
1001
|
-
originalHeight,
|
|
1002
|
-
};
|
|
1003
|
-
} catch {
|
|
1004
|
-
// Trim failed (e.g. JPEG without alpha, nothing to trim) — return original
|
|
1005
|
-
const buf = await encoder(filePath).png().toBuffer();
|
|
1006
|
-
return {
|
|
1007
|
-
buffer: buf,
|
|
1008
|
-
width: originalWidth,
|
|
1009
|
-
height: originalHeight,
|
|
1010
|
-
offsetX: 0,
|
|
1011
|
-
offsetY: 0,
|
|
1012
|
-
originalWidth,
|
|
1013
|
-
originalHeight,
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/**
|
|
1019
|
-
* Resolve an ImageResource to its actual file path on disk.
|
|
1020
|
-
*/
|
|
1021
|
-
function _resolveImagePath(resource: ImageResource, pkg: Package, basePath: string): string {
|
|
1022
|
-
const imgPath = resource.getPath() ?? '/';
|
|
1023
|
-
const fileName = resolveImageFileName(resource);
|
|
1024
|
-
const branchName = resource.getBranch?.() ?? '';
|
|
1025
|
-
const normalizedBasePath = basePath.replace(/[/\\]+$/, '');
|
|
1026
|
-
const packageBasePath = !branchName
|
|
1027
|
-
? normalizedBasePath
|
|
1028
|
-
: /[\\/]assets$/i.test(normalizedBasePath)
|
|
1029
|
-
? normalizedBasePath.replace(/([\\/])assets$/i, `$1assets_${branchName}`)
|
|
1030
|
-
: `${normalizedBasePath}_${branchName}`;
|
|
1031
|
-
return `${packageBasePath}/${pkg.getName()}${imgPath}${fileName}`;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
type InputItem = {
|
|
1035
|
-
id: string; width: number; height: number;
|
|
1036
|
-
originalWidth: number; originalHeight: number;
|
|
1037
|
-
offsetX: number; offsetY: number;
|
|
1038
|
-
resource: PackInputResource; trimBuffer?: Uint8Array;
|
|
1039
|
-
sourceKind: 'image' | 'movieclip-frame';
|
|
1040
|
-
};
|
|
1041
|
-
|
|
1042
|
-
/** Collect a single ImageResource into the inputs array. */
|
|
1043
|
-
async function _collectImage(
|
|
1044
|
-
resource: ImageResource,
|
|
1045
|
-
pkg: Package,
|
|
1046
|
-
inputs: InputItem[],
|
|
1047
|
-
encoder: AtlasEncoder | undefined,
|
|
1048
|
-
options: AtlasOptions,
|
|
1049
|
-
doTrim: boolean,
|
|
1050
|
-
logger: ILogger,
|
|
1051
|
-
): Promise<void> {
|
|
1052
|
-
let origW = resource.getWidth() ?? 0;
|
|
1053
|
-
let origH = resource.getHeight() ?? 0;
|
|
1054
|
-
let sourceHasAlpha = false;
|
|
1055
|
-
|
|
1056
|
-
if (encoder && options.basePath) {
|
|
1057
|
-
const filePath = _resolveImagePath(resource, pkg, options.basePath);
|
|
1058
|
-
try {
|
|
1059
|
-
const metadata = await encoder(filePath).metadata();
|
|
1060
|
-
if (origW === 0 || origH === 0) {
|
|
1061
|
-
origW = metadata.width ?? 0;
|
|
1062
|
-
origH = metadata.height ?? 0;
|
|
1063
|
-
resource.setWidth(origW);
|
|
1064
|
-
resource.setHeight(origH);
|
|
1065
|
-
}
|
|
1066
|
-
sourceHasAlpha = metadata.hasAlpha === true || metadata.channels === 4;
|
|
1067
|
-
} catch {
|
|
1068
|
-
if (origW === 0 || origH === 0) {
|
|
1069
|
-
logger.warn(`atlas: Could not read image "${filePath}", skipping.`);
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if (origW <= 0 || origH <= 0) return;
|
|
1076
|
-
|
|
1077
|
-
let packW = origW, packH = origH, offX = 0, offY = 0;
|
|
1078
|
-
let trimBuf: Uint8Array | undefined;
|
|
1079
|
-
|
|
1080
|
-
if (doTrim && sourceHasAlpha && options.basePath && encoder) {
|
|
1081
|
-
const filePath = _resolveImagePath(resource, pkg, options.basePath);
|
|
1082
|
-
try {
|
|
1083
|
-
const trimResult = await _trimImage(encoder, filePath, origW, origH);
|
|
1084
|
-
packW = trimResult.width;
|
|
1085
|
-
packH = trimResult.height;
|
|
1086
|
-
offX = trimResult.offsetX;
|
|
1087
|
-
offY = trimResult.offsetY;
|
|
1088
|
-
trimBuf = trimResult.buffer;
|
|
1089
|
-
} catch {
|
|
1090
|
-
logger.warn(`atlas: Could not trim "${filePath}", using original.`);
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
inputs.push({
|
|
1095
|
-
id: getPublishedItemId(resource), width: packW, height: packH,
|
|
1096
|
-
originalWidth: origW, originalHeight: origH,
|
|
1097
|
-
offsetX: offX, offsetY: offY,
|
|
1098
|
-
resource,
|
|
1099
|
-
trimBuffer: trimBuf,
|
|
1100
|
-
sourceKind: 'image',
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
/** Collect MovieClip frame textures from a .jta file into the inputs array. */
|
|
1105
|
-
async function _collectMovieClipFrames(
|
|
1106
|
-
doc: Document,
|
|
1107
|
-
resource: MovieClipResource,
|
|
1108
|
-
pkg: Package,
|
|
1109
|
-
inputs: InputItem[],
|
|
1110
|
-
encoder: AtlasEncoder | undefined,
|
|
1111
|
-
options: AtlasOptions,
|
|
1112
|
-
logger: ILogger,
|
|
1113
|
-
): Promise<void> {
|
|
1114
|
-
if (!options.basePath || !options.readFileRaw) return;
|
|
1115
|
-
|
|
1116
|
-
const mcId = resource.getId();
|
|
1117
|
-
const mcName = resource.getName() + '.jta';
|
|
1118
|
-
const mcPath = resource.getPath() ?? '/';
|
|
1119
|
-
const filePath = `${options.basePath}/${pkg.getName()}${mcPath}${mcName}`;
|
|
1120
|
-
|
|
1121
|
-
try {
|
|
1122
|
-
const raw = await options.readFileRaw(filePath);
|
|
1123
|
-
const jta = _extractJtaFrames(raw);
|
|
1124
|
-
if (jta.frames.length === 0) return;
|
|
1125
|
-
|
|
1126
|
-
const frameMetas = jta.meta?.frames ?? [];
|
|
1127
|
-
for (const frame of resource.listFrames()) {
|
|
1128
|
-
resource.removeFrame(frame);
|
|
1129
|
-
}
|
|
1130
|
-
resource
|
|
1131
|
-
.setInterval(jta.meta?.interval ?? 100)
|
|
1132
|
-
.setSwing(jta.meta?.swing ?? false)
|
|
1133
|
-
.setRepeatDelay(jta.meta?.repeatDelay ?? 0);
|
|
1134
|
-
|
|
1135
|
-
if (frameMetas.length > 0) {
|
|
1136
|
-
const firstFrameIndexByTextureIndex = new Map<number, number>();
|
|
1137
|
-
for (let frameIndex = 0; frameIndex < frameMetas.length; frameIndex += 1) {
|
|
1138
|
-
const meta = frameMetas[frameIndex];
|
|
1139
|
-
const textureIndex = Number.isFinite(meta.textureIndex) ? meta.textureIndex : frameIndex;
|
|
1140
|
-
if (!firstFrameIndexByTextureIndex.has(textureIndex)) {
|
|
1141
|
-
firstFrameIndexByTextureIndex.set(textureIndex, frameIndex);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const spriteIdByTextureIndex = new Map<number, string>();
|
|
1146
|
-
for (let textureIndex = 0; textureIndex < jta.frames.length; textureIndex += 1) {
|
|
1147
|
-
const exportFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
|
|
1148
|
-
if (exportFrameIndex === undefined) continue;
|
|
1149
|
-
const itemId = `${mcId}_${exportFrameIndex}`;
|
|
1150
|
-
const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder);
|
|
1151
|
-
if (!input) continue;
|
|
1152
|
-
inputs.push(input);
|
|
1153
|
-
spriteIdByTextureIndex.set(textureIndex, itemId);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
for (let frameIndex = 0; frameIndex < frameMetas.length; frameIndex += 1) {
|
|
1157
|
-
const meta = frameMetas[frameIndex];
|
|
1158
|
-
const textureIndex = Number.isFinite(meta.textureIndex) ? meta.textureIndex : frameIndex;
|
|
1159
|
-
const frame = doc.createMovieFrame(`${mcId}_${frameIndex}`);
|
|
1160
|
-
frame
|
|
1161
|
-
.setRectX(meta.offsetX)
|
|
1162
|
-
.setRectY(meta.offsetY)
|
|
1163
|
-
.setRectWidth(meta.width)
|
|
1164
|
-
.setRectHeight(meta.height)
|
|
1165
|
-
.setAddDelay(meta.addDelay)
|
|
1166
|
-
.setSpriteId(spriteIdByTextureIndex.get(textureIndex) ?? '');
|
|
1167
|
-
resource.addFrame(frame);
|
|
1168
|
-
}
|
|
1169
|
-
} else {
|
|
1170
|
-
for (let frameIndex = 0; frameIndex < jta.frames.length; frameIndex += 1) {
|
|
1171
|
-
const itemId = `${mcId}_${frameIndex}`;
|
|
1172
|
-
const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder);
|
|
1173
|
-
if (!input) continue;
|
|
1174
|
-
inputs.push(input);
|
|
1175
|
-
const frame = doc.createMovieFrame(itemId);
|
|
1176
|
-
frame
|
|
1177
|
-
.setRectX(0)
|
|
1178
|
-
.setRectY(0)
|
|
1179
|
-
.setRectWidth(input.originalWidth)
|
|
1180
|
-
.setRectHeight(input.originalHeight)
|
|
1181
|
-
.setAddDelay(0)
|
|
1182
|
-
.setSpriteId(itemId);
|
|
1183
|
-
resource.addFrame(frame);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
if ((jta.meta?.width ?? 0) > 0 && (jta.meta?.height ?? 0) > 0) {
|
|
1188
|
-
resource.setWidth(jta.meta?.width ?? 0);
|
|
1189
|
-
resource.setHeight(jta.meta?.height ?? 0);
|
|
1190
|
-
}
|
|
1191
|
-
} catch {
|
|
1192
|
-
logger.warn(`atlas: Could not parse MovieClip "${filePath}", skipping frames.`);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
async function _createMovieClipFrameInput(
|
|
1197
|
-
buffer: Uint8Array,
|
|
1198
|
-
itemId: string,
|
|
1199
|
-
resource: MovieClipResource,
|
|
1200
|
-
encoder: AtlasEncoder | undefined,
|
|
1201
|
-
): Promise<InputItem | null> {
|
|
1202
|
-
if (!encoder || buffer.length === 0) return null;
|
|
1203
|
-
try {
|
|
1204
|
-
const meta = await encoder(buffer).metadata();
|
|
1205
|
-
const width = meta.width ?? 0;
|
|
1206
|
-
const height = meta.height ?? 0;
|
|
1207
|
-
if (width <= 0 || height <= 0) return null;
|
|
1208
|
-
return {
|
|
1209
|
-
id: itemId,
|
|
1210
|
-
width,
|
|
1211
|
-
height,
|
|
1212
|
-
originalWidth: width,
|
|
1213
|
-
originalHeight: height,
|
|
1214
|
-
offsetX: 0,
|
|
1215
|
-
offsetY: 0,
|
|
1216
|
-
resource,
|
|
1217
|
-
trimBuffer: buffer,
|
|
1218
|
-
sourceKind: 'movieclip-frame',
|
|
1219
|
-
};
|
|
1220
|
-
} catch {
|
|
1221
|
-
return null;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
1226
|
-
|
|
1227
|
-
function _extractJtaFrames(data: Uint8Array): ExtractedJtaData {
|
|
1228
|
-
const frames: Uint8Array[] = [];
|
|
1229
|
-
let offset = 0;
|
|
1230
|
-
let firstPngOffset = -1;
|
|
1231
|
-
|
|
1232
|
-
while (offset < data.length) {
|
|
1233
|
-
const sigIndex = _findPngSignature(data, offset);
|
|
1234
|
-
if (sigIndex === -1) break;
|
|
1235
|
-
if (firstPngOffset === -1) firstPngOffset = sigIndex;
|
|
1236
|
-
const end = _findPngEnd(data, sigIndex);
|
|
1237
|
-
if (end === -1) break;
|
|
1238
|
-
frames.push(data.subarray(sigIndex, end));
|
|
1239
|
-
offset = end;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
if (firstPngOffset === -1 || frames.length === 0) {
|
|
1243
|
-
return { frames: [] };
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
return {
|
|
1247
|
-
frames,
|
|
1248
|
-
meta: _parseJtaHeader(data, firstPngOffset, frames.length),
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function _findPngSignature(data: Uint8Array, fromIndex: number): number {
|
|
1253
|
-
for (let index = fromIndex; index <= data.length - PNG_SIGNATURE.length; index += 1) {
|
|
1254
|
-
let matched = true;
|
|
1255
|
-
for (let sigIndex = 0; sigIndex < PNG_SIGNATURE.length; sigIndex += 1) {
|
|
1256
|
-
if (data[index + sigIndex] !== PNG_SIGNATURE[sigIndex]) {
|
|
1257
|
-
matched = false;
|
|
1258
|
-
break;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
if (matched) return index;
|
|
1262
|
-
}
|
|
1263
|
-
return -1;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
function _findPngEnd(data: Uint8Array, start: number): number {
|
|
1267
|
-
let pos = start + PNG_SIGNATURE.length;
|
|
1268
|
-
while (pos + 8 <= data.length) {
|
|
1269
|
-
const length = _readUint32BE(data, pos);
|
|
1270
|
-
pos += 8;
|
|
1271
|
-
if (pos + length + 4 > data.length) return -1;
|
|
1272
|
-
const isIEND =
|
|
1273
|
-
data[pos - 4] === 0x49 &&
|
|
1274
|
-
data[pos - 3] === 0x45 &&
|
|
1275
|
-
data[pos - 2] === 0x4e &&
|
|
1276
|
-
data[pos - 1] === 0x44;
|
|
1277
|
-
pos += length + 4;
|
|
1278
|
-
if (isIEND) return pos;
|
|
1279
|
-
}
|
|
1280
|
-
return -1;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
function _parseJtaHeader(data: Uint8Array, firstPngOffset: number, frameCount: number): ExtractedJtaMeta | undefined {
|
|
1284
|
-
if (data.length < 10) return undefined;
|
|
1285
|
-
|
|
1286
|
-
const state = { offset: 0 };
|
|
1287
|
-
const end = Math.min(firstPngOffset, data.length);
|
|
1288
|
-
const mark = _readUtfBE(data, state, end);
|
|
1289
|
-
if (!mark) return undefined;
|
|
1290
|
-
|
|
1291
|
-
const version = _readInt32BEAt(data, state, end);
|
|
1292
|
-
if (version == null) return undefined;
|
|
1293
|
-
|
|
1294
|
-
const fpsRaw = _readInt8At(data, state, end);
|
|
1295
|
-
if (fpsRaw == null) return undefined;
|
|
1296
|
-
const fps = fpsRaw > 0 ? fpsRaw : 24;
|
|
1297
|
-
|
|
1298
|
-
if (state.offset + 3 > end) return undefined;
|
|
1299
|
-
state.offset += 3;
|
|
1300
|
-
|
|
1301
|
-
if (version < 102) return undefined;
|
|
1302
|
-
|
|
1303
|
-
_readUint16BEAt(data, state, end);
|
|
1304
|
-
_readUint16BEAt(data, state, end);
|
|
1305
|
-
const width = _readUint16BEAt(data, state, end);
|
|
1306
|
-
const height = _readUint16BEAt(data, state, end);
|
|
1307
|
-
if (width == null || height == null) return undefined;
|
|
1308
|
-
|
|
1309
|
-
const speedRaw = _readUint8At(data, state, end);
|
|
1310
|
-
const repeatDelayRaw = _readUint8At(data, state, end);
|
|
1311
|
-
const swingRaw = _readInt8At(data, state, end);
|
|
1312
|
-
const frameTableCount = _readInt16BEAt(data, state, end);
|
|
1313
|
-
if (speedRaw == null || repeatDelayRaw == null || swingRaw == null || frameTableCount == null) return undefined;
|
|
1314
|
-
|
|
1315
|
-
const frames: ExtractedJtaFrameMeta[] = [];
|
|
1316
|
-
for (let index = 0; index < frameTableCount; index += 1) {
|
|
1317
|
-
const delayRaw = _readInt16BEAt(data, state, end);
|
|
1318
|
-
const offsetX = _readInt16BEAt(data, state, end);
|
|
1319
|
-
const offsetY = _readInt16BEAt(data, state, end);
|
|
1320
|
-
const frameWidth = _readInt16BEAt(data, state, end);
|
|
1321
|
-
const frameHeight = _readInt16BEAt(data, state, end);
|
|
1322
|
-
const textureIndex = _readInt16BEAt(data, state, end);
|
|
1323
|
-
if (
|
|
1324
|
-
delayRaw == null ||
|
|
1325
|
-
offsetX == null ||
|
|
1326
|
-
offsetY == null ||
|
|
1327
|
-
frameWidth == null ||
|
|
1328
|
-
frameHeight == null ||
|
|
1329
|
-
textureIndex == null
|
|
1330
|
-
) {
|
|
1331
|
-
break;
|
|
1332
|
-
}
|
|
1333
|
-
frames.push({
|
|
1334
|
-
addDelay: Math.trunc((1000 / fps) * delayRaw),
|
|
1335
|
-
offsetX,
|
|
1336
|
-
offsetY,
|
|
1337
|
-
width: frameWidth,
|
|
1338
|
-
height: frameHeight,
|
|
1339
|
-
textureIndex,
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
return {
|
|
1344
|
-
interval: Math.trunc((1000 / fps) * (speedRaw || 1)),
|
|
1345
|
-
repeatDelay: Math.trunc((1000 / fps) * repeatDelayRaw),
|
|
1346
|
-
swing: swingRaw === 1,
|
|
1347
|
-
width,
|
|
1348
|
-
height,
|
|
1349
|
-
frames: frames.length === 0 && frameCount > 0 ? [] : frames,
|
|
1350
|
-
};
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
function _readUtfBE(data: Uint8Array, state: { offset: number }, end: number): string | null {
|
|
1354
|
-
const length = _readUint16BEAt(data, state, end);
|
|
1355
|
-
if (length == null || state.offset + length > end) return null;
|
|
1356
|
-
const value = new TextDecoder().decode(data.subarray(state.offset, state.offset + length));
|
|
1357
|
-
state.offset += length;
|
|
1358
|
-
return value;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
function _readUint8At(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
1362
|
-
if (state.offset + 1 > end) return null;
|
|
1363
|
-
const value = data[state.offset];
|
|
1364
|
-
state.offset += 1;
|
|
1365
|
-
return value ?? 0;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
function _readInt8At(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
1369
|
-
if (state.offset + 1 > end) return null;
|
|
1370
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1371
|
-
const value = view.getInt8(state.offset);
|
|
1372
|
-
state.offset += 1;
|
|
1373
|
-
return value;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
function _readUint16BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
1377
|
-
if (state.offset + 2 > end) return null;
|
|
1378
|
-
const value = _readUint16BE(data, state.offset);
|
|
1379
|
-
state.offset += 2;
|
|
1380
|
-
return value;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
function _readInt16BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
1384
|
-
if (state.offset + 2 > end) return null;
|
|
1385
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1386
|
-
const value = view.getInt16(state.offset, false);
|
|
1387
|
-
state.offset += 2;
|
|
1388
|
-
return value;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
function _readInt32BEAt(data: Uint8Array, state: { offset: number }, end: number): number | null {
|
|
1392
|
-
if (state.offset + 4 > end) return null;
|
|
1393
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1394
|
-
const value = view.getInt32(state.offset, false);
|
|
1395
|
-
state.offset += 4;
|
|
1396
|
-
return value;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
function _readUint16BE(data: Uint8Array, offset: number): number {
|
|
1400
|
-
if (offset + 1 >= data.length) return 0;
|
|
1401
|
-
return (data[offset] << 8) | data[offset + 1];
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
function _readUint32BE(data: Uint8Array, offset: number): number {
|
|
1405
|
-
if (offset + 3 >= data.length) return 0;
|
|
1406
|
-
return (
|
|
1407
|
-
(data[offset] * 0x1000000) +
|
|
1408
|
-
((data[offset + 1] ?? 0) << 16) +
|
|
1409
|
-
((data[offset + 2] ?? 0) << 8) +
|
|
1410
|
-
(data[offset + 3] ?? 0)
|
|
1411
|
-
);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
/** Collect a Bitmap Font's texture image, packed under the font's ID. */
|
|
1415
|
-
async function _collectFontTexture(
|
|
1416
|
-
doc: Document,
|
|
1417
|
-
fontRes: FontResource,
|
|
1418
|
-
pkg: Package,
|
|
1419
|
-
options: AtlasOptions,
|
|
1420
|
-
): Promise<void> {
|
|
1421
|
-
const textureId = fontRes.getTextureId?.() ?? '';
|
|
1422
|
-
|
|
1423
|
-
if (textureId) {
|
|
1424
|
-
// Record font→texture mapping so we can add a duplicate sprite entry
|
|
1425
|
-
// after atlas packing. The editor stores both the image ID (jb800) and
|
|
1426
|
-
// the font ID (wa8u2r) as separate sprites at the same atlas position.
|
|
1427
|
-
const fontId = fontRes.getId();
|
|
1428
|
-
fontRes.setExtras({ ...fontRes.getExtras(), _fontSpriteAlias: { fontId, textureId } });
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Parse .fnt file for glyph data (needed for binary encoding)
|
|
1432
|
-
// This applies to ALL fonts, not just those with a textureId
|
|
1433
|
-
if (options.readFileRaw && options.basePath) {
|
|
1434
|
-
const fontName = resolveFontFileName(fontRes.getName());
|
|
1435
|
-
const fontPath = fontRes.getPath() ?? '/';
|
|
1436
|
-
const pkgName = pkg.getName();
|
|
1437
|
-
const fntFile = `${options.basePath}/${pkgName}${fontPath}${fontName}`;
|
|
1438
|
-
try {
|
|
1439
|
-
const fntData = await options.readFileRaw(fntFile);
|
|
1440
|
-
const fntText = new TextDecoder().decode(fntData);
|
|
1441
|
-
const fntParsed = _parseFnt(fntText);
|
|
1442
|
-
for (const glyph of fontRes.listGlyphs()) {
|
|
1443
|
-
fontRes.removeGlyph(glyph);
|
|
1444
|
-
}
|
|
1445
|
-
fontRes
|
|
1446
|
-
.setTtf(fntParsed.hasFace)
|
|
1447
|
-
.setTint(fntParsed.colored)
|
|
1448
|
-
.setAutoScale(fntParsed.resizable)
|
|
1449
|
-
.setHasChannel(fntParsed.hasChannel)
|
|
1450
|
-
.setFontSize(fntParsed.fontSize)
|
|
1451
|
-
.setXAdvance(fntParsed.xadvance)
|
|
1452
|
-
.setLineHeight(fntParsed.lineHeight);
|
|
1453
|
-
for (const item of fntParsed.glyphs) {
|
|
1454
|
-
const glyph = doc.createFontGlyph(`${fontRes.getId()}_${item.charId}`);
|
|
1455
|
-
glyph
|
|
1456
|
-
.setCharId(item.charId)
|
|
1457
|
-
.setChar(item.charId > 0 ? String.fromCodePoint(item.charId) : '')
|
|
1458
|
-
.setImg(item.img ?? '')
|
|
1459
|
-
.setX(item.x)
|
|
1460
|
-
.setY(item.y)
|
|
1461
|
-
.setXOffset(item.xoffset)
|
|
1462
|
-
.setYOffset(item.yoffset)
|
|
1463
|
-
.setWidth(item.width)
|
|
1464
|
-
.setHeight(item.height)
|
|
1465
|
-
.setAdvance(item.xadvance)
|
|
1466
|
-
.setLineHeight(fntParsed.lineHeight)
|
|
1467
|
-
.setChannel(item.channel);
|
|
1468
|
-
fontRes.addGlyph(glyph);
|
|
1469
|
-
}
|
|
1470
|
-
} catch { /* .fnt not found */ }
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
/** Parse a BMFont .fnt text file into structured data for binary encoding. */
|
|
1475
|
-
function _parseFnt(text: string): {
|
|
1476
|
-
hasFace: boolean; colored: boolean; resizable: boolean; hasChannel: boolean;
|
|
1477
|
-
fontSize: number; xadvance: number; lineHeight: number;
|
|
1478
|
-
glyphs: Array<{
|
|
1479
|
-
charId: number; img: string | null;
|
|
1480
|
-
x: number; y: number; xoffset: number; yoffset: number;
|
|
1481
|
-
width: number; height: number; xadvance: number; channel: number;
|
|
1482
|
-
}>;
|
|
1483
|
-
} {
|
|
1484
|
-
const lines = text.split(/\r?\n/);
|
|
1485
|
-
let hasFace = false, colored = false, resizable = false, hasChannel = false;
|
|
1486
|
-
let fontSize = 0, globalXadvance = 0, lineHeight = 0;
|
|
1487
|
-
const glyphs: Array<{
|
|
1488
|
-
charId: number; img: string | null;
|
|
1489
|
-
x: number; y: number; xoffset: number; yoffset: number;
|
|
1490
|
-
width: number; height: number; xadvance: number; channel: number;
|
|
1491
|
-
}> = [];
|
|
1492
|
-
|
|
1493
|
-
for (const line of lines) {
|
|
1494
|
-
const trimmed = line.trim();
|
|
1495
|
-
if (!trimmed) continue;
|
|
1496
|
-
const parts = trimmed.split(/\s+/);
|
|
1497
|
-
const attrs: Record<string, string> = {};
|
|
1498
|
-
for (let i = 1; i < parts.length; i++) {
|
|
1499
|
-
const eq = parts[i].split('=');
|
|
1500
|
-
if (eq.length === 2) attrs[eq[0]] = eq[1];
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
switch (parts[0]) {
|
|
1504
|
-
case 'info':
|
|
1505
|
-
hasFace = attrs.face != null;
|
|
1506
|
-
colored = hasFace;
|
|
1507
|
-
if (attrs.colored !== undefined) colored = attrs.colored === 'true';
|
|
1508
|
-
fontSize = parseInt(attrs.size, 10) || 0;
|
|
1509
|
-
resizable = attrs.resizable === 'true';
|
|
1510
|
-
break;
|
|
1511
|
-
case 'common':
|
|
1512
|
-
lineHeight = parseInt(attrs.lineHeight, 10) || 0;
|
|
1513
|
-
globalXadvance = parseInt(attrs.xadvance, 10) || 0;
|
|
1514
|
-
if (fontSize === 0) fontSize = lineHeight;
|
|
1515
|
-
else if (lineHeight === 0) lineHeight = fontSize;
|
|
1516
|
-
break;
|
|
1517
|
-
case 'char': {
|
|
1518
|
-
const charId = parseInt(attrs.id, 10) || 0;
|
|
1519
|
-
if (charId === 0) continue;
|
|
1520
|
-
const img = attrs.img || null;
|
|
1521
|
-
if (!hasFace && !img) continue;
|
|
1522
|
-
const chnl = parseInt(attrs.chnl, 10) || 0;
|
|
1523
|
-
if (chnl !== 0 && chnl !== 15) hasChannel = true;
|
|
1524
|
-
glyphs.push({
|
|
1525
|
-
charId, img,
|
|
1526
|
-
x: parseInt(attrs.x, 10) || 0,
|
|
1527
|
-
y: parseInt(attrs.y, 10) || 0,
|
|
1528
|
-
xoffset: parseInt(attrs.xoffset, 10) || 0,
|
|
1529
|
-
yoffset: parseInt(attrs.yoffset, 10) || 0,
|
|
1530
|
-
width: parseInt(attrs.width, 10) || 0,
|
|
1531
|
-
height: parseInt(attrs.height, 10) || 0,
|
|
1532
|
-
xadvance: parseInt(attrs.xadvance, 10) || 0,
|
|
1533
|
-
channel: chnl,
|
|
1534
|
-
});
|
|
1535
|
-
break;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
return { hasFace, colored, resizable: fontSize > 0 ? resizable : false, hasChannel, fontSize, xadvance: globalXadvance, lineHeight, glyphs };
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
function isComponentResource(resource: PackageResource): resource is Component {
|
|
1544
|
-
return resource.propertyType === 'Component';
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function isImageResource(resource: PackageResource): resource is ImageResource {
|
|
1548
|
-
return resource.propertyType === 'ImageResource';
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
function isMovieClipResource(resource: PackageResource): resource is MovieClipResource {
|
|
1552
|
-
return resource.propertyType === 'MovieClipResource';
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
function isSkeletonResource(resource: PackageResource): resource is SpineResource | DragonBonesResource {
|
|
1556
|
-
return resource.propertyType === 'SpineResource' || resource.propertyType === 'DragonBonesResource';
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
function isFontResource(resource: PackageResource): resource is FontResource {
|
|
1560
|
-
return resource.propertyType === 'FontResource';
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
function isPackableResource(resource: PackageResource): resource is PackableResource {
|
|
1564
|
-
return isImageResource(resource) || isMovieClipResource(resource) || isFontResource(resource);
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
function addUiResourceRef(target: Set<string>, value: string | undefined | null): void {
|
|
1568
|
-
if (!value?.startsWith('ui://')) return;
|
|
1569
|
-
const refId = value.slice(5).slice(8);
|
|
1570
|
-
if (refId) target.add(refId);
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function addUiResourceRefsFromText(target: Set<string>, value: string | undefined | null): void {
|
|
1574
|
-
if (!value || typeof value !== 'string') return;
|
|
1575
|
-
const matches = value.matchAll(/ui:\/\/[0-9a-z]{8}([0-9a-z]+)/gi);
|
|
1576
|
-
for (const match of matches) {
|
|
1577
|
-
const refId = match[1] ?? '';
|
|
1578
|
-
if (refId) target.add(refId);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
function addUiResourceRefsFromUnknown(target: Set<string>, value: unknown): void {
|
|
1583
|
-
if (Array.isArray(value)) {
|
|
1584
|
-
for (const entry of value) addUiResourceRefsFromUnknown(target, entry);
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
if (typeof value === 'string') {
|
|
1588
|
-
addUiResourceRef(target, value);
|
|
1589
|
-
addUiResourceRefsFromText(target, value);
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
function isResolvedBuffer(value: Uint8Array | AtlasEncoderResolvedBuffer): value is AtlasEncoderResolvedBuffer {
|
|
1594
|
-
return typeof value === 'object' && value !== null && 'data' in value && 'info' in value;
|
|
1595
|
-
}
|