@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.
- package/README.md +12 -4
- package/dist/{codegen-C7PPZrB3.d.cts → codegen-B8ZM1F4j.d.cts} +12 -11
- package/dist/{codegen-CEnmtjBP.d.ts → codegen-CfbDHuFt.d.ts} +12 -11
- package/dist/index.cjs +152 -56
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +152 -56
- package/dist/node.cjs +4 -4
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +4 -4
- package/dist/{publish-CtCXsMdj.cjs → publish-D7268_u4.cjs} +59 -32
- package/dist/{publish-Bl-GK9kt.js → publish-DsPK0SJ1.js} +59 -32
- package/dist/web.cjs +1 -1
- package/dist/web.js +1 -1
- package/package.json +2 -2
- package/src/adapters/node/publish.ts +6 -5
- package/src/adapters/web/publish.ts +1 -0
- package/src/atlas.ts +80 -17
- package/src/publish.ts +57 -49
- package/src/restore.ts +177 -68
|
@@ -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
|
|
912
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
}))
|
|
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
|
-
|
|
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
|
|
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
|
-
}))
|
|
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
|
-
|
|
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
|
|
3072
|
-
*
|
|
3073
|
-
* const doc = await
|
|
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
|
|
3098
|
+
* await publishNode({
|
|
3099
|
+
* document: doc,
|
|
3076
3100
|
* output: './release/',
|
|
3077
3101
|
* compressed: true,
|
|
3078
|
-
*
|
|
3079
|
-
* basePath: './assets/',
|
|
3102
|
+
* assetsPath: './assets/',
|
|
3080
3103
|
* fileExtension: 'bytes',
|
|
3081
|
-
*
|
|
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
|
-
|
|
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-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/functions",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
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.
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
424
|
-
const
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
798
|
+
: undefined,
|
|
784
799
|
);
|
|
785
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|