@openfairygui/functions 0.2.0-alpha.13 → 0.2.0-alpha.15

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.
@@ -751,7 +751,8 @@ const ATLAS_DEFAULTS = {
751
751
  preserveInputOrderOnTie: false,
752
752
  directSingleImageOutput: false,
753
753
  extractAlpha: false,
754
- separatedAtlasForBranch: false
754
+ separatedAtlasForBranch: false,
755
+ strictOutput: false
755
756
  };
756
757
  function getPublishedItemId(resource) {
757
758
  return (resource.getExtras() ?? {})._publishedId ?? resource.getId();
@@ -908,8 +909,9 @@ function atlas(_options = {}) {
908
909
  const packageFilter = options.packages ? new Set(options.packages) : null;
909
910
  for (const pkg of root.listPackages()) {
910
911
  if (packageFilter && !packageFilter.has(pkg.getName())) continue;
911
- const selectedPublishIds = new Set((pkg.getExtras() ?? {}).publishedResourceIds ?? []);
912
- const allResources = selectedPublishIds.size > 0 ? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId())) : pkg.listResources();
912
+ const publishedResourceIds = pkg.getExtras()?.publishedResourceIds;
913
+ const selectedPublishIds = new Set(publishedResourceIds);
914
+ const allResources = publishedResourceIds !== void 0 && (options.strictOutput || selectedPublishIds.size > 0) ? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId())) : pkg.listResources();
913
915
  const skeletonDependencyImageIds = getSelectedSkeletonDependencyImageIds(allResources);
914
916
  const orderedResources = await resolveEditorCompatibleResourceOrder(pkg, allResources, options);
915
917
  const orderedAllResources = sortResourcesByOrder(allResources, new Map(orderedResources.map((resource, index) => [resource.getId(), index])), new Map(allResources.map((resource, index) => [resource.getId(), index])));
@@ -998,6 +1000,7 @@ function atlas(_options = {}) {
998
1000
  await _collectFontTexture(doc, res, pkg, options);
999
1001
  }
1000
1002
  if (inputs.length === 0) continue;
1003
+ if (options.strictOutput && (!encoder || !options.basePath || !options.outputPath)) throw new Error(`atlas: Package "${pkg.getName()}" requires encoder, basePath, and outputPath for complete raster output.`);
1001
1004
  let totalPageCount = 0;
1002
1005
  let usedDirectOutput = false;
1003
1006
  const { autoInputs, fixedPageGroups, standaloneGroups, reservedPageIndexes } = groupStandaloneInputs(doc, inputs, options);
@@ -1095,7 +1098,7 @@ function reserveAutoPageStart(branchPageOffsets, branchOrdinal, reservedPageInde
1095
1098
  async function emitPagedAtlasGroup(doc, pkg, allResources, inputs, context) {
1096
1099
  if (inputs.length === 0) return 0;
1097
1100
  const pages = packAtlasPages(inputs, context.options, context.forceSinglePage === true);
1098
- if (pages.length === 0) return 0;
1101
+ assertPackedInputCoverage(pages, inputs.length, `package "${pkg.getName()}"`);
1099
1102
  for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
1100
1103
  const page = pages[pageOffset];
1101
1104
  const pageIndex = context.pageStart + pageOffset;
@@ -1121,7 +1124,7 @@ async function emitStandaloneAtlasGroup(doc, pkg, group, context) {
1121
1124
  multipleOfFour: true,
1122
1125
  square: false
1123
1126
  } : void 0);
1124
- if (pages.length === 0) return 0;
1127
+ assertPackedInputCoverage(pages, group.inputs.length, `standalone texture in package "${pkg.getName()}"`);
1125
1128
  for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
1126
1129
  const page = pages[pageOffset];
1127
1130
  const baseFileName = resolveStandaloneAtlasOutputFileName(pkg, group.resource, group.branchName);
@@ -1164,6 +1167,12 @@ function packAtlasPages(inputs, options, forceSinglePage, sizeOverrides) {
1164
1167
  preserveInputOrderOnTie: options.preserveInputOrderOnTie
1165
1168
  }).pack(inputs.map((input, index) => inputToCompatRect(input, index))) ?? [];
1166
1169
  }
1170
+ function assertPackedInputCoverage(pages, inputCount, label) {
1171
+ const packedIndexes = /* @__PURE__ */ new Set();
1172
+ for (const page of pages) for (const outputRect of page.outputRects) packedIndexes.add(outputRect.index);
1173
+ const hasEveryInput = Array.from({ length: inputCount }, (_, index) => packedIndexes.has(index)).every(Boolean);
1174
+ if (packedIndexes.size !== inputCount || !hasEveryInput) throw new Error(`atlas: Could not pack every input for ${label}.`);
1175
+ }
1167
1176
  function attachSpritesToAtlas(doc, allResources, inputs, outputRects, atlasNode) {
1168
1177
  for (const packedRect of outputRects) {
1169
1178
  const input = inputs[packedRect.index];
@@ -1223,7 +1232,9 @@ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, op
1223
1232
  } else if (input.rasterizedBuffer) imageBuffer = input.rasterizedBuffer;
1224
1233
  else {
1225
1234
  if (!isImageResource$1(input.resource)) {
1226
- logger.warn(`atlas: Non-image input "${input.id}" is missing inline buffer, skipping compositing.`);
1235
+ const message = `atlas: Non-image input "${input.id}" is missing inline buffer.`;
1236
+ if (options.strictOutput) throw new Error(message);
1237
+ logger.warn(`${message} Skipping compositing.`);
1227
1238
  continue;
1228
1239
  }
1229
1240
  imageBuffer = await encoder(_resolveImagePath(input.resource, pkg, options.basePath)).toBuffer();
@@ -1235,7 +1246,9 @@ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, op
1235
1246
  top: packedRect.y
1236
1247
  });
1237
1248
  } catch {
1238
- logger.warn(`atlas: Could not read image "${input.id}" for compositing.`);
1249
+ const message = `atlas: Could not read image "${input.id}" for compositing.`;
1250
+ if (options.strictOutput) throw new Error(message);
1251
+ logger.warn(message);
1239
1252
  }
1240
1253
  }
1241
1254
  const outputFile = `${options.outputPath}/${atlasFileName}`;
@@ -1352,7 +1365,9 @@ async function emitDirectImageOutput(doc, pkg, input, encoder, options, logger,
1352
1365
  }]).png().toFile(outputFile);
1353
1366
  }
1354
1367
  } catch {
1355
- logger.warn(`atlas: Could not write direct-output atlas "${atlasFileName}".`);
1368
+ const message = `atlas: Could not write direct-output atlas "${atlasFileName}".`;
1369
+ if (options.strictOutput) throw new Error(message);
1370
+ logger.warn(message);
1356
1371
  }
1357
1372
  }
1358
1373
  function getInputBranchName(input) {
@@ -1578,6 +1593,7 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
1578
1593
  sourceHasAlpha = true;
1579
1594
  }
1580
1595
  } catch {
1596
+ if (options.strictOutput) throw new Error(`atlas: Could not read image "${filePath}".`);
1581
1597
  if (origW === 0 || origH === 0) {
1582
1598
  logger.warn(`atlas: Could not read image "${filePath}", skipping.`);
1583
1599
  return;
@@ -1616,7 +1632,11 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
1616
1632
  }
1617
1633
  /** Collect MovieClip frame textures from a .jta file into the inputs array. */
1618
1634
  async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, options, logger) {
1619
- if (!options.basePath || !options.readFileRaw) return;
1635
+ if (!options.basePath || !options.readFileRaw) {
1636
+ if (options.strictOutput) throw new Error(`atlas: MovieClip "${resource.getId()}" requires basePath and readFileRaw for complete raster output.`);
1637
+ return;
1638
+ }
1639
+ if (!encoder && options.strictOutput) throw new Error(`atlas: MovieClip "${resource.getId()}" requires an encoder for complete raster output.`);
1620
1640
  const mcId = resource.getId();
1621
1641
  const mcName = resource.getName() + ".jta";
1622
1642
  const mcPath = resource.getPath() ?? "/";
@@ -1639,7 +1659,7 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
1639
1659
  const exportFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
1640
1660
  if (exportFrameIndex === void 0) continue;
1641
1661
  const itemId = `${mcId}_${exportFrameIndex}`;
1642
- const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder);
1662
+ const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder, options.strictOutput);
1643
1663
  if (!input) continue;
1644
1664
  inputs.push(input);
1645
1665
  spriteIdByTextureIndex.set(textureIndex, itemId);
@@ -1653,7 +1673,7 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
1653
1673
  }
1654
1674
  } else for (let frameIndex = 0; frameIndex < jta.frames.length; frameIndex += 1) {
1655
1675
  const itemId = `${mcId}_${frameIndex}`;
1656
- const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder);
1676
+ const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder, options.strictOutput);
1657
1677
  if (!input) continue;
1658
1678
  inputs.push(input);
1659
1679
  const frame = doc.createMovieFrame(itemId);
@@ -1665,10 +1685,12 @@ async function _collectMovieClipFrames(doc, resource, pkg, inputs, encoder, opti
1665
1685
  resource.setHeight(jta.meta?.height ?? 0);
1666
1686
  }
1667
1687
  } catch {
1668
- logger.warn(`atlas: Could not parse MovieClip "${filePath}", skipping frames.`);
1688
+ const message = `atlas: Could not parse MovieClip "${filePath}".`;
1689
+ if (options.strictOutput) throw new Error(message);
1690
+ logger.warn(`${message} Skipping frames.`);
1669
1691
  }
1670
1692
  }
1671
- async function _createMovieClipFrameInput(buffer, itemId, resource, encoder) {
1693
+ async function _createMovieClipFrameInput(buffer, itemId, resource, encoder, strictOutput) {
1672
1694
  if (!encoder || buffer.length === 0) return null;
1673
1695
  try {
1674
1696
  const meta = await encoder(buffer).metadata();
@@ -1688,6 +1710,7 @@ async function _createMovieClipFrameInput(buffer, itemId, resource, encoder) {
1688
1710
  sourceKind: "movieclip-frame"
1689
1711
  };
1690
1712
  } catch {
1713
+ if (strictOutput) throw new Error(`atlas: Could not decode MovieClip frame "${itemId}".`);
1691
1714
  return null;
1692
1715
  }
1693
1716
  }
@@ -3000,13 +3023,13 @@ function getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds) {
3000
3023
  }
3001
3024
  return imageIds;
3002
3025
  }
3003
- async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw, logger) {
3026
+ async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw) {
3004
3027
  const publishedResourceIds = getAnnotatedPublishedResourceIds(pkg);
3005
3028
  if (publishedResourceIds.size === 0) return;
3006
3029
  if (!basePath || !readFileRaw) {
3007
3030
  if (pkg.listResources().some((resource) => {
3008
3031
  return isSoundResource(resource) && publishedResourceIds.has(resource.getId());
3009
- })) logger.warn(`publish: Sound resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`);
3032
+ })) throw new Error(`publish: Sound resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`);
3010
3033
  return;
3011
3034
  }
3012
3035
  for (const resource of pkg.listResources()) {
@@ -3019,18 +3042,18 @@ async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw, lo
3019
3042
  const data = await readFileRaw(sourcePath);
3020
3043
  await fs.writeFileRaw(targetPath, data);
3021
3044
  } catch {
3022
- logger.warn(`publish: Could not export sound "${resource.getId()}" from package "${pkg.getName()}".`);
3045
+ throw new Error(`publish: Could not export sound "${resource.getId()}" from package "${pkg.getName()}".`);
3023
3046
  }
3024
3047
  }
3025
3048
  }
3026
- async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw, logger) {
3049
+ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw) {
3027
3050
  const exportedResourceIds = getAnnotatedExportedResourceIds(pkg);
3028
3051
  const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, exportedResourceIds);
3029
3052
  if (exportedResourceIds.size === 0) return;
3030
3053
  if (!basePath || !readFileRaw) {
3031
3054
  if (pkg.listResources().some((resource) => {
3032
3055
  return (isMiscResource(resource) || isSkeletonResource(resource)) && exportedResourceIds.has(resource.getId()) || skeletonDependencyImageIds.has(resource.getId());
3033
- })) logger.warn(`publish: External resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`);
3056
+ })) throw new Error(`publish: External resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`);
3034
3057
  return;
3035
3058
  }
3036
3059
  for (const resource of pkg.listResources()) {
@@ -3052,7 +3075,7 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
3052
3075
  const data = await readFileRaw(sourcePath);
3053
3076
  await fs.writeFileRaw(targetPath, data);
3054
3077
  } catch {
3055
- logger.warn(`publish: Could not export external resource "${resource.getId()}" from package "${pkg.getName()}".`);
3078
+ throw new Error(`publish: Could not export external resource "${resource.getId()}" from package "${pkg.getName()}".`);
3056
3079
  }
3057
3080
  }
3058
3081
  }
@@ -3068,18 +3091,17 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
3068
3091
  * `publishNode()` or `publishBrowser()` through their dedicated entries.
3069
3092
  *
3070
3093
  * ```ts
3071
- * import sharp from 'sharp';
3072
- * const io = new NodeIO();
3073
- * const doc = await io.readProject('./project.fairy');
3094
+ * import { NodeIO } from '@openfairygui/core/node';
3095
+ * import { publishNode } from '@openfairygui/functions/node';
3096
+ * const doc = await new NodeIO().readProject('./project.fairy');
3074
3097
  *
3075
- * await doc.transform(publish({
3098
+ * await publishNode({
3099
+ * document: doc,
3076
3100
  * output: './release/',
3077
3101
  * compressed: true,
3078
- * encoder: sharp,
3079
- * basePath: './assets/',
3102
+ * assetsPath: './assets/',
3080
3103
  * fileExtension: 'bytes',
3081
- * fs: io.createFileSystem(),
3082
- * }));
3104
+ * });
3083
3105
  * ```
3084
3106
  */
3085
3107
  function publish(options) {
@@ -3147,6 +3169,12 @@ function publish(options) {
3147
3169
  }
3148
3170
  });
3149
3171
  const publishPackage = async (plan, writerFs, packageIndex) => {
3172
+ if (options.fs && !plan.outputDir) throw new Error("publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.");
3173
+ if (options.fs) {
3174
+ await options.fs.mkdir(plan.outputDir);
3175
+ await exportPackageSounds(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw);
3176
+ await exportPackageExternalResources(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw);
3177
+ }
3150
3178
  const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(plan.fileExtension);
3151
3179
  await atlas({
3152
3180
  ...plan.atlas,
@@ -3157,20 +3185,17 @@ function publish(options) {
3157
3185
  outputPath: options.fs ? plan.outputDir : void 0,
3158
3186
  mkdir: options.fs ? options.fs.mkdir : void 0,
3159
3187
  readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
3188
+ strictOutput: options.fs !== void 0,
3160
3189
  packages: [plan.pkg.getName()],
3161
3190
  ...atlasRuntimeOptions
3162
3191
  })(doc);
3163
3192
  if (!options.fs) return;
3164
- if (!plan.outputDir) throw new Error("publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.");
3165
- await options.fs.mkdir(plan.outputDir);
3166
3193
  const filePath = options.fs.join(plan.outputDir, plan.fileName);
3167
3194
  const bwOptions = {
3168
3195
  compressed: plan.compressed,
3169
3196
  packageIndex
3170
3197
  };
3171
3198
  await new BinaryWriter(writerFs).write(doc, filePath, bwOptions);
3172
- await exportPackageSounds(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
3173
- await exportPackageExternalResources(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
3174
3199
  logger.info(`publish: Written ${plan.fileName}`);
3175
3200
  };
3176
3201
  const root = doc.getRoot();
@@ -3203,7 +3228,9 @@ function publish(options) {
3203
3228
  }
3204
3229
  const plans = allPackages.map((pkg) => resolvePackagePublishPlan(pkg, resolved, projectBasePath));
3205
3230
  if (!options.fs) {
3206
- logger.info(`publish: No fs provided — layout computed for ${allPackages.length} package(s), skipping file output.`);
3231
+ const outputPlan = plans.find((plan) => !!plan.outputDir);
3232
+ if (outputPlan) throw new Error(`publish: Output for package "${outputPlan.pkg.getName()}" requires a filesystem. Omit output and publish paths to run a layout-only transform.`);
3233
+ logger.info(`publish: Layout computed for ${allPackages.length} package(s); no output directory was requested.`);
3207
3234
  const noopWriterFs = toBinaryWriterFileSystem(createNoopPublishFs());
3208
3235
  for (const plan of plans) await publishPackage(plan, noopWriterFs, allDocPackages.indexOf(plan.pkg));
3209
3236
  await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
package/dist/web.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_publish = require("./publish-CtCXsMdj.cjs");
2
+ const require_publish = require("./publish-D7268_u4.cjs");
3
3
  let _openfairygui_core = require("@openfairygui/core");
4
4
  //#region src/adapters/web/publish.ts
5
5
  function getBrowserContext(canvas) {
package/dist/web.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as publish } from "./publish-Bl-GK9kt.js";
1
+ import { t as publish } from "./publish-DsPK0SJ1.js";
2
2
  import { ProjectType } from "@openfairygui/core";
3
3
  //#region src/adapters/web/publish.ts
4
4
  function getBrowserContext(canvas) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/functions",
3
- "version": "0.2.0-alpha.13",
3
+ "version": "0.2.0-alpha.15",
4
4
  "description": "FairyGUI Headless Authoring SDK — composable transform functions.",
5
5
  "author": "OpenFairyGUI Contributors",
6
6
  "license": "MIT",
@@ -74,7 +74,7 @@
74
74
  ],
75
75
  "dependencies": {
76
76
  "jiti": "^2.6.1",
77
- "@openfairygui/core": "0.2.0-alpha.13"
77
+ "@openfairygui/core": "0.2.0-alpha.15"
78
78
  },
79
79
  "devDependencies": {
80
80
  "ava": "^7.0.0",
@@ -1,8 +1,8 @@
1
1
  import type { Document } from '@openfairygui/core';
2
2
  import { resolveProjectBasePath } from '../../codegen.js';
3
- import { publish, type PublishOptions } from '../../publish.js';
4
- import type { AtlasRasterBackend, PublishFileSystem } from '../../publish/contracts.js';
5
3
  import type { LoadedPlugin } from '../../plugins/types.js';
4
+ import type { AtlasRasterBackend, PublishFileSystem } from '../../publish/contracts.js';
5
+ import { type PublishOptions, publish } from '../../publish.js';
6
6
  import { loadPlugins } from './plugins.js';
7
7
 
8
8
  const importNative = new Function('id', 'return import(id)') as <T>(id: string) => Promise<T>;
@@ -68,8 +68,9 @@ async function resolveNodeAssetsPath(document: Document, assetsPath: string | un
68
68
 
69
69
  async function loadSharpBackend(): Promise<AtlasRasterBackend | undefined> {
70
70
  try {
71
- const sharp = await importNative<typeof import('sharp')>('sharp');
72
- return (sharp.default ?? sharp) as unknown as AtlasRasterBackend;
71
+ const loaded = await importNative<typeof import('sharp')>('sharp');
72
+ const sharp = loaded as unknown as { default?: AtlasRasterBackend };
73
+ return sharp.default ?? (loaded as unknown as AtlasRasterBackend);
73
74
  } catch {
74
75
  return undefined;
75
76
  }
@@ -110,7 +111,7 @@ export async function publishNode(options: PublishNodeOptions): Promise<void> {
110
111
  ]);
111
112
 
112
113
  if (!encoder) {
113
- document.getLogger().warn('publish: Sharp is unavailable; atlas layout will be generated without PNG output.');
114
+ throw new Error('publishNode: Sharp is required for a complete publish. Install sharp or provide an encoder.');
114
115
  }
115
116
 
116
117
  await document.transform(
@@ -63,6 +63,7 @@ type BrowserCanvas = OffscreenCanvas | HTMLCanvasElement;
63
63
  interface BrowserContext {
64
64
  clearRect(x: number, y: number, width: number, height: number): void;
65
65
  drawImage(image: CanvasImageSource, dx: number, dy: number): void;
66
+ drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
66
67
  drawImage(
67
68
  image: CanvasImageSource,
68
69
  sx: number,
package/src/atlas.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import {
2
- GearType,
3
- TransitionActionType,
4
2
  type Component,
5
3
  type Document,
6
4
  type DragonBonesResource,
7
5
  type FontResource,
6
+ GearType,
8
7
  type ILogger,
9
8
  type ImageResource,
10
9
  type MovieClipResource,
11
10
  type Package,
12
11
  type SpineResource,
13
12
  type Transform,
13
+ TransitionActionType,
14
14
  } from '@openfairygui/core';
15
15
  import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect } from './max-rects-compat.js';
16
16
  import { MaxRectsPackerCompat } from './max-rects-packer-compat.js';
@@ -76,6 +76,14 @@ export interface AtlasOptions {
76
76
  */
77
77
  outputPath?: string;
78
78
 
79
+ /**
80
+ * Require a complete raster artifact when there are packable inputs.
81
+ * This is used by publish() so a runtime package cannot contain atlas
82
+ * references without the matching PNG output.
83
+ * @internal
84
+ */
85
+ strictOutput?: boolean;
86
+
79
87
  /**
80
88
  * Optional mkdir function to ensure output directory exists.
81
89
  * If not provided, the outputPath directory must already exist.
@@ -129,6 +137,7 @@ const ATLAS_DEFAULTS: Required<
129
137
  directSingleImageOutput: false,
130
138
  extractAlpha: false,
131
139
  separatedAtlasForBranch: false,
140
+ strictOutput: false,
132
141
  };
133
142
 
134
143
  /** Trim info for a single image. */
@@ -420,12 +429,13 @@ export function atlas(_options: AtlasOptions = {}): Transform {
420
429
 
421
430
  for (const pkg of root.listPackages()) {
422
431
  if (packageFilter && !packageFilter.has(pkg.getName())) continue;
423
- // Respect publish-selected resources when publish() precomputes a merged branch view.
424
- const selectedPublishIds = new Set(
425
- ((pkg.getExtras() as PackageAtlasExtras | undefined) ?? {}).publishedResourceIds ?? [],
426
- );
432
+ // Publish annotations select merged resources; only strict output treats an empty selection as explicit.
433
+ const publishedResourceIds = (pkg.getExtras() as PackageAtlasExtras | undefined)?.publishedResourceIds;
434
+ const selectedPublishIds = new Set(publishedResourceIds);
435
+ const hasPublishSelection =
436
+ publishedResourceIds !== undefined && (options.strictOutput || selectedPublishIds.size > 0);
427
437
  const allResources =
428
- selectedPublishIds.size > 0
438
+ hasPublishSelection
429
439
  ? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId()))
430
440
  : pkg.listResources();
431
441
  const skeletonDependencyImageIds = getSelectedSkeletonDependencyImageIds(allResources);
@@ -568,6 +578,11 @@ export function atlas(_options: AtlasOptions = {}): Transform {
568
578
  }
569
579
 
570
580
  if (inputs.length === 0) continue;
581
+ if (options.strictOutput && (!encoder || !options.basePath || !options.outputPath)) {
582
+ throw new Error(
583
+ `atlas: Package "${pkg.getName()}" requires encoder, basePath, and outputPath for complete raster output.`,
584
+ );
585
+ }
571
586
  let totalPageCount = 0;
572
587
  let usedDirectOutput = false;
573
588
  const { autoInputs, fixedPageGroups, standaloneGroups, reservedPageIndexes } = groupStandaloneInputs(
@@ -733,7 +748,7 @@ async function emitPagedAtlasGroup(
733
748
  ): Promise<number> {
734
749
  if (inputs.length === 0) return 0;
735
750
  const pages = packAtlasPages(inputs, context.options, context.forceSinglePage === true);
736
- if (pages.length === 0) return 0;
751
+ assertPackedInputCoverage(pages, inputs.length, `package "${pkg.getName()}"`);
737
752
 
738
753
  for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
739
754
  const page = pages[pageOffset];
@@ -780,9 +795,9 @@ async function emitStandaloneAtlasGroup(
780
795
  ? { powerOfTwo: false, multipleOfFour: false, square: false }
781
796
  : group.sizeMode === 'multipleOf4'
782
797
  ? { powerOfTwo: false, multipleOfFour: true, square: false }
783
- : undefined,
798
+ : undefined,
784
799
  );
785
- if (pages.length === 0) return 0;
800
+ assertPackedInputCoverage(pages, group.inputs.length, `standalone texture in package "${pkg.getName()}"`);
786
801
 
787
802
  for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
788
803
  const page = pages[pageOffset];
@@ -844,6 +859,21 @@ function packAtlasPages(
844
859
  return packer.pack(inputs.map((input, index) => inputToCompatRect(input, index))) ?? [];
845
860
  }
846
861
 
862
+ function assertPackedInputCoverage(
863
+ pages: Array<{ outputRects: Array<{ index: number }> }>,
864
+ inputCount: number,
865
+ label: string,
866
+ ): void {
867
+ const packedIndexes = new Set<number>();
868
+ for (const page of pages) {
869
+ for (const outputRect of page.outputRects) packedIndexes.add(outputRect.index);
870
+ }
871
+ const hasEveryInput = Array.from({ length: inputCount }, (_, index) => packedIndexes.has(index)).every(Boolean);
872
+ if (packedIndexes.size !== inputCount || !hasEveryInput) {
873
+ throw new Error(`atlas: Could not pack every input for ${label}.`);
874
+ }
875
+ }
876
+
847
877
  function attachSpritesToAtlas(
848
878
  doc: Document,
849
879
  allResources: PackageResource[],
@@ -928,7 +958,9 @@ async function writeAtlasPageImage(
928
958
  imageBuffer = input.rasterizedBuffer;
929
959
  } else {
930
960
  if (!isImageResource(input.resource)) {
931
- logger.warn(`atlas: Non-image input "${input.id}" is missing inline buffer, skipping compositing.`);
961
+ const message = `atlas: Non-image input "${input.id}" is missing inline buffer.`;
962
+ if (options.strictOutput) throw new Error(message);
963
+ logger.warn(`${message} Skipping compositing.`);
932
964
  continue;
933
965
  }
934
966
  const filePath = _resolveImagePath(input.resource, pkg, options.basePath!);
@@ -941,7 +973,9 @@ async function writeAtlasPageImage(
941
973
  top: packedRect.y,
942
974
  });
943
975
  } catch {
944
- logger.warn(`atlas: Could not read image "${input.id}" for compositing.`);
976
+ const message = `atlas: Could not read image "${input.id}" for compositing.`;
977
+ if (options.strictOutput) throw new Error(message);
978
+ logger.warn(message);
945
979
  }
946
980
  }
947
981
 
@@ -1080,7 +1114,9 @@ async function emitDirectImageOutput(
1080
1114
  .toFile(outputFile);
1081
1115
  }
1082
1116
  } catch {
1083
- logger.warn(`atlas: Could not write direct-output atlas "${atlasFileName}".`);
1117
+ const message = `atlas: Could not write direct-output atlas "${atlasFileName}".`;
1118
+ if (options.strictOutput) throw new Error(message);
1119
+ logger.warn(message);
1084
1120
  }
1085
1121
  }
1086
1122
 
@@ -1472,6 +1508,9 @@ async function _collectImage(
1472
1508
  sourceHasAlpha = true;
1473
1509
  }
1474
1510
  } catch {
1511
+ if (options.strictOutput) {
1512
+ throw new Error(`atlas: Could not read image "${filePath}".`);
1513
+ }
1475
1514
  if (origW === 0 || origH === 0) {
1476
1515
  logger.warn(`atlas: Could not read image "${filePath}", skipping.`);
1477
1516
  return;
@@ -1526,7 +1565,15 @@ async function _collectMovieClipFrames(
1526
1565
  options: AtlasOptions,
1527
1566
  logger: ILogger,
1528
1567
  ): Promise<void> {
1529
- if (!options.basePath || !options.readFileRaw) return;
1568
+ if (!options.basePath || !options.readFileRaw) {
1569
+ if (options.strictOutput) {
1570
+ throw new Error(`atlas: MovieClip "${resource.getId()}" requires basePath and readFileRaw for complete raster output.`);
1571
+ }
1572
+ return;
1573
+ }
1574
+ if (!encoder && options.strictOutput) {
1575
+ throw new Error(`atlas: MovieClip "${resource.getId()}" requires an encoder for complete raster output.`);
1576
+ }
1530
1577
 
1531
1578
  const mcId = resource.getId();
1532
1579
  const mcName = resource.getName() + '.jta';
@@ -1562,7 +1609,13 @@ async function _collectMovieClipFrames(
1562
1609
  const exportFrameIndex = firstFrameIndexByTextureIndex.get(textureIndex);
1563
1610
  if (exportFrameIndex === undefined) continue;
1564
1611
  const itemId = `${mcId}_${exportFrameIndex}`;
1565
- const input = await _createMovieClipFrameInput(jta.frames[textureIndex], itemId, resource, encoder);
1612
+ const input = await _createMovieClipFrameInput(
1613
+ jta.frames[textureIndex],
1614
+ itemId,
1615
+ resource,
1616
+ encoder,
1617
+ options.strictOutput,
1618
+ );
1566
1619
  if (!input) continue;
1567
1620
  inputs.push(input);
1568
1621
  spriteIdByTextureIndex.set(textureIndex, itemId);
@@ -1584,7 +1637,13 @@ async function _collectMovieClipFrames(
1584
1637
  } else {
1585
1638
  for (let frameIndex = 0; frameIndex < jta.frames.length; frameIndex += 1) {
1586
1639
  const itemId = `${mcId}_${frameIndex}`;
1587
- const input = await _createMovieClipFrameInput(jta.frames[frameIndex], itemId, resource, encoder);
1640
+ const input = await _createMovieClipFrameInput(
1641
+ jta.frames[frameIndex],
1642
+ itemId,
1643
+ resource,
1644
+ encoder,
1645
+ options.strictOutput,
1646
+ );
1588
1647
  if (!input) continue;
1589
1648
  inputs.push(input);
1590
1649
  const frame = doc.createMovieFrame(itemId);
@@ -1604,7 +1663,9 @@ async function _collectMovieClipFrames(
1604
1663
  resource.setHeight(jta.meta?.height ?? 0);
1605
1664
  }
1606
1665
  } catch {
1607
- logger.warn(`atlas: Could not parse MovieClip "${filePath}", skipping frames.`);
1666
+ const message = `atlas: Could not parse MovieClip "${filePath}".`;
1667
+ if (options.strictOutput) throw new Error(message);
1668
+ logger.warn(`${message} Skipping frames.`);
1608
1669
  }
1609
1670
  }
1610
1671
 
@@ -1613,6 +1674,7 @@ async function _createMovieClipFrameInput(
1613
1674
  itemId: string,
1614
1675
  resource: MovieClipResource,
1615
1676
  encoder: AtlasRasterBackend | undefined,
1677
+ strictOutput: boolean,
1616
1678
  ): Promise<InputItem | null> {
1617
1679
  if (!encoder || buffer.length === 0) return null;
1618
1680
  try {
@@ -1633,6 +1695,7 @@ async function _createMovieClipFrameInput(
1633
1695
  sourceKind: 'movieclip-frame',
1634
1696
  };
1635
1697
  } catch {
1698
+ if (strictOutput) throw new Error(`atlas: Could not decode MovieClip frame "${itemId}".`);
1636
1699
  return null;
1637
1700
  }
1638
1701
  }