@openfairygui/functions 0.1.0 → 0.1.1

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/src/atlas.ts CHANGED
@@ -1,67 +1,67 @@
1
1
  import { GearType, TransitionActionType, type Component, type Document, type DragonBonesResource, type FontResource, type ILogger, type ImageResource, type MovieClipResource, type Package, type SpineResource, type Transform } from '@openfairygui/core';
2
2
  import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect } from './max-rects-compat.js';
3
3
  import { MaxRectsPackerCompat } from './max-rects-packer-compat.js';
4
- import type { ExtrasMap, HasOptionalSrc, HasOptionalUrl } from './shared-types.js';
5
- import { createTransform } from './utils.js';
6
-
4
+ import type { ExtrasMap, HasOptionalSrc, HasOptionalUrl } from './shared-types.js';
5
+ import { createTransform } from './utils.js';
6
+
7
7
  export interface AtlasOptions {
8
- /**
9
- * Sharp module instance, injected by the caller.
10
- * Required for actual image compositing and trimImage.
11
- *
12
- * ```ts
13
- * import sharp from 'sharp';
14
- * await doc.transform(atlas({ encoder: sharp }));
15
- * ```
16
- */
17
- encoder?: unknown;
18
-
8
+ /**
9
+ * Sharp module instance, injected by the caller.
10
+ * Required for actual image compositing and trimImage.
11
+ *
12
+ * ```ts
13
+ * import sharp from 'sharp';
14
+ * await doc.transform(atlas({ encoder: sharp }));
15
+ * ```
16
+ */
17
+ encoder?: unknown;
18
+
19
19
  /** Maximum atlas texture size (width and height). Default: 2048. */
20
20
  maxSize?: number;
21
21
 
22
22
  /** Whether to use the fast editor-compatible packing heuristics. Default: true. */
23
23
  fast?: boolean;
24
-
25
- /** Allow rotating sprites 90° for better packing. Default: true. */
26
- allowRotation?: boolean;
27
-
28
- /** Pixel padding between sprites. Default: 1. */
29
- padding?: number;
30
-
31
- /** Constrain atlas dimensions to powers of two. Default: false. */
32
- powerOfTwo?: boolean;
33
-
24
+
25
+ /** Allow rotating sprites 90° for better packing. Default: true. */
26
+ allowRotation?: boolean;
27
+
28
+ /** Pixel padding between sprites. Default: 1. */
29
+ padding?: number;
30
+
31
+ /** Constrain atlas dimensions to powers of two. Default: false. */
32
+ powerOfTwo?: boolean;
33
+
34
34
  /** Force square atlas (width === height). Default: false. */
35
35
  square?: boolean;
36
36
 
37
37
  /** Allow spilling into multiple atlas pages. Default: true. */
38
38
  multiPage?: boolean;
39
-
40
- /**
41
- * Trim transparent pixels from image edges before packing.
42
- * Requires encoder (sharp). Stores offset/originalSize in Sprite nodes.
43
- * Default: false.
44
- */
45
- trimImage?: boolean;
46
-
47
- /**
48
- * Base path for reading source images. If not set, images must have
49
- * their pixel data stored in extras._imageData as Uint8Array.
50
- */
51
- basePath?: string;
52
-
53
- /**
54
- * Output directory for generated atlas PNGs.
55
- * Required when encoder is provided.
56
- */
57
- outputPath?: string;
58
-
59
- /**
60
- * Optional mkdir function to ensure output directory exists.
61
- * If not provided, the outputPath directory must already exist.
62
- */
63
- mkdir?: (path: string) => Promise<void>;
64
-
39
+
40
+ /**
41
+ * Trim transparent pixels from image edges before packing.
42
+ * Requires encoder (sharp). Stores offset/originalSize in Sprite nodes.
43
+ * Default: false.
44
+ */
45
+ trimImage?: boolean;
46
+
47
+ /**
48
+ * Base path for reading source images. If not set, images must have
49
+ * their pixel data stored in extras._imageData as Uint8Array.
50
+ */
51
+ basePath?: string;
52
+
53
+ /**
54
+ * Output directory for generated atlas PNGs.
55
+ * Required when encoder is provided.
56
+ */
57
+ outputPath?: string;
58
+
59
+ /**
60
+ * Optional mkdir function to ensure output directory exists.
61
+ * If not provided, the outputPath directory must already exist.
62
+ */
63
+ mkdir?: (path: string) => Promise<void>;
64
+
65
65
  /**
66
66
  * Optional raw file reader for reading .jta MovieClip files.
67
67
  * Required for MovieClip frame atlas packing.
@@ -109,34 +109,34 @@ const ATLAS_DEFAULTS: Required<Omit<AtlasOptions, 'encoder' | 'basePath' | 'outp
109
109
  extractAlpha: false,
110
110
  separatedAtlasForBranch: false,
111
111
  };
112
-
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;
112
+
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
134
  type ParsedFnt = ReturnType<typeof _parseFnt>;
135
135
 
136
136
  function getPublishedItemId(resource: { getId(): string; getExtras(): ExtrasMap | undefined }): string {
137
137
  return ((resource.getExtras() as ImageResourceExtras | undefined) ?? {})._publishedId ?? resource.getId();
138
138
  }
139
-
139
+
140
140
  interface AtlasReferenceItem {
141
141
  icon?: string | null;
142
142
  url?: string | null;
@@ -176,12 +176,12 @@ interface ChildWithReferenceUrls extends HasOptionalSrc, HasOptionalUrl {
176
176
  getListItems?(): AtlasReferenceItem[];
177
177
  listGears?(): GearWithAtlasRefs[];
178
178
  }
179
-
179
+
180
180
  interface ImageResourceExtras extends ExtrasMap {
181
181
  _fileName?: string;
182
182
  _publishedId?: string;
183
183
  }
184
-
184
+
185
185
  interface FontSpriteAlias {
186
186
  fontId: string;
187
187
  textureId: string;
@@ -209,18 +209,18 @@ interface AtlasEncoderMetadata {
209
209
  trimOffsetLeft?: number;
210
210
  trimOffsetTop?: number;
211
211
  }
212
-
212
+
213
213
  interface AtlasEncoderResolvedBuffer {
214
214
  data: Uint8Array;
215
215
  info: Required<Pick<AtlasEncoderMetadata, 'width' | 'height' | 'channels'>> & AtlasEncoderMetadata;
216
216
  }
217
-
218
- interface AtlasCompositeInput {
219
- input: Uint8Array;
220
- left: number;
221
- top: number;
222
- }
223
-
217
+
218
+ interface AtlasCompositeInput {
219
+ input: Uint8Array;
220
+ left: number;
221
+ top: number;
222
+ }
223
+
224
224
  interface AtlasEncoderPipeline {
225
225
  ensureAlpha(): AtlasEncoderPipeline;
226
226
  raw(): AtlasEncoderPipeline;
@@ -228,25 +228,25 @@ interface AtlasEncoderPipeline {
228
228
  toBuffer(options: { resolveWithObject: true }): Promise<AtlasEncoderResolvedBuffer>;
229
229
  toBuffer(options?: { resolveWithObject?: false }): Promise<Uint8Array>;
230
230
  toBuffer(options?: { resolveWithObject?: boolean }): Promise<Uint8Array | AtlasEncoderResolvedBuffer>;
231
- png(): AtlasEncoderPipeline;
232
- metadata(): Promise<AtlasEncoderMetadata>;
233
- rotate(angle: number): AtlasEncoderPipeline;
234
- composite(inputs: AtlasCompositeInput[]): AtlasEncoderPipeline;
235
- toFile(path: string): Promise<unknown>;
236
- }
237
-
238
- type AtlasEncoderInput =
239
- | string
240
- | Uint8Array
241
- | {
242
- create: {
243
- width: number;
244
- height: number;
245
- channels: 4;
246
- background: { r: number; g: number; b: number; alpha: number };
247
- };
248
- };
249
-
231
+ png(): AtlasEncoderPipeline;
232
+ metadata(): Promise<AtlasEncoderMetadata>;
233
+ rotate(angle: number): AtlasEncoderPipeline;
234
+ composite(inputs: AtlasCompositeInput[]): AtlasEncoderPipeline;
235
+ toFile(path: string): Promise<unknown>;
236
+ }
237
+
238
+ type AtlasEncoderInput =
239
+ | string
240
+ | Uint8Array
241
+ | {
242
+ create: {
243
+ width: number;
244
+ height: number;
245
+ channels: 4;
246
+ background: { r: number; g: number; b: number; alpha: number };
247
+ };
248
+ };
249
+
250
250
  type AtlasEncoder = (input: AtlasEncoderInput) => AtlasEncoderPipeline;
251
251
 
252
252
  function resolveFontFileName(fontName: string): string {
@@ -449,38 +449,38 @@ async function resolveEditorCompatibleResourceOrder(
449
449
 
450
450
  return ordered;
451
451
  }
452
-
453
- /**
454
- * Packs image resources into texture atlases.
455
- *
456
- * This transform performs MaxRects bin-packing on all ImageResource items
457
- * within each package, creating Atlas and Sprite property nodes. When an
458
- * `encoder` (sharp) is provided, it also composites the actual PNG files.
459
- *
460
- * When `trimImage` is enabled and encoder is available, transparent pixels
461
- * at image edges are trimmed before packing. The trimmed offset and original
462
- * dimensions are stored in the Sprite nodes for runtime reconstruction.
463
- *
464
- * ```ts
465
- * import sharp from 'sharp';
466
- * await doc.transform(atlas({
467
- * encoder: sharp,
468
- * maxSize: 2048,
469
- * trimImage: true,
470
- * basePath: './assets/',
471
- * outputPath: './dist/',
472
- * }));
473
- * ```
474
- */
452
+
453
+ /**
454
+ * Packs image resources into texture atlases.
455
+ *
456
+ * This transform performs MaxRects bin-packing on all ImageResource items
457
+ * within each package, creating Atlas and Sprite property nodes. When an
458
+ * `encoder` (sharp) is provided, it also composites the actual PNG files.
459
+ *
460
+ * When `trimImage` is enabled and encoder is available, transparent pixels
461
+ * at image edges are trimmed before packing. The trimmed offset and original
462
+ * dimensions are stored in the Sprite nodes for runtime reconstruction.
463
+ *
464
+ * ```ts
465
+ * import sharp from 'sharp';
466
+ * await doc.transform(atlas({
467
+ * encoder: sharp,
468
+ * maxSize: 2048,
469
+ * trimImage: true,
470
+ * basePath: './assets/',
471
+ * outputPath: './dist/',
472
+ * }));
473
+ * ```
474
+ */
475
475
  export function atlas(_options: AtlasOptions = {}): Transform {
476
- const options = { ...ATLAS_DEFAULTS, ..._options };
477
-
478
- return createTransform('atlas', async (doc: Document): Promise<void> => {
479
- const root = doc.getRoot();
480
- const logger = doc.getLogger();
481
- const encoder = options.encoder as AtlasEncoder | undefined;
482
- const doTrim = options.trimImage && !!encoder && !!options.basePath;
483
-
476
+ const options = { ...ATLAS_DEFAULTS, ..._options };
477
+
478
+ return createTransform('atlas', async (doc: Document): Promise<void> => {
479
+ const root = doc.getRoot();
480
+ const logger = doc.getLogger();
481
+ const encoder = options.encoder as AtlasEncoder | undefined;
482
+ const doTrim = options.trimImage && !!encoder && !!options.basePath;
483
+
484
484
  for (const pkg of root.listPackages()) {
485
485
  // Respect publish-selected resources when publish() precomputes a merged branch view.
486
486
  const selectedPublishIds = new Set(((pkg.getExtras() as PackageAtlasExtras | undefined) ?? {}).publishedResourceIds ?? []);
@@ -494,30 +494,30 @@ export function atlas(_options: AtlasOptions = {}): Transform {
494
494
  const orderedAllResources = sortResourcesByOrder(allResources, resourceOrder, inputOrder);
495
495
  const hasPackable = allResources.some((resource) => isPackableResource(resource));
496
496
  if (!hasPackable) continue;
497
-
498
- // Collect packable items in declaration order
499
- const inputs: InputItem[] = [];
500
-
501
- // Build set of referenced resource IDs (editor only packs referenced images)
502
- // Walk component tree recursively to find all image references
503
- const referencedIds = new Set<string>();
504
- const resourceMap = new Map<string, PackageResource>();
505
- for (const res of allResources) {
506
- const id = res.getId();
507
- if (id) resourceMap.set(id, res);
508
- }
509
- function collectRefs(component: Component, visited: Set<string>): void {
510
- for (const child of component.listChildren()) {
511
- const refChild = child as ChildWithReferenceUrls;
512
- const src = refChild.getSrc?.();
513
- if (src && !visited.has(src)) {
514
- referencedIds.add(src);
515
- visited.add(src);
516
- const srcRes = resourceMap.get(src);
517
- if (srcRes && isComponentResource(srcRes)) {
518
- collectRefs(srcRes, visited);
519
- }
520
- }
497
+
498
+ // Collect packable items in declaration order
499
+ const inputs: InputItem[] = [];
500
+
501
+ // Build set of referenced resource IDs (editor only packs referenced images)
502
+ // Walk component tree recursively to find all image references
503
+ const referencedIds = new Set<string>();
504
+ const resourceMap = new Map<string, PackageResource>();
505
+ for (const res of allResources) {
506
+ const id = res.getId();
507
+ if (id) resourceMap.set(id, res);
508
+ }
509
+ function collectRefs(component: Component, visited: Set<string>): void {
510
+ for (const child of component.listChildren()) {
511
+ const refChild = child as ChildWithReferenceUrls;
512
+ const src = refChild.getSrc?.();
513
+ if (src && !visited.has(src)) {
514
+ referencedIds.add(src);
515
+ visited.add(src);
516
+ const srcRes = resourceMap.get(src);
517
+ if (srcRes && isComponentResource(srcRes)) {
518
+ collectRefs(srcRes, visited);
519
+ }
520
+ }
521
521
  for (const ref of [
522
522
  refChild.getIcon?.(),
523
523
  refChild.getSelectedIcon?.(),
@@ -565,23 +565,23 @@ export function atlas(_options: AtlasOptions = {}): Transform {
565
565
  if (isFontResource(res)) {
566
566
  const textureId = res.getTextureId?.() ?? '';
567
567
  if (textureId) referencedIds.add(textureId);
568
- // Parse .fnt file for glyph image references
568
+ // Parse .fnt file for glyph image references
569
569
  if (options.readFileRaw && options.basePath) {
570
570
  const fontName = resolveFontFileName(res.getName());
571
571
  const fontPath = res.getPath() ?? '/';
572
572
  const fntFile = `${options.basePath}/${pkg.getName()}${fontPath}${fontName}`;
573
- try {
574
- const fntData = await options.readFileRaw(fntFile);
575
- const fntText = new TextDecoder().decode(fntData);
576
- for (const line of fntText.split(/\r?\n/)) {
577
- const match = line.match(/img=(\w+)/);
578
- if (match) referencedIds.add(match[1]);
579
- }
580
- } catch { /* .fnt file not found — OK */ }
581
- }
582
- }
583
- }
584
-
573
+ try {
574
+ const fntData = await options.readFileRaw(fntFile);
575
+ const fntText = new TextDecoder().decode(fntData);
576
+ for (const line of fntText.split(/\r?\n/)) {
577
+ const match = line.match(/img=(\w+)/);
578
+ if (match) referencedIds.add(match[1]);
579
+ }
580
+ } catch { /* .fnt file not found — OK */ }
581
+ }
582
+ }
583
+ }
584
+
585
585
  for (const res of orderedAllResources) {
586
586
  if (isImageResource(res)) {
587
587
  // Pack referenced images, plus explicitly exported standalone images.
@@ -801,7 +801,7 @@ function buildBranchAtlasGroups(doc: Document, inputs: InputItem[], options: Atl
801
801
  inputs: groups.get(branchName) ?? [],
802
802
  }));
803
803
  }
804
-
804
+
805
805
  function inputToCompatRect(input: InputItem, index: number): CompatNodeRect {
806
806
  const duplicatePadding = isImageResource(input.resource) && input.resource.getDuplicatePadding?.() === true;
807
807
  return {
@@ -983,12 +983,12 @@ interface ExtractedJtaData {
983
983
  frames: Uint8Array[];
984
984
  meta?: ExtractedJtaMeta;
985
985
  }
986
-
987
- /**
988
- * Trim transparent edges from an image using sharp.
989
- * Returns the trimmed buffer, dimensions, and offsets.
990
- * Falls back to the original image if trim fails (e.g. no alpha channel, no transparent edges).
991
- */
986
+
987
+ /**
988
+ * Trim transparent edges from an image using sharp.
989
+ * Returns the trimmed buffer, dimensions, and offsets.
990
+ * Falls back to the original image if trim fails (e.g. no alpha channel, no transparent edges).
991
+ */
992
992
  async function _trimImage(
993
993
  encoder: AtlasEncoder,
994
994
  filePath: string,
@@ -1055,24 +1055,24 @@ async function _trimImage(
1055
1055
  originalWidth,
1056
1056
  originalHeight,
1057
1057
  };
1058
- } catch {
1059
- // Trim failed (e.g. JPEG without alpha, nothing to trim) — return original
1060
- const buf = await encoder(filePath).png().toBuffer();
1061
- return {
1062
- buffer: buf,
1063
- width: originalWidth,
1064
- height: originalHeight,
1065
- offsetX: 0,
1066
- offsetY: 0,
1067
- originalWidth,
1068
- originalHeight,
1069
- };
1070
- }
1071
- }
1072
-
1073
- /**
1074
- * Resolve an ImageResource to its actual file path on disk.
1075
- */
1058
+ } catch {
1059
+ // Trim failed (e.g. JPEG without alpha, nothing to trim) — return original
1060
+ const buf = await encoder(filePath).png().toBuffer();
1061
+ return {
1062
+ buffer: buf,
1063
+ width: originalWidth,
1064
+ height: originalHeight,
1065
+ offsetX: 0,
1066
+ offsetY: 0,
1067
+ originalWidth,
1068
+ originalHeight,
1069
+ };
1070
+ }
1071
+ }
1072
+
1073
+ /**
1074
+ * Resolve an ImageResource to its actual file path on disk.
1075
+ */
1076
1076
  function _resolveImagePath(resource: ImageResource, pkg: Package, basePath: string): string {
1077
1077
  const imgPath = resource.getPath() ?? '/';
1078
1078
  const fileName = resolveImageFileName(resource);
@@ -1085,7 +1085,7 @@ function _resolveImagePath(resource: ImageResource, pkg: Package, basePath: stri
1085
1085
  : `${normalizedBasePath}_${branchName}`;
1086
1086
  return `${packageBasePath}/${pkg.getName()}${imgPath}${fileName}`;
1087
1087
  }
1088
-
1088
+
1089
1089
  type InputItem = {
1090
1090
  id: string; width: number; height: number;
1091
1091
  originalWidth: number; originalHeight: number;
@@ -1093,17 +1093,17 @@ type InputItem = {
1093
1093
  resource: PackInputResource; trimBuffer?: Uint8Array;
1094
1094
  sourceKind: 'image' | 'movieclip-frame';
1095
1095
  };
1096
-
1097
- /** Collect a single ImageResource into the inputs array. */
1096
+
1097
+ /** Collect a single ImageResource into the inputs array. */
1098
1098
  async function _collectImage(
1099
1099
  resource: ImageResource,
1100
- pkg: Package,
1101
- inputs: InputItem[],
1102
- encoder: AtlasEncoder | undefined,
1103
- options: AtlasOptions,
1104
- doTrim: boolean,
1105
- logger: ILogger,
1106
- ): Promise<void> {
1100
+ pkg: Package,
1101
+ inputs: InputItem[],
1102
+ encoder: AtlasEncoder | undefined,
1103
+ options: AtlasOptions,
1104
+ doTrim: boolean,
1105
+ logger: ILogger,
1106
+ ): Promise<void> {
1107
1107
  let origW = resource.getWidth() ?? 0;
1108
1108
  let origH = resource.getHeight() ?? 0;
1109
1109
  let sourceHasAlpha = false;
@@ -1128,24 +1128,24 @@ async function _collectImage(
1128
1128
  }
1129
1129
 
1130
1130
  if (origW <= 0 || origH <= 0) return;
1131
-
1132
- let packW = origW, packH = origH, offX = 0, offY = 0;
1133
- let trimBuf: Uint8Array | undefined;
1134
-
1131
+
1132
+ let packW = origW, packH = origH, offX = 0, offY = 0;
1133
+ let trimBuf: Uint8Array | undefined;
1134
+
1135
1135
  if (doTrim && sourceHasAlpha && options.basePath && encoder) {
1136
1136
  const filePath = _resolveImagePath(resource, pkg, options.basePath);
1137
1137
  try {
1138
1138
  const trimResult = await _trimImage(encoder, filePath, origW, origH);
1139
- packW = trimResult.width;
1140
- packH = trimResult.height;
1141
- offX = trimResult.offsetX;
1142
- offY = trimResult.offsetY;
1143
- trimBuf = trimResult.buffer;
1144
- } catch {
1145
- logger.warn(`atlas: Could not trim "${filePath}", using original.`);
1146
- }
1147
- }
1148
-
1139
+ packW = trimResult.width;
1140
+ packH = trimResult.height;
1141
+ offX = trimResult.offsetX;
1142
+ offY = trimResult.offsetY;
1143
+ trimBuf = trimResult.buffer;
1144
+ } catch {
1145
+ logger.warn(`atlas: Could not trim "${filePath}", using original.`);
1146
+ }
1147
+ }
1148
+
1149
1149
  inputs.push({
1150
1150
  id: getPublishedItemId(resource), width: packW, height: packH,
1151
1151
  originalWidth: origW, originalHeight: origH,
@@ -1155,22 +1155,22 @@ async function _collectImage(
1155
1155
  sourceKind: 'image',
1156
1156
  });
1157
1157
  }
1158
-
1159
- /** Collect MovieClip frame textures from a .jta file into the inputs array. */
1158
+
1159
+ /** Collect MovieClip frame textures from a .jta file into the inputs array. */
1160
1160
  async function _collectMovieClipFrames(
1161
1161
  doc: Document,
1162
1162
  resource: MovieClipResource,
1163
1163
  pkg: Package,
1164
1164
  inputs: InputItem[],
1165
- encoder: AtlasEncoder | undefined,
1166
- options: AtlasOptions,
1167
- logger: ILogger,
1168
- ): Promise<void> {
1169
- if (!options.basePath || !options.readFileRaw) return;
1170
-
1171
- const mcId = resource.getId();
1172
- const mcName = resource.getName() + '.jta';
1173
- const mcPath = resource.getPath() ?? '/';
1165
+ encoder: AtlasEncoder | undefined,
1166
+ options: AtlasOptions,
1167
+ logger: ILogger,
1168
+ ): Promise<void> {
1169
+ if (!options.basePath || !options.readFileRaw) return;
1170
+
1171
+ const mcId = resource.getId();
1172
+ const mcName = resource.getName() + '.jta';
1173
+ const mcPath = resource.getPath() ?? '/';
1174
1174
  const filePath = `${options.basePath}/${pkg.getName()}${mcPath}${mcName}`;
1175
1175
 
1176
1176
  try {
@@ -1465,8 +1465,8 @@ function _readUint32BE(data: Uint8Array, offset: number): number {
1465
1465
  (data[offset + 3] ?? 0)
1466
1466
  );
1467
1467
  }
1468
-
1469
- /** Collect a Bitmap Font's texture image, packed under the font's ID. */
1468
+
1469
+ /** Collect a Bitmap Font's texture image, packed under the font's ID. */
1470
1470
  async function _collectFontTexture(
1471
1471
  doc: Document,
1472
1472
  fontRes: FontResource,
@@ -1475,22 +1475,22 @@ async function _collectFontTexture(
1475
1475
  ): Promise<void> {
1476
1476
  const extras = fontRes.getExtras() as FontResourceExtras;
1477
1477
  const textureId = fontRes.getTextureId?.() ?? '';
1478
-
1479
- if (textureId) {
1480
- // Record font→texture mapping so we can add a duplicate sprite entry
1481
- // after atlas packing. The editor stores both the image ID (jb800) and
1482
- // the font ID (wa8u2r) as separate sprites at the same atlas position.
1483
- const fontId = fontRes.getId();
1484
- fontRes.setExtras({ ...fontRes.getExtras(), _fontSpriteAlias: { fontId, textureId } });
1485
- }
1486
-
1487
- // Parse .fnt file for glyph data (needed for binary encoding)
1488
- // This applies to ALL fonts, not just those with a textureId
1478
+
1479
+ if (textureId) {
1480
+ // Record font→texture mapping so we can add a duplicate sprite entry
1481
+ // after atlas packing. The editor stores both the image ID (jb800) and
1482
+ // the font ID (wa8u2r) as separate sprites at the same atlas position.
1483
+ const fontId = fontRes.getId();
1484
+ fontRes.setExtras({ ...fontRes.getExtras(), _fontSpriteAlias: { fontId, textureId } });
1485
+ }
1486
+
1487
+ // Parse .fnt file for glyph data (needed for binary encoding)
1488
+ // This applies to ALL fonts, not just those with a textureId
1489
1489
  if (options.readFileRaw && options.basePath) {
1490
1490
  const fontName = resolveFontFileName(fontRes.getName());
1491
1491
  const fontPath = fontRes.getPath() ?? '/';
1492
1492
  const pkgName = pkg.getName();
1493
- const fntFile = `${options.basePath}/${pkgName}${fontPath}${fontName}`;
1493
+ const fntFile = `${options.basePath}/${pkgName}${fontPath}${fontName}`;
1494
1494
  try {
1495
1495
  const fntData = await options.readFileRaw(fntFile);
1496
1496
  const fntText = new TextDecoder().decode(fntData);
@@ -1526,84 +1526,84 @@ async function _collectFontTexture(
1526
1526
  } catch { /* .fnt not found */ }
1527
1527
  }
1528
1528
  }
1529
-
1530
- /** Parse a BMFont .fnt text file into structured data for binary encoding. */
1531
- function _parseFnt(text: string): {
1532
- hasFace: boolean; colored: boolean; resizable: boolean; hasChannel: boolean;
1533
- fontSize: number; xadvance: number; lineHeight: number;
1534
- glyphs: Array<{
1535
- charId: number; img: string | null;
1536
- x: number; y: number; xoffset: number; yoffset: number;
1537
- width: number; height: number; xadvance: number; channel: number;
1538
- }>;
1539
- } {
1540
- const lines = text.split(/\r?\n/);
1541
- let hasFace = false, colored = false, resizable = false, hasChannel = false;
1542
- let fontSize = 0, globalXadvance = 0, lineHeight = 0;
1543
- const glyphs: Array<{
1544
- charId: number; img: string | null;
1545
- x: number; y: number; xoffset: number; yoffset: number;
1546
- width: number; height: number; xadvance: number; channel: number;
1547
- }> = [];
1548
-
1549
- for (const line of lines) {
1550
- const trimmed = line.trim();
1551
- if (!trimmed) continue;
1552
- const parts = trimmed.split(/\s+/);
1553
- const attrs: Record<string, string> = {};
1554
- for (let i = 1; i < parts.length; i++) {
1555
- const eq = parts[i].split('=');
1556
- if (eq.length === 2) attrs[eq[0]] = eq[1];
1557
- }
1558
-
1559
- switch (parts[0]) {
1560
- case 'info':
1561
- hasFace = attrs.face != null;
1562
- colored = hasFace;
1563
- if (attrs.colored !== undefined) colored = attrs.colored === 'true';
1564
- fontSize = parseInt(attrs.size, 10) || 0;
1565
- resizable = attrs.resizable === 'true';
1566
- break;
1567
- case 'common':
1568
- lineHeight = parseInt(attrs.lineHeight, 10) || 0;
1569
- globalXadvance = parseInt(attrs.xadvance, 10) || 0;
1570
- if (fontSize === 0) fontSize = lineHeight;
1571
- else if (lineHeight === 0) lineHeight = fontSize;
1572
- break;
1573
- case 'char': {
1574
- const charId = parseInt(attrs.id, 10) || 0;
1575
- if (charId === 0) continue;
1576
- const img = attrs.img || null;
1577
- if (!hasFace && !img) continue;
1578
- const chnl = parseInt(attrs.chnl, 10) || 0;
1579
- if (chnl !== 0 && chnl !== 15) hasChannel = true;
1580
- glyphs.push({
1581
- charId, img,
1582
- x: parseInt(attrs.x, 10) || 0,
1583
- y: parseInt(attrs.y, 10) || 0,
1584
- xoffset: parseInt(attrs.xoffset, 10) || 0,
1585
- yoffset: parseInt(attrs.yoffset, 10) || 0,
1586
- width: parseInt(attrs.width, 10) || 0,
1587
- height: parseInt(attrs.height, 10) || 0,
1588
- xadvance: parseInt(attrs.xadvance, 10) || 0,
1589
- channel: chnl,
1590
- });
1591
- break;
1592
- }
1593
- }
1594
- }
1595
-
1596
- return { hasFace, colored, resizable: fontSize > 0 ? resizable : false, hasChannel, fontSize, xadvance: globalXadvance, lineHeight, glyphs };
1597
- }
1598
-
1599
- function isComponentResource(resource: PackageResource): resource is Component {
1600
- return resource.propertyType === 'Component';
1601
- }
1602
-
1603
- function isImageResource(resource: PackageResource): resource is ImageResource {
1604
- return resource.propertyType === 'ImageResource';
1605
- }
1606
-
1529
+
1530
+ /** Parse a BMFont .fnt text file into structured data for binary encoding. */
1531
+ function _parseFnt(text: string): {
1532
+ hasFace: boolean; colored: boolean; resizable: boolean; hasChannel: boolean;
1533
+ fontSize: number; xadvance: number; lineHeight: number;
1534
+ glyphs: Array<{
1535
+ charId: number; img: string | null;
1536
+ x: number; y: number; xoffset: number; yoffset: number;
1537
+ width: number; height: number; xadvance: number; channel: number;
1538
+ }>;
1539
+ } {
1540
+ const lines = text.split(/\r?\n/);
1541
+ let hasFace = false, colored = false, resizable = false, hasChannel = false;
1542
+ let fontSize = 0, globalXadvance = 0, lineHeight = 0;
1543
+ const glyphs: Array<{
1544
+ charId: number; img: string | null;
1545
+ x: number; y: number; xoffset: number; yoffset: number;
1546
+ width: number; height: number; xadvance: number; channel: number;
1547
+ }> = [];
1548
+
1549
+ for (const line of lines) {
1550
+ const trimmed = line.trim();
1551
+ if (!trimmed) continue;
1552
+ const parts = trimmed.split(/\s+/);
1553
+ const attrs: Record<string, string> = {};
1554
+ for (let i = 1; i < parts.length; i++) {
1555
+ const eq = parts[i].split('=');
1556
+ if (eq.length === 2) attrs[eq[0]] = eq[1];
1557
+ }
1558
+
1559
+ switch (parts[0]) {
1560
+ case 'info':
1561
+ hasFace = attrs.face != null;
1562
+ colored = hasFace;
1563
+ if (attrs.colored !== undefined) colored = attrs.colored === 'true';
1564
+ fontSize = parseInt(attrs.size, 10) || 0;
1565
+ resizable = attrs.resizable === 'true';
1566
+ break;
1567
+ case 'common':
1568
+ lineHeight = parseInt(attrs.lineHeight, 10) || 0;
1569
+ globalXadvance = parseInt(attrs.xadvance, 10) || 0;
1570
+ if (fontSize === 0) fontSize = lineHeight;
1571
+ else if (lineHeight === 0) lineHeight = fontSize;
1572
+ break;
1573
+ case 'char': {
1574
+ const charId = parseInt(attrs.id, 10) || 0;
1575
+ if (charId === 0) continue;
1576
+ const img = attrs.img || null;
1577
+ if (!hasFace && !img) continue;
1578
+ const chnl = parseInt(attrs.chnl, 10) || 0;
1579
+ if (chnl !== 0 && chnl !== 15) hasChannel = true;
1580
+ glyphs.push({
1581
+ charId, img,
1582
+ x: parseInt(attrs.x, 10) || 0,
1583
+ y: parseInt(attrs.y, 10) || 0,
1584
+ xoffset: parseInt(attrs.xoffset, 10) || 0,
1585
+ yoffset: parseInt(attrs.yoffset, 10) || 0,
1586
+ width: parseInt(attrs.width, 10) || 0,
1587
+ height: parseInt(attrs.height, 10) || 0,
1588
+ xadvance: parseInt(attrs.xadvance, 10) || 0,
1589
+ channel: chnl,
1590
+ });
1591
+ break;
1592
+ }
1593
+ }
1594
+ }
1595
+
1596
+ return { hasFace, colored, resizable: fontSize > 0 ? resizable : false, hasChannel, fontSize, xadvance: globalXadvance, lineHeight, glyphs };
1597
+ }
1598
+
1599
+ function isComponentResource(resource: PackageResource): resource is Component {
1600
+ return resource.propertyType === 'Component';
1601
+ }
1602
+
1603
+ function isImageResource(resource: PackageResource): resource is ImageResource {
1604
+ return resource.propertyType === 'ImageResource';
1605
+ }
1606
+
1607
1607
  function isMovieClipResource(resource: PackageResource): resource is MovieClipResource {
1608
1608
  return resource.propertyType === 'MovieClipResource';
1609
1609
  }
@@ -1611,15 +1611,15 @@ function isMovieClipResource(resource: PackageResource): resource is MovieClipRe
1611
1611
  function isSkeletonResource(resource: PackageResource): resource is SpineResource | DragonBonesResource {
1612
1612
  return resource.propertyType === 'SpineResource' || resource.propertyType === 'DragonBonesResource';
1613
1613
  }
1614
-
1615
- function isFontResource(resource: PackageResource): resource is FontResource {
1616
- return resource.propertyType === 'FontResource';
1617
- }
1618
-
1619
- function isPackableResource(resource: PackageResource): resource is PackableResource {
1620
- return isImageResource(resource) || isMovieClipResource(resource) || isFontResource(resource);
1621
- }
1622
-
1614
+
1615
+ function isFontResource(resource: PackageResource): resource is FontResource {
1616
+ return resource.propertyType === 'FontResource';
1617
+ }
1618
+
1619
+ function isPackableResource(resource: PackageResource): resource is PackableResource {
1620
+ return isImageResource(resource) || isMovieClipResource(resource) || isFontResource(resource);
1621
+ }
1622
+
1623
1623
  function addUiResourceRef(target: Set<string>, value: string | undefined | null): void {
1624
1624
  if (!value?.startsWith('ui://')) return;
1625
1625
  const refId = value.slice(5).slice(8);
@@ -1645,7 +1645,7 @@ function addUiResourceRefsFromUnknown(target: Set<string>, value: unknown): void
1645
1645
  addUiResourceRefsFromText(target, value);
1646
1646
  }
1647
1647
  }
1648
-
1649
- function isResolvedBuffer(value: Uint8Array | AtlasEncoderResolvedBuffer): value is AtlasEncoderResolvedBuffer {
1650
- return typeof value === 'object' && value !== null && 'data' in value && 'info' in value;
1651
- }
1648
+
1649
+ function isResolvedBuffer(value: Uint8Array | AtlasEncoderResolvedBuffer): value is AtlasEncoderResolvedBuffer {
1650
+ return typeof value === 'object' && value !== null && 'data' in value && 'info' in value;
1651
+ }