@openfairygui/functions 0.3.0-alpha.4 → 0.3.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.
Files changed (37) hide show
  1. package/dist/{atlas-C6tbl7nn.d.ts → atlas-BtWa1DwO.d.ts} +6 -0
  2. package/dist/{atlas-CHsu2Y8i.d.cts → atlas-CSqHsG0X.d.cts} +6 -0
  3. package/dist/index.cjs +2 -2
  4. package/dist/index.d.cts +2 -2
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/node.cjs +128 -30
  8. package/dist/node.d.cts +2 -2
  9. package/dist/node.d.ts +2 -2
  10. package/dist/node.js +128 -30
  11. package/dist/{publish-CyBj2o7n.js → publish-Cy09yjfn.js} +122 -54
  12. package/dist/{publish-C7qHwEiP.cjs → publish-DhMv_lEP.cjs} +122 -54
  13. package/dist/{restore-BeWaJNjR.d.cts → restore-CbscKkNw.d.ts} +9 -4
  14. package/dist/{restore-DVo1hXpN.js → restore-DB_L_xq4.js} +1 -1
  15. package/dist/{restore-Dh0-Nvms.d.ts → restore-k3oX26Fs.d.cts} +9 -4
  16. package/dist/{restore-LXbDQqmT.cjs → restore-n61oZ4nw.cjs} +1 -1
  17. package/dist/web.cjs +47 -64
  18. package/dist/web.d.cts +2 -2
  19. package/dist/web.d.ts +2 -2
  20. package/dist/web.js +49 -66
  21. package/package.json +5 -2
  22. package/src/adapters/node/plugins.ts +29 -13
  23. package/src/adapters/node/publish.ts +74 -4
  24. package/src/adapters/node/validate.ts +1 -0
  25. package/src/adapters/web/publish.ts +2 -0
  26. package/src/adapters/web/raster.ts +55 -72
  27. package/src/atlas/packing.ts +22 -13
  28. package/src/atlas.ts +22 -3
  29. package/src/codegen.ts +24 -11
  30. package/src/plugins/types.ts +7 -0
  31. package/src/publish/contracts.ts +3 -0
  32. package/src/publish/external-resources.ts +12 -6
  33. package/src/publish/options.ts +21 -7
  34. package/src/publish/package-context.ts +35 -11
  35. package/src/publish/resource-references.ts +21 -6
  36. package/src/publish.ts +26 -4
  37. package/src/utils.ts +2 -2
@@ -6,7 +6,8 @@ import type {
6
6
  PublishFileSystem,
7
7
  PublishSourceFileSystem,
8
8
  } from '../../publish/contracts.js';
9
- import { XMLParser, XMLValidator } from 'fast-xml-parser';
9
+ import { validateSafeSvgSource } from '@openfairygui/core';
10
+ import { XMLParser } from 'fast-xml-parser';
10
11
 
11
12
  type BrowserCanvas = OffscreenCanvas | HTMLCanvasElement;
12
13
 
@@ -27,6 +28,7 @@ interface BrowserContext {
27
28
  ): void;
28
29
  fillRect(x: number, y: number, width: number, height: number): void;
29
30
  getImageData(sx: number, sy: number, sw: number, sh: number): ImageData;
31
+ putImageData(imageData: ImageData, dx: number, dy: number): void;
30
32
  rotate(angle: number): void;
31
33
  restore(): void;
32
34
  save(): void;
@@ -40,30 +42,8 @@ interface BrowserRaster {
40
42
  height: number;
41
43
  }
42
44
 
43
- const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
44
45
  const MAX_SVG_DIMENSION = 16_384;
45
46
  const MAX_SVG_PIXELS = 64 * 1024 * 1024;
46
- const MAX_SVG_NODES = 50_000;
47
- const UNSAFE_SVG_ELEMENTS = new Set([
48
- 'a',
49
- 'animate',
50
- 'animatecolor',
51
- 'animatemotion',
52
- 'animatetransform',
53
- 'audio',
54
- 'canvas',
55
- 'discard',
56
- 'embed',
57
- 'feimage',
58
- 'foreignobject',
59
- 'iframe',
60
- 'image',
61
- 'object',
62
- 'script',
63
- 'set',
64
- 'style',
65
- 'video',
66
- ]);
67
47
 
68
48
  type ParsedSvgEntry = Record<string, unknown> & { ':@'?: Record<string, unknown> };
69
49
 
@@ -86,57 +66,16 @@ function parseSvgLength(value: unknown, name: string): number | undefined {
86
66
  return parsed;
87
67
  }
88
68
 
89
- function validateSvgAttribute(name: string, value: unknown): void {
90
- const normalizedName = name.toLowerCase();
91
- if (normalizedName === 'xmlns' || normalizedName.startsWith('xmlns:')) return;
92
- const localName = svgLocalName(name);
93
- const text = String(value);
94
- if (localName.startsWith('on')) unsafeSvg(`event attribute "${name}" is not allowed`);
95
- if (localName === 'style' || localName === 'src') unsafeSvg(`attribute "${name}" is not allowed`);
96
- if (localName === 'href' && !/^#[A-Za-z_][\w:.-]*$/u.test(text)) {
97
- unsafeSvg(`external reference in "${name}" is not allowed`);
98
- }
99
- if (/(?:^|[\s("'=])(?:https?:|file:|javascript:|data:|\/\/)/iu.test(text)) {
100
- unsafeSvg(`external URL in "${name}" is not allowed`);
101
- }
102
- for (const match of text.matchAll(/url\s*\(([^)]*)\)/giu)) {
103
- const reference = (match[1] ?? '').trim().replace(/^(['"])(.*)\1$/u, '$2');
104
- if (!/^#[A-Za-z_][\w:.-]*$/u.test(reference)) unsafeSvg(`external url() in "${name}" is not allowed`);
105
- }
106
- }
107
-
108
- function visitSvgEntry(entry: ParsedSvgEntry): void {
109
- const pending = [entry];
110
- let nodeCount = 0;
111
- while (pending.length > 0) {
112
- const current = pending.pop()!;
113
- for (const [name, value] of Object.entries(current)) {
114
- if (name === ':@' || name.startsWith('#') || name.startsWith('?')) continue;
115
- if (++nodeCount > MAX_SVG_NODES) unsafeSvg('node count exceeds the supported limit');
116
- const localName = svgLocalName(name);
117
- if (UNSAFE_SVG_ELEMENTS.has(localName)) unsafeSvg(`element <${name}> is not allowed`);
118
- for (const [attributeName, attributeValue] of Object.entries(current[':@'] ?? {})) {
119
- validateSvgAttribute(attributeName, attributeValue);
120
- }
121
- if (Array.isArray(value)) {
122
- for (const child of value) {
123
- if (child && typeof child === 'object' && !Array.isArray(child)) pending.push(child as ParsedSvgEntry);
124
- }
125
- }
126
- }
127
- }
128
- }
129
-
130
69
  function validateSvg(bytes: Uint8Array): void {
131
- if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg('source size is unsupported');
132
- let source: string;
133
70
  try {
134
- source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
135
- } catch {
136
- unsafeSvg('source is not valid UTF-8');
71
+ const source = validateSafeSvgSource(bytes);
72
+ validateSvgDimensions(source);
73
+ } catch (error) {
74
+ unsafeSvg(error instanceof Error ? error.message : String(error));
137
75
  }
138
- if (/<!\s*(?:doctype|entity)\b|<\?xml-stylesheet\b/iu.test(source)) unsafeSvg('DTD, entities, and stylesheets are not allowed');
139
- if (XMLValidator.validate(source, { allowBooleanAttributes: false }) !== true) unsafeSvg('source is not well-formed XML');
76
+ }
77
+
78
+ function validateSvgDimensions(source: string): void {
140
79
  const parsed = new XMLParser({
141
80
  preserveOrder: true,
142
81
  ignoreAttributes: false,
@@ -151,7 +90,6 @@ function validateSvg(bytes: Uint8Array): void {
151
90
  .map((name) => ({ entry, name })));
152
91
  if (roots.length !== 1 || svgLocalName(roots[0]!.name) !== 'svg') unsafeSvg('a single <svg> root is required');
153
92
  const root = roots[0]!.entry;
154
- visitSvgEntry(root);
155
93
  const attributes = root[':@'] ?? {};
156
94
  const width = parseSvgLength(attributes.width, 'width');
157
95
  const height = parseSvgLength(attributes.height, 'height');
@@ -312,6 +250,51 @@ class BrowserImagePipeline implements AtlasRasterPipeline {
312
250
  return this;
313
251
  }
314
252
 
253
+ removeAlpha(): this {
254
+ this.raster = this.raster.then((source) => {
255
+ const context = getBrowserContext(source.canvas);
256
+ const image = context.getImageData(0, 0, source.width, source.height);
257
+ for (let index = 3; index < image.data.length; index += 4) image.data[index] = 255;
258
+ context.putImageData(image, 0, 0);
259
+ return source;
260
+ });
261
+ return this;
262
+ }
263
+
264
+ extractChannel(channel: 'alpha'): this {
265
+ if (channel !== 'alpha') throw new Error(`publishBrowser: Unsupported channel "${channel}".`);
266
+ this.raster = this.raster.then((source) => {
267
+ const context = getBrowserContext(source.canvas);
268
+ const image = context.getImageData(0, 0, source.width, source.height);
269
+ for (let index = 0; index < image.data.length; index += 4) {
270
+ const alpha = image.data[index + 3] ?? 0;
271
+ image.data[index] = alpha;
272
+ image.data[index + 1] = alpha;
273
+ image.data[index + 2] = alpha;
274
+ image.data[index + 3] = 255;
275
+ }
276
+ context.putImageData(image, 0, 0);
277
+ return source;
278
+ });
279
+ return this;
280
+ }
281
+
282
+ joinChannel(images: Uint8Array[]): this {
283
+ this.raster = Promise.all([this.raster, ...images.map((image) => this.decode(image))]).then(([source, ...channels]) => {
284
+ const context = getBrowserContext(source.canvas);
285
+ const image = context.getImageData(0, 0, source.width, source.height);
286
+ for (const [channelIndex, channel] of channels.slice(0, 2).entries()) {
287
+ const channelData = getBrowserContext(channel.canvas).getImageData(0, 0, channel.width, channel.height).data;
288
+ for (let index = 0; index < image.data.length; index += 4) {
289
+ image.data[index + channelIndex + 1] = channelData[index] ?? 0;
290
+ }
291
+ }
292
+ context.putImageData(image, 0, 0);
293
+ return source;
294
+ });
295
+ return this;
296
+ }
297
+
315
298
  resize(options: { width: number; height: number; fit?: 'fill' }): this {
316
299
  this.raster = this.raster.then((source) => {
317
300
  const target = createRaster(options.width, options.height);
@@ -1,6 +1,6 @@
1
1
  import type { Document, ILogger, Package } from '@openfairygui/core';
2
2
  import type { AtlasOptions } from '../atlas.js';
3
- import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect } from '../max-rects-compat.js';
3
+ import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect, type CompatPage } from '../max-rects-compat.js';
4
4
  import { MaxRectsPackerCompat } from '../max-rects-packer-compat.js';
5
5
  import type { AtlasRasterBackend } from '../publish/contracts.js';
6
6
  import {
@@ -289,13 +289,13 @@ function packAtlasPages(
289
289
  multipleOfFour: boolean;
290
290
  square: boolean;
291
291
  },
292
- ): ReturnType<MaxRectsPackerCompat['pack']> {
292
+ ): CompatPage[] {
293
293
  const hasDuplicatePadding = inputs.some((input) => {
294
294
  return isImageResource(input.resource) && input.resource.getDuplicatePadding?.() === true;
295
295
  });
296
296
  const packer = new MaxRectsPackerCompat({
297
297
  pot: sizeOverrides?.powerOfTwo ?? options.powerOfTwo,
298
- mof: sizeOverrides?.multipleOfFour ?? !options.powerOfTwo,
298
+ mof: sizeOverrides?.multipleOfFour ?? options.multipleOfFour,
299
299
  padding: options.padding,
300
300
  rotation: options.allowRotation,
301
301
  minWidth: 16,
@@ -433,16 +433,25 @@ async function writeAtlasPageImage(
433
433
  }
434
434
 
435
435
  const outputFile = `${options.outputPath}/${atlasFileName}`;
436
- await encoder({
436
+ const atlasPipeline = encoder({
437
437
  create: {
438
438
  width: page.width,
439
439
  height: page.height,
440
440
  channels: 4 as const,
441
441
  background: { r: 0, g: 0, b: 0, alpha: 0 },
442
442
  },
443
- })
444
- .composite(compositeInputs)
445
- .toFile(outputFile);
443
+ }).composite(compositeInputs);
444
+ if (options.extractAlpha) {
445
+ const atlasBuffer = await atlasPipeline.png().toBuffer();
446
+ await encoder(atlasBuffer).removeAlpha().png().toFile(outputFile);
447
+ const alphaBuffer = await encoder(atlasBuffer).extractChannel('alpha').png().toBuffer();
448
+ await encoder(alphaBuffer)
449
+ .joinChannel([alphaBuffer, alphaBuffer])
450
+ .png()
451
+ .toFile(`${options.outputPath}/${insertFileNameSuffix(atlasFileName, '!a')}`);
452
+ } else {
453
+ await atlasPipeline.toFile(outputFile);
454
+ }
446
455
 
447
456
  logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
448
457
  }
@@ -504,6 +513,9 @@ function resolveDirectOutputAtlasSize(
504
513
  if (options.powerOfTwo) {
505
514
  resolvedWidth = nextPow2(resolvedWidth);
506
515
  resolvedHeight = nextPow2(resolvedHeight);
516
+ } else if (options.multipleOfFour) {
517
+ resolvedWidth = roundUpToMultiple(resolvedWidth, 4);
518
+ resolvedHeight = roundUpToMultiple(resolvedHeight, 4);
507
519
  }
508
520
  return { width: resolvedWidth, height: resolvedHeight };
509
521
  }
@@ -663,11 +675,8 @@ export function sortResourcesByOrder(
663
675
  return ordered;
664
676
  }
665
677
 
666
- function getResourceTextureSetMode(resource: PackInputResource): TextureSetMode {
667
- if (isImageResource(resource)) {
668
- return parseTextureSetMode(resource.getTextureSetMode?.());
669
- }
670
- return parseTextureSetMode(resource.getTextureSetMode?.());
678
+ function getResourceTextureSetMode(resource: PackInputResource, maxAtlasIndex: number): TextureSetMode {
679
+ return parseTextureSetMode(resource.getTextureSetMode?.(), maxAtlasIndex);
671
680
  }
672
681
 
673
682
  function groupStandaloneInputs(
@@ -710,7 +719,7 @@ function groupStandaloneInputs(
710
719
  for (const input of inputs) {
711
720
  const branchName = getInputBranchName(input);
712
721
  const branchOrdinal = branchOrdinalByName.get(branchName) ?? 0;
713
- const mode = getResourceTextureSetMode(input.resource);
722
+ const mode = getResourceTextureSetMode(input.resource, options.maxAtlasIndex ?? 10);
714
723
  if (mode.kind === 'standalone') {
715
724
  const resourceId = getPublishedItemId(input.resource);
716
725
  const key = `${branchName}\u0000${resourceId}`;
package/src/atlas.ts CHANGED
@@ -62,6 +62,9 @@ export interface AtlasOptions {
62
62
 
63
63
  /** Constrain atlas dimensions to powers of two. Default: false. */
64
64
  powerOfTwo?: boolean;
65
+ /** Highest fixed atlas page index accepted from resource textureSetMode. Default: 10. */
66
+ maxAtlasIndex?: number;
67
+ multipleOfFour?: boolean;
65
68
 
66
69
  /** Force square atlas (width === height). Default: false. */
67
70
  square?: boolean;
@@ -151,6 +154,8 @@ const ATLAS_DEFAULTS: Required<
151
154
  allowRotation: true,
152
155
  padding: 1,
153
156
  powerOfTwo: false,
157
+ maxAtlasIndex: 10,
158
+ multipleOfFour: false,
154
159
  square: false,
155
160
  multiPage: true,
156
161
  trimImage: false,
@@ -163,7 +168,9 @@ const ATLAS_DEFAULTS: Required<
163
168
 
164
169
  interface AtlasReferenceItem {
165
170
  icon?: string | null;
171
+ selectedIcon?: string | null;
166
172
  url?: string | null;
173
+ propertyOverrides?: Array<{ value: string }>;
167
174
  }
168
175
 
169
176
  interface GearWithAtlasRefs {
@@ -183,12 +190,14 @@ interface TransitionWithAtlasRefs {
183
190
  }
184
191
 
185
192
  interface ChildWithReferenceUrls extends HasOptionalSrc, HasOptionalUrl {
193
+ getClearOnPublish?(): boolean;
186
194
  getDefaultItem?(): string;
187
195
  getIcon?(): string;
188
196
  getSelectedIcon?(): string;
189
197
  getDropdown?(): string;
190
198
  getSound?(): string;
191
199
  getText?(): string;
200
+ getAutoClearText?(): boolean;
192
201
  getFont?(): string;
193
202
  getInstanceIcon?(): string;
194
203
  getInstanceSelectedIcon?(): string;
@@ -197,7 +206,10 @@ interface ChildWithReferenceUrls extends HasOptionalSrc, HasOptionalUrl {
197
206
  getHeaderRes?(): string;
198
207
  getFooterRes?(): string;
199
208
  getInstanceComboItems?(): Array<{ icon: string | null }>;
209
+ getInstanceAutoClearItems?(): boolean;
200
210
  getListItems?(): AtlasReferenceItem[];
211
+ getAutoClearItems?(): boolean;
212
+ getPropertyOverrides?(): Array<{ value: string }>;
201
213
  listGears?(): GearWithAtlasRefs[];
202
214
  }
203
215
 
@@ -303,7 +315,7 @@ async function resolveEditorCompatibleResourceOrder(
303
315
  const refChild = child as ChildWithReferenceUrls;
304
316
  await addResource(resourceMap.get(refChild.getSrc?.() ?? ''));
305
317
  for (const ref of [
306
- refChild.getUrl?.(),
318
+ refChild.getClearOnPublish?.() ? undefined : refChild.getUrl?.(),
307
319
  refChild.getDefaultItem?.(),
308
320
  refChild.getIcon?.(),
309
321
  refChild.getSelectedIcon?.(),
@@ -319,12 +331,19 @@ async function resolveEditorCompatibleResourceOrder(
319
331
  ]) {
320
332
  await addResourceByLocalUiUrl(ref);
321
333
  }
322
- for (const item of refChild.getInstanceComboItems?.() ?? []) {
334
+ for (const item of refChild.getInstanceAutoClearItems?.() ? [] : (refChild.getInstanceComboItems?.() ?? [])) {
323
335
  await addResourceByLocalUiUrl(item.icon ?? undefined);
324
336
  }
325
- for (const item of refChild.getListItems?.() ?? []) {
337
+ for (const item of refChild.getAutoClearItems?.() ? [] : (refChild.getListItems?.() ?? [])) {
326
338
  await addResourceByLocalUiUrl(item.icon ?? undefined);
339
+ await addResourceByLocalUiUrl(item.selectedIcon ?? undefined);
327
340
  await addResourceByLocalUiUrl(item.url ?? undefined);
341
+ for (const property of item.propertyOverrides ?? []) {
342
+ await addResourceByLocalUiUrl(property.value);
343
+ }
344
+ }
345
+ for (const property of refChild.getPropertyOverrides?.() ?? []) {
346
+ await addResourceByLocalUiUrl(property.value);
328
347
  }
329
348
  for (const gear of refChild.listGears?.() ?? []) {
330
349
  await addGearIconResources(gear);
package/src/codegen.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  UNITY_BINDER_TEMPLATE,
13
13
  UNITY_COMPONENT_TEMPLATE,
14
14
  } from './codegen-templates.js';
15
- import { formatPluginError, type LoadedPlugin } from './plugins/types.js';
15
+ import { formatPluginError, type LoadedPlugin, shouldAbortPluginFailure } from './plugins/types.js';
16
16
  import { dirname, isAbsolutePathLike, trimTrailingSlashes } from './path-utils.js';
17
17
  import type { PublishFileSystem } from './publish/contracts.js';
18
18
  import type { CliCodeGenerationSettings, RootProjectSettings } from './shared-types.js';
@@ -33,12 +33,13 @@ export interface ResolvedPackageCodegenPlan {
33
33
  packageFolderName: string;
34
34
  packageNamespace: string;
35
35
  binderClassName: string;
36
- settings: CliCodeGenerationSettings;
36
+ settings: Required<CliCodeGenerationSettings>;
37
37
  }
38
38
 
39
39
  interface FguiTypescriptVariant {
40
40
  binderMethod: 'setExtension';
41
41
  runtimeNamespace: 'fgui';
42
+ runtimeImport: string;
42
43
  }
43
44
 
44
45
  const FGUI_TYPESCRIPT_RUNTIME_TYPES = new Set([
@@ -65,9 +66,15 @@ const FGUI_TYPESCRIPT_RUNTIME_TYPES = new Set([
65
66
  'Transition',
66
67
  ]);
67
68
 
68
- const SHARED_FGUI_TYPESCRIPT_VARIANT: FguiTypescriptVariant = {
69
+ const LAYABOX_TYPESCRIPT_VARIANT: FguiTypescriptVariant = {
69
70
  binderMethod: 'setExtension',
70
71
  runtimeNamespace: 'fgui',
72
+ runtimeImport: '',
73
+ };
74
+
75
+ const COCOS_CREATOR_TYPESCRIPT_VARIANT: FguiTypescriptVariant = {
76
+ ...LAYABOX_TYPESCRIPT_VARIANT,
77
+ runtimeImport: 'import * as fgui from "fairygui-cc";',
71
78
  };
72
79
 
73
80
  export interface CodegenMember {
@@ -101,16 +108,20 @@ export async function publishCodeGeneration(doc: Document, options: PublishCodeG
101
108
  const settings = resolveCodeGenerationSettings(doc);
102
109
  if (!settings.allowGenCode) return;
103
110
 
104
- const plugins = options.plugins?.filter((plugin) => typeof plugin.plugin.genCode === 'function') ?? [];
111
+ const plugins = options.plugins ?? [];
105
112
  if (plugins.length > 0) {
106
113
  let handled = false;
107
114
  for (const plugin of plugins) {
115
+ const genCode = plugin.plugin.genCode;
116
+ if (!genCode) continue;
108
117
  try {
109
- await plugin.plugin.genCode(doc, settings, options);
118
+ await genCode(doc, settings, options);
110
119
  handled = true;
111
120
  logger.info(`publish: Generated code using plugin "${plugin.name}"`);
112
121
  } catch (error) {
113
- logger.warn(`publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`);
122
+ const message = `publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`;
123
+ if (shouldAbortPluginFailure(plugin)) throw new Error(message);
124
+ logger.warn(message);
114
125
  }
115
126
  }
116
127
  if (handled) {
@@ -172,7 +183,7 @@ export function resolveCodeGenerationSettings(doc: Document): Required<CliCodeGe
172
183
  };
173
184
  }
174
185
 
175
- export function resolvePackageCodegenPlan(pkg: Package, settings: CliCodeGenerationSettings, options: PublishCodeGenerationOptions): ResolvedPackageCodegenPlan | null {
186
+ export function resolvePackageCodegenPlan(pkg: Package, settings: Required<CliCodeGenerationSettings>, options: PublishCodeGenerationOptions): ResolvedPackageCodegenPlan | null {
176
187
  const rawCodePath = (pkg.getCodePath() || settings.codePath || '').trim();
177
188
  if (!rawCodePath) return null;
178
189
 
@@ -198,11 +209,11 @@ function supportsCodeGenerationLane(doc: Document, codeType: string): boolean {
198
209
  return false;
199
210
  }
200
211
 
201
- // Layabox and Cocos Creator currently share the same modern fgui TypeScript output contract.
202
212
  function resolveFguiTypescriptVariant(doc: Document): FguiTypescriptVariant | null {
203
213
  const projectType = doc.getRoot().getProjectType();
204
- if (projectType !== ProjectType.LayaBox && projectType !== ProjectType.CocosCreator) return null;
205
- return SHARED_FGUI_TYPESCRIPT_VARIANT;
214
+ if (projectType === ProjectType.LayaBox) return LAYABOX_TYPESCRIPT_VARIANT;
215
+ if (projectType === ProjectType.CocosCreator) return COCOS_CREATOR_TYPESCRIPT_VARIANT;
216
+ return null;
206
217
  }
207
218
 
208
219
  async function generateUnityCode(
@@ -528,9 +539,10 @@ function renderFguiTypescriptBinder(
528
539
  const bindLines = classes
529
540
  .map((classInfo) => `\t\t${variant.runtimeNamespace}.UIObjectFactory.${variant.binderMethod}(${classInfo.encodedClassName}.URL, ${classInfo.encodedClassName});`)
530
541
  .join('\n');
531
- const importLines = classes
542
+ const classImports = classes
532
543
  .map((classInfo) => `import ${classInfo.encodedClassName} from "./${classInfo.encodedClassName}";`)
533
544
  .join('\n');
545
+ const importLines = [variant.runtimeImport, classImports].filter(Boolean).join('\n');
534
546
 
535
547
  return renderTemplate(FGUI_TYPESCRIPT_BINDER_TEMPLATE, {
536
548
  binderClassName: plan.binderClassName,
@@ -636,6 +648,7 @@ function normalizeTypeName(value: string): string {
636
648
 
637
649
  function collectFguiTypescriptImports(classInfo: CodegenClass, variant: FguiTypescriptVariant): string {
638
650
  const imports = new Set<string>();
651
+ if (variant.runtimeImport) imports.add(variant.runtimeImport);
639
652
  for (const member of classInfo.members) {
640
653
  if (member.ignored) continue;
641
654
  const translated = translateFguiTypescriptType(member.type, variant);
@@ -15,6 +15,8 @@ export interface PluginManifest {
15
15
  };
16
16
  icon?: string;
17
17
  main: string;
18
+ required?: boolean;
19
+ failureMode?: 'abort' | 'warn';
18
20
  }
19
21
 
20
22
  export interface ICodeWriterConfig {
@@ -47,6 +49,7 @@ export interface Plugin {
47
49
  export interface LoadedPlugin {
48
50
  name: string;
49
51
  plugin: Plugin;
52
+ failureMode?: 'abort' | 'warn';
50
53
  }
51
54
 
52
55
  export type PluginModule = Plugin & { default?: Plugin };
@@ -54,3 +57,7 @@ export type PluginModule = Plugin & { default?: Plugin };
54
57
  export function formatPluginError(error: unknown): string {
55
58
  return error instanceof Error ? error.message : String(error);
56
59
  }
60
+
61
+ export function shouldAbortPluginFailure(plugin: LoadedPlugin): boolean {
62
+ return plugin.failureMode !== 'warn';
63
+ }
@@ -65,6 +65,9 @@ export type AtlasRasterInput =
65
65
  */
66
66
  export interface AtlasRasterPipeline {
67
67
  ensureAlpha(): AtlasRasterPipeline;
68
+ removeAlpha(): AtlasRasterPipeline;
69
+ extractChannel(channel: 'alpha'): AtlasRasterPipeline;
70
+ joinChannel(images: Uint8Array[]): AtlasRasterPipeline;
68
71
  resize(options: { width: number; height: number; fit?: 'fill' }): AtlasRasterPipeline;
69
72
  raw(): AtlasRasterPipeline;
70
73
  extract(options: { left: number; top: number; width: number; height: number }): AtlasRasterPipeline;
@@ -10,6 +10,7 @@ import {
10
10
  isMiscResource,
11
11
  isSkeletonResource,
12
12
  isSoundResource,
13
+ isSwfResource,
13
14
  resolveGenericResourcePath,
14
15
  resolveImageFileName,
15
16
  resolveImagePath,
@@ -71,7 +72,7 @@ export async function exportPackageExternalResources(
71
72
  if (!basePath || !readFileRaw) {
72
73
  const hasPublishedExternal = pkg.listResources().some((resource) => {
73
74
  return (
74
- ((isMiscResource(resource) || isSkeletonResource(resource)) &&
75
+ ((isMiscResource(resource) || isSwfResource(resource) || isSkeletonResource(resource)) &&
75
76
  exportedResourceIds.has(resource.getId())) ||
76
77
  skeletonDependencyImageIds.has(resource.getId())
77
78
  );
@@ -86,20 +87,25 @@ export async function exportPackageExternalResources(
86
87
 
87
88
  for (const resource of pkg.listResources()) {
88
89
  const resourceId = resource.getId();
89
- const isSkeletonExternal =
90
- exportedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
90
+ const isExternal =
91
+ exportedResourceIds.has(resourceId) &&
92
+ (isMiscResource(resource) || isSwfResource(resource) || isSkeletonResource(resource));
91
93
  const isSkeletonImageDependency = skeletonDependencyImageIds.has(resourceId) && isImageResource(resource);
92
- if (!isSkeletonExternal && !isSkeletonImageDependency) continue;
94
+ if (!isExternal && !isSkeletonImageDependency) continue;
93
95
 
94
96
  let sourcePath: string;
95
97
  let targetName: string;
96
98
  if (isSkeletonImageDependency) {
97
99
  sourcePath = resolveImagePath(resource, pkg, basePath);
98
100
  targetName = resolveImageFileName(resource);
99
- } else if (isMiscResource(resource) || isSkeletonResource(resource)) {
101
+ } else if (isMiscResource(resource) || isSwfResource(resource) || isSkeletonResource(resource)) {
100
102
  sourcePath = resolveGenericResourcePath(resource, pkg, basePath);
101
- targetName =
103
+ const publishedFile =
102
104
  ((resource.getExtras() as PublishFileExtras | undefined) ?? {})._publishedFile ?? resource.getFile();
105
+ targetName =
106
+ isMiscResource(resource) || isSwfResource(resource)
107
+ ? `${pkg.getPublishName() || pkg.getName()}_${publishedFile}`
108
+ : publishedFile;
103
109
  } else {
104
110
  continue;
105
111
  }
@@ -1,8 +1,8 @@
1
1
  import { type Document, ProjectType } from '@openfairygui/core';
2
2
  import type { AtlasOptions } from '../atlas.js';
3
3
  import type { LoadedPlugin } from '../plugins/types.js';
4
- import type { AtlasRasterBackend, PublishFileSystem } from './contracts.js';
5
4
  import type { CliPublishSettings, RootProjectSettings } from '../shared-types.js';
5
+ import type { AtlasRasterBackend, PublishFileSystem } from './contracts.js';
6
6
 
7
7
  export interface PublishOptions {
8
8
  /**
@@ -80,6 +80,8 @@ export interface ResolvedPublishAtlasOptions
80
80
  | 'allowRotation'
81
81
  | 'padding'
82
82
  | 'powerOfTwo'
83
+ | 'maxAtlasIndex'
84
+ | 'multipleOfFour'
83
85
  | 'square'
84
86
  | 'multiPage'
85
87
  | 'trimImage'
@@ -87,6 +89,8 @@ export interface ResolvedPublishAtlasOptions
87
89
  > {}
88
90
 
89
91
  export interface ResolvePublishOptionsOverrides {
92
+ /** Select a target profile between direct overrides and persisted project settings. */
93
+ targetProjectType?: number;
90
94
  compressed?: boolean;
91
95
  fileExtension?: string;
92
96
  packages?: string[];
@@ -150,21 +154,31 @@ export function resolvePublishOptions(
150
154
  const settings = (root.getSettings?.() ?? {}) as RootProjectSettings;
151
155
  const publishSettings: CliPublishSettings = settings.publish ?? {};
152
156
  const atlasSetting = publishSettings.atlasSetting ?? {};
153
- const projectType = root.getProjectType();
157
+ const projectType = overrides.targetProjectType ?? root.getProjectType();
158
+ const explicitLayaboxTarget = overrides.targetProjectType === ProjectType.LayaBox;
154
159
 
155
- const fileExtension = overrides.fileExtension ?? resolveDefaultPublishFileExtension(projectType, publishSettings);
160
+ const fileExtension =
161
+ overrides.fileExtension ??
162
+ (explicitLayaboxTarget ? 'fui' : resolveDefaultPublishFileExtension(projectType, publishSettings));
156
163
 
157
- let compressed = overrides.compressed ?? publishSettings.compressDesc ?? false;
158
- if (projectType === UNITY_PROJECT_TYPE) {
159
- compressed = overrides.compressed ?? false;
164
+ const runtimeRejectsCompression =
165
+ projectType === UNITY_PROJECT_TYPE || projectType === COCOS_CREATOR_PROJECT_TYPE;
166
+ if (runtimeRejectsCompression && overrides.compressed === true) {
167
+ throw new Error('publish: The selected target runtime does not support compressed package data.');
160
168
  }
169
+ const compressed = runtimeRejectsCompression
170
+ ? false
171
+ : (overrides.compressed ?? publishSettings.compressDesc ?? false);
161
172
 
162
173
  const atlasOptions: ResolvedPublishAtlasOptions = {
163
174
  maxSize: overrides.atlas?.maxSize ?? atlasSetting.maxSize ?? 2048,
164
175
  fast: overrides.atlas?.fast ?? atlasSetting.fast ?? true,
165
- allowRotation: overrides.atlas?.allowRotation ?? atlasSetting.allowRotation ?? false,
176
+ allowRotation:
177
+ overrides.atlas?.allowRotation ?? (explicitLayaboxTarget ? false : (atlasSetting.allowRotation ?? false)),
166
178
  padding: overrides.atlas?.padding ?? atlasSetting.padding ?? 2,
167
179
  powerOfTwo: overrides.atlas?.powerOfTwo ?? atlasSetting.sizeOption === 'pot',
180
+ maxAtlasIndex: overrides.atlas?.maxAtlasIndex ?? 10,
181
+ multipleOfFour: overrides.atlas?.multipleOfFour ?? atlasSetting.sizeOption === 'mof',
168
182
  square: overrides.atlas?.square ?? atlasSetting.forceSquare ?? false,
169
183
  multiPage: overrides.atlas?.multiPage ?? atlasSetting.paging ?? true,
170
184
  trimImage: overrides.atlas?.trimImage ?? atlasSetting.trimImage ?? false,