@openfairygui/functions 0.2.0-alpha.0 → 0.2.0-alpha.11

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/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
- import { BinaryReader, BinaryWriter, GearType, ProjectType, ProjectWriter, TransitionActionType, UamTransactionError, applyUamTransaction, generateId } from "@openfairygui/core";
1
+ import { applyUamTransactionApp } from "./uam-transaction.js";
2
+ import { BinaryReader, BinaryWriter, GearType, ProjectType, ProjectWriter, TransitionActionType, generateId } from "@openfairygui/core";
3
+ import { createJiti } from "jiti";
2
4
  //#region src/inspect.ts
3
5
  function mapResource(resource) {
4
6
  return {
@@ -109,6 +111,40 @@ function createTransform(name, fn) {
109
111
  Object.defineProperty(fn, "name", { value: name });
110
112
  return fn;
111
113
  }
114
+ function parseTextureSetMode(value) {
115
+ const raw = value?.trim() ?? "";
116
+ if (!raw) return {
117
+ kind: "auto",
118
+ raw: ""
119
+ };
120
+ if (raw === "alone") return {
121
+ kind: "standalone",
122
+ raw,
123
+ sizeMode: "default"
124
+ };
125
+ if (raw === "alone_npot") return {
126
+ kind: "standalone",
127
+ raw,
128
+ sizeMode: "npot"
129
+ };
130
+ if (raw === "alone_mof") return {
131
+ kind: "standalone",
132
+ raw,
133
+ sizeMode: "multipleOf4"
134
+ };
135
+ if (/^\d+$/.test(raw)) {
136
+ const pageIndex = Number(raw);
137
+ if (pageIndex >= 0 && pageIndex <= 10) return {
138
+ kind: "page",
139
+ raw,
140
+ pageIndex
141
+ };
142
+ }
143
+ return {
144
+ kind: "auto",
145
+ raw
146
+ };
147
+ }
112
148
  //#endregion
113
149
  //#region src/validate.ts
114
150
  /**
@@ -1040,6 +1076,19 @@ const ATLAS_DEFAULTS = {
1040
1076
  function getPublishedItemId(resource) {
1041
1077
  return (resource.getExtras() ?? {})._publishedId ?? resource.getId();
1042
1078
  }
1079
+ function getSelectedSkeletonDependencyImageIds(resources) {
1080
+ const imageIds = /* @__PURE__ */ new Set();
1081
+ const resourcesById = new Map(resources.map((resource) => [resource.getId(), resource]));
1082
+ for (const resource of resources) {
1083
+ if (!isSkeletonResource$1(resource)) continue;
1084
+ for (const requiredId of resource.getRequireIds()) {
1085
+ if (!requiredId) continue;
1086
+ const required = resourcesById.get(requiredId);
1087
+ if (required && isImageResource$1(required)) imageIds.add(requiredId);
1088
+ }
1089
+ }
1090
+ return imageIds;
1091
+ }
1043
1092
  function resolveFontFileName(fontName) {
1044
1093
  return /\.fnt$/i.test(fontName) ? fontName : `${fontName}.fnt`;
1045
1094
  }
@@ -1176,12 +1225,18 @@ function atlas(_options = {}) {
1176
1225
  const logger = doc.getLogger();
1177
1226
  const encoder = options.encoder;
1178
1227
  const doTrim = options.trimImage && !!encoder && !!options.basePath;
1228
+ const packageFilter = options.packages ? new Set(options.packages) : null;
1179
1229
  for (const pkg of root.listPackages()) {
1230
+ if (packageFilter && !packageFilter.has(pkg.getName())) continue;
1180
1231
  const selectedPublishIds = new Set((pkg.getExtras() ?? {}).publishedResourceIds ?? []);
1181
1232
  const allResources = selectedPublishIds.size > 0 ? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId())) : pkg.listResources();
1233
+ const skeletonDependencyImageIds = getSelectedSkeletonDependencyImageIds(allResources);
1182
1234
  const orderedResources = await resolveEditorCompatibleResourceOrder(pkg, allResources, options);
1183
1235
  const orderedAllResources = sortResourcesByOrder(allResources, new Map(orderedResources.map((resource, index) => [resource.getId(), index])), new Map(allResources.map((resource, index) => [resource.getId(), index])));
1184
- if (!allResources.some((resource) => isPackableResource(resource))) continue;
1236
+ if (!allResources.some((resource) => {
1237
+ if (isImageResource$1(resource) && skeletonDependencyImageIds.has(resource.getId())) return false;
1238
+ return isPackableResource(resource);
1239
+ })) continue;
1185
1240
  const inputs = [];
1186
1241
  const referencedIds = /* @__PURE__ */ new Set();
1187
1242
  const resourceMap = /* @__PURE__ */ new Map();
@@ -1250,148 +1305,72 @@ function atlas(_options = {}) {
1250
1305
  }
1251
1306
  for (const res of orderedAllResources) if (isImageResource$1(res)) {
1252
1307
  const resId = res.getId();
1253
- if (!res.getExported() && referencedIds.size > 0 && !referencedIds.has(resId)) continue;
1308
+ if (skeletonDependencyImageIds.has(resId)) continue;
1309
+ if (selectedPublishIds.size === 0 && !res.getExported() && referencedIds.size > 0 && !referencedIds.has(resId)) continue;
1254
1310
  await _collectImage(res, pkg, inputs, encoder, options, doTrim, logger);
1255
1311
  } else if (isMovieClipResource$1(res)) {
1256
1312
  const resId = res.getId();
1257
- if (!res.getExported() && referencedIds.size > 0 && !referencedIds.has(resId)) continue;
1313
+ if (selectedPublishIds.size === 0 && !res.getExported() && referencedIds.size > 0 && !referencedIds.has(resId)) continue;
1258
1314
  await _collectMovieClipFrames(doc, res, pkg, inputs, encoder, options, logger);
1259
1315
  } else if (isFontResource$1(res)) {
1260
1316
  const resId = res.getId();
1261
- if (!res.getExported() && referencedIds.size > 0 && !referencedIds.has(resId)) continue;
1317
+ if (selectedPublishIds.size === 0 && !res.getExported() && referencedIds.size > 0 && !referencedIds.has(resId)) continue;
1262
1318
  await _collectFontTexture(doc, res, pkg, options);
1263
1319
  }
1264
1320
  if (inputs.length === 0) continue;
1265
- const branchGroups = buildBranchAtlasGroups(doc, inputs, options);
1266
1321
  let totalPageCount = 0;
1267
1322
  let usedDirectOutput = false;
1323
+ const { autoInputs, fixedPageGroups, standaloneGroups, reservedPageIndexes } = groupStandaloneInputs(doc, inputs, options);
1324
+ const branchGroups = buildBranchAtlasGroups(doc, autoInputs, options);
1325
+ const branchPageOffsets = /* @__PURE__ */ new Map();
1268
1326
  for (const group of branchGroups) {
1269
- const directOutput = resolveDirectImageOutput(group.inputs, options);
1327
+ const directOutput = fixedPageGroups.length === 0 && standaloneGroups.length === 0 ? resolveDirectImageOutput(group.inputs, options) : null;
1270
1328
  if (directOutput) {
1271
1329
  await emitDirectImageOutput(doc, pkg, directOutput, encoder, options, logger, group.branchName, group.branchOrdinal);
1272
1330
  usedDirectOutput = true;
1273
1331
  totalPageCount += 1;
1274
1332
  continue;
1275
1333
  }
1276
- const hasDuplicatePadding = group.inputs.some((i) => {
1277
- return isImageResource$1(i.resource) && i.resource.getDuplicatePadding?.() === true;
1334
+ const pageStart = reserveAutoPageStart(branchPageOffsets, group.branchOrdinal, reservedPageIndexes);
1335
+ const emittedPageCount = await emitPagedAtlasGroup(doc, pkg, allResources, group.inputs, {
1336
+ branchName: group.branchName,
1337
+ branchOrdinal: group.branchOrdinal,
1338
+ pageStart,
1339
+ fileNameAt: (pageIndex) => resolveAtlasOutputFileName(pkg, pageIndex, group.branchName),
1340
+ options,
1341
+ encoder,
1342
+ logger
1278
1343
  });
1279
- const pages = new MaxRectsPackerCompat({
1280
- pot: options.powerOfTwo,
1281
- mof: !options.powerOfTwo,
1282
- padding: options.padding,
1283
- rotation: options.allowRotation,
1284
- minWidth: 16,
1285
- minHeight: 16,
1286
- maxWidth: options.maxSize,
1287
- maxHeight: options.maxSize,
1288
- square: options.square,
1289
- fast: options.fast,
1290
- edgePadding: false,
1291
- duplicatePadding: hasDuplicatePadding,
1292
- multiPage: options.multiPage,
1293
- preserveInputOrderOnTie: options.preserveInputOrderOnTie
1294
- }).pack(group.inputs.map((input, index) => inputToCompatRect(input, index)));
1295
- if (!pages || pages.length === 0) continue;
1296
- totalPageCount += pages.length;
1297
- for (let p = 0; p < pages.length; p++) {
1298
- const page = pages[p];
1299
- const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(group.branchOrdinal, p)}`);
1300
- atlasNode.setIndex(resolveAtlasIndex(group.branchOrdinal, p));
1301
- atlasNode.setFile(resolveAtlasOutputFileName(pkg, p, group.branchName));
1302
- atlasNode.setWidth(page.width);
1303
- atlasNode.setHeight(page.height);
1304
- pkg.addAtlas(atlasNode);
1305
- for (const pr of page.outputRects) {
1306
- const input = group.inputs[pr.index];
1307
- if (!input) continue;
1308
- const packedSize = resolvePackedRectSize(input, pr.width, pr.height, pr.rotated);
1309
- const rotated = pr.rotated;
1310
- const sprite = doc.createSprite();
1311
- sprite.setItemId(input.id);
1312
- sprite.setRectX(pr.x);
1313
- sprite.setRectY(pr.y);
1314
- sprite.setRectWidth(packedSize.width);
1315
- sprite.setRectHeight(packedSize.height);
1316
- sprite.setRotated(rotated);
1317
- sprite.setOffsetX(input.offsetX);
1318
- sprite.setOffsetY(input.offsetY);
1319
- sprite.setOriginalWidth(input.originalWidth);
1320
- sprite.setOriginalHeight(input.originalHeight);
1321
- sprite.setAtlas(atlasNode);
1322
- atlasNode.addSprite(sprite);
1323
- }
1324
- for (const res of allResources) {
1325
- if (!isFontResource$1(res)) continue;
1326
- const alias = res.getExtras()?._fontSpriteAlias;
1327
- if (!alias) continue;
1328
- const imgSprite = page.outputRects.find((result) => group.inputs[result.index]?.id === alias.textureId);
1329
- if (!imgSprite) continue;
1330
- const imgInput = group.inputs[imgSprite.index];
1331
- const fontSprite = doc.createSprite();
1332
- fontSprite.setItemId(alias.fontId);
1333
- fontSprite.setRectX(imgSprite.x);
1334
- fontSprite.setRectY(imgSprite.y);
1335
- fontSprite.setRectWidth(imgSprite.width);
1336
- fontSprite.setRectHeight(imgSprite.height);
1337
- fontSprite.setRotated(imgSprite.rotated);
1338
- if (imgInput) {
1339
- fontSprite.setOffsetX(imgInput.offsetX);
1340
- fontSprite.setOffsetY(imgInput.offsetY);
1341
- fontSprite.setOriginalWidth(imgInput.originalWidth);
1342
- fontSprite.setOriginalHeight(imgInput.originalHeight);
1343
- }
1344
- fontSprite.setAtlas(atlasNode);
1345
- atlasNode.addSprite(fontSprite);
1346
- }
1347
- }
1348
- if (encoder && options.outputPath) {
1349
- if (options.mkdir) await options.mkdir(options.outputPath);
1350
- for (let p = 0; p < pages.length; p++) {
1351
- const page = pages[p];
1352
- const compositeInputs = [];
1353
- for (const pr of page.outputRects) {
1354
- const input = group.inputs[pr.index];
1355
- if (!input) continue;
1356
- if (pr.width <= 0 || pr.height <= 0 || input.width <= 0 || input.height <= 0) continue;
1357
- try {
1358
- let imgBuffer;
1359
- if (input.trimBuffer) {
1360
- imgBuffer = input.trimBuffer;
1361
- if (imgBuffer.length === 0) continue;
1362
- } else {
1363
- if (!isImageResource$1(input.resource)) {
1364
- logger.warn(`atlas: Non-image input "${input.id}" is missing inline buffer, skipping compositing.`);
1365
- continue;
1366
- }
1367
- imgBuffer = await encoder(_resolveImagePath(input.resource, pkg, options.basePath)).toBuffer();
1368
- }
1369
- if (pr.rotated) imgBuffer = await encoder(imgBuffer).rotate(270).toBuffer();
1370
- compositeInputs.push({
1371
- input: imgBuffer,
1372
- left: pr.x,
1373
- top: pr.y
1374
- });
1375
- } catch {
1376
- logger.warn(`atlas: Could not read image "${input.id}" for compositing.`);
1377
- }
1378
- }
1379
- const atlasFileName = resolveAtlasOutputFileName(pkg, p, group.branchName);
1380
- const outputFile = `${options.outputPath}/${atlasFileName}`;
1381
- await encoder({ create: {
1382
- width: page.width,
1383
- height: page.height,
1384
- channels: 4,
1385
- background: {
1386
- r: 0,
1387
- g: 0,
1388
- b: 0,
1389
- alpha: 0
1390
- }
1391
- } }).composite(compositeInputs).png().toFile(outputFile);
1392
- logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
1393
- }
1394
- }
1344
+ totalPageCount += emittedPageCount;
1345
+ branchPageOffsets.set(group.branchOrdinal, pageStart + emittedPageCount);
1346
+ }
1347
+ for (const group of fixedPageGroups) {
1348
+ const emittedPageCount = await emitPagedAtlasGroup(doc, pkg, allResources, group.inputs, {
1349
+ branchName: group.branchName,
1350
+ branchOrdinal: group.branchOrdinal,
1351
+ pageStart: group.pageIndex,
1352
+ forceSinglePage: true,
1353
+ fileNameAt: () => resolveAtlasOutputFileName(pkg, group.pageIndex, group.branchName),
1354
+ options,
1355
+ encoder,
1356
+ logger
1357
+ });
1358
+ totalPageCount += emittedPageCount;
1359
+ }
1360
+ const standalonePageOffsets = new Map(branchPageOffsets);
1361
+ for (const group of fixedPageGroups) {
1362
+ const nextPageIndex = group.pageIndex + 1;
1363
+ if (nextPageIndex > (standalonePageOffsets.get(group.branchOrdinal) ?? 0)) standalonePageOffsets.set(group.branchOrdinal, nextPageIndex);
1364
+ }
1365
+ for (const group of standaloneGroups) {
1366
+ const emittedPageCount = await emitStandaloneAtlasGroup(doc, pkg, group, {
1367
+ atlasIndexStart: standalonePageOffsets.get(group.branchOrdinal) ?? 0,
1368
+ options,
1369
+ encoder,
1370
+ logger
1371
+ });
1372
+ totalPageCount += emittedPageCount;
1373
+ standalonePageOffsets.set(group.branchOrdinal, (standalonePageOffsets.get(group.branchOrdinal) ?? 0) + emittedPageCount);
1395
1374
  }
1396
1375
  if (usedDirectOutput) logger.info(`atlas: Direct output for single image package "${pkg.getName()}".`);
1397
1376
  logger.info(`atlas: Packed ${inputs.length} images into ${totalPageCount} atlas(es) for package "${pkg.getName()}".`);
@@ -1428,6 +1407,171 @@ function buildBranchAtlasGroups(doc, inputs, options) {
1428
1407
  inputs: groups.get(branchName) ?? []
1429
1408
  }));
1430
1409
  }
1410
+ function reserveAutoPageStart(branchPageOffsets, branchOrdinal, reservedPageIndexes) {
1411
+ let pageIndex = branchPageOffsets.get(branchOrdinal) ?? 0;
1412
+ while (branchOrdinal === 0 && reservedPageIndexes.has(pageIndex)) pageIndex += 1;
1413
+ return pageIndex;
1414
+ }
1415
+ async function emitPagedAtlasGroup(doc, pkg, allResources, inputs, context) {
1416
+ if (inputs.length === 0) return 0;
1417
+ const pages = packAtlasPages(inputs, context.options, context.forceSinglePage === true);
1418
+ if (pages.length === 0) return 0;
1419
+ for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
1420
+ const page = pages[pageOffset];
1421
+ const pageIndex = context.pageStart + pageOffset;
1422
+ const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(context.branchOrdinal, pageIndex)}`);
1423
+ atlasNode.setIndex(resolveAtlasIndex(context.branchOrdinal, pageIndex));
1424
+ atlasNode.setFile(context.fileNameAt(pageIndex));
1425
+ atlasNode.setWidth(page.width);
1426
+ atlasNode.setHeight(page.height);
1427
+ pkg.addAtlas(atlasNode);
1428
+ attachSpritesToAtlas(doc, allResources, inputs, page.outputRects, atlasNode);
1429
+ await writeAtlasPageImage(pkg, inputs, page, atlasNode.getFile(), context.encoder, context.options, context.logger);
1430
+ }
1431
+ return pages.length;
1432
+ }
1433
+ async function emitStandaloneAtlasGroup(doc, pkg, group, context) {
1434
+ if (group.inputs.length === 0) return 0;
1435
+ const pages = packAtlasPages(group.inputs, context.options, true, group.sizeMode === "npot" ? {
1436
+ powerOfTwo: false,
1437
+ multipleOfFour: false,
1438
+ square: false
1439
+ } : group.sizeMode === "multipleOf4" ? {
1440
+ powerOfTwo: false,
1441
+ multipleOfFour: true,
1442
+ square: false
1443
+ } : void 0);
1444
+ if (pages.length === 0) return 0;
1445
+ for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
1446
+ const page = pages[pageOffset];
1447
+ const baseFileName = resolveStandaloneAtlasOutputFileName(pkg, group.resource, group.branchName);
1448
+ const atlasFileName = pages.length <= 1 ? baseFileName : insertFileNameSuffix(baseFileName, `_${pageOffset}`);
1449
+ const atlasIndex = context.atlasIndexStart + pageOffset;
1450
+ const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(group.branchOrdinal, atlasIndex)}`);
1451
+ atlasNode.setIndex(resolveAtlasIndex(group.branchOrdinal, atlasIndex));
1452
+ atlasNode.setFile(atlasFileName);
1453
+ const standaloneSize = resolveStandaloneAtlasSize(page.width, page.height, group.sizeMode, context.options);
1454
+ atlasNode.setWidth(standaloneSize.width);
1455
+ atlasNode.setHeight(standaloneSize.height);
1456
+ pkg.addAtlas(atlasNode);
1457
+ attachSpritesToAtlas(doc, [], group.inputs, page.outputRects, atlasNode);
1458
+ await writeAtlasPageImage(pkg, group.inputs, {
1459
+ ...page,
1460
+ width: standaloneSize.width,
1461
+ height: standaloneSize.height
1462
+ }, atlasFileName, context.encoder, context.options, context.logger);
1463
+ }
1464
+ return pages.length;
1465
+ }
1466
+ function packAtlasPages(inputs, options, forceSinglePage, sizeOverrides) {
1467
+ const hasDuplicatePadding = inputs.some((input) => {
1468
+ return isImageResource$1(input.resource) && input.resource.getDuplicatePadding?.() === true;
1469
+ });
1470
+ return new MaxRectsPackerCompat({
1471
+ pot: sizeOverrides?.powerOfTwo ?? options.powerOfTwo,
1472
+ mof: sizeOverrides?.multipleOfFour ?? !options.powerOfTwo,
1473
+ padding: options.padding,
1474
+ rotation: options.allowRotation,
1475
+ minWidth: 16,
1476
+ minHeight: 16,
1477
+ maxWidth: options.maxSize,
1478
+ maxHeight: options.maxSize,
1479
+ square: sizeOverrides?.square ?? options.square,
1480
+ fast: options.fast,
1481
+ edgePadding: false,
1482
+ duplicatePadding: hasDuplicatePadding,
1483
+ multiPage: forceSinglePage ? false : options.multiPage,
1484
+ preserveInputOrderOnTie: options.preserveInputOrderOnTie
1485
+ }).pack(inputs.map((input, index) => inputToCompatRect(input, index))) ?? [];
1486
+ }
1487
+ function attachSpritesToAtlas(doc, allResources, inputs, outputRects, atlasNode) {
1488
+ for (const packedRect of outputRects) {
1489
+ const input = inputs[packedRect.index];
1490
+ if (!input) continue;
1491
+ const packedSize = resolvePackedRectSize(input, packedRect.width, packedRect.height, packedRect.rotated);
1492
+ const sprite = doc.createSprite();
1493
+ sprite.setItemId(input.id);
1494
+ sprite.setRectX(packedRect.x);
1495
+ sprite.setRectY(packedRect.y);
1496
+ sprite.setRectWidth(packedSize.width);
1497
+ sprite.setRectHeight(packedSize.height);
1498
+ sprite.setRotated(packedRect.rotated);
1499
+ sprite.setOffsetX(input.offsetX);
1500
+ sprite.setOffsetY(input.offsetY);
1501
+ sprite.setOriginalWidth(input.originalWidth);
1502
+ sprite.setOriginalHeight(input.originalHeight);
1503
+ sprite.setAtlas(atlasNode);
1504
+ atlasNode.addSprite(sprite);
1505
+ }
1506
+ for (const resource of allResources) {
1507
+ if (!isFontResource$1(resource)) continue;
1508
+ const alias = resource.getExtras()?._fontSpriteAlias;
1509
+ if (!alias) continue;
1510
+ const imageSprite = outputRects.find((result) => inputs[result.index]?.id === alias.textureId);
1511
+ if (!imageSprite) continue;
1512
+ const imageInput = inputs[imageSprite.index];
1513
+ const fontSprite = doc.createSprite();
1514
+ fontSprite.setItemId(alias.fontId);
1515
+ fontSprite.setRectX(imageSprite.x);
1516
+ fontSprite.setRectY(imageSprite.y);
1517
+ fontSprite.setRectWidth(imageSprite.width);
1518
+ fontSprite.setRectHeight(imageSprite.height);
1519
+ fontSprite.setRotated(imageSprite.rotated);
1520
+ if (imageInput) {
1521
+ fontSprite.setOffsetX(imageInput.offsetX);
1522
+ fontSprite.setOffsetY(imageInput.offsetY);
1523
+ fontSprite.setOriginalWidth(imageInput.originalWidth);
1524
+ fontSprite.setOriginalHeight(imageInput.originalHeight);
1525
+ }
1526
+ fontSprite.setAtlas(atlasNode);
1527
+ atlasNode.addSprite(fontSprite);
1528
+ }
1529
+ }
1530
+ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, options, logger) {
1531
+ if (!encoder || !options.outputPath) return;
1532
+ if (options.mkdir) await options.mkdir(options.outputPath);
1533
+ const compositeInputs = [];
1534
+ for (const packedRect of page.outputRects) {
1535
+ const input = inputs[packedRect.index];
1536
+ if (!input) continue;
1537
+ if (packedRect.width <= 0 || packedRect.height <= 0 || input.width <= 0 || input.height <= 0) continue;
1538
+ try {
1539
+ let imageBuffer;
1540
+ if (input.trimBuffer) {
1541
+ imageBuffer = input.trimBuffer;
1542
+ if (imageBuffer.length === 0) continue;
1543
+ } else if (input.rasterizedBuffer) imageBuffer = input.rasterizedBuffer;
1544
+ else {
1545
+ if (!isImageResource$1(input.resource)) {
1546
+ logger.warn(`atlas: Non-image input "${input.id}" is missing inline buffer, skipping compositing.`);
1547
+ continue;
1548
+ }
1549
+ imageBuffer = await encoder(_resolveImagePath(input.resource, pkg, options.basePath)).toBuffer();
1550
+ }
1551
+ if (packedRect.rotated) imageBuffer = await encoder(imageBuffer).rotate(270).toBuffer();
1552
+ compositeInputs.push({
1553
+ input: imageBuffer,
1554
+ left: packedRect.x,
1555
+ top: packedRect.y
1556
+ });
1557
+ } catch {
1558
+ logger.warn(`atlas: Could not read image "${input.id}" for compositing.`);
1559
+ }
1560
+ }
1561
+ const outputFile = `${options.outputPath}/${atlasFileName}`;
1562
+ await encoder({ create: {
1563
+ width: page.width,
1564
+ height: page.height,
1565
+ channels: 4,
1566
+ background: {
1567
+ r: 0,
1568
+ g: 0,
1569
+ b: 0,
1570
+ alpha: 0
1571
+ }
1572
+ } }).composite(compositeInputs).toFile(outputFile);
1573
+ logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
1574
+ }
1431
1575
  function inputToCompatRect(input, index) {
1432
1576
  const duplicatePadding = isImageResource$1(input.resource) && input.resource.getDuplicatePadding?.() === true;
1433
1577
  return {
@@ -1542,14 +1686,47 @@ function resolveAtlasOutputFileName(pkg, pageIndex, branchName) {
1542
1686
  const suffix = branchName ? `_${branchName}` : "";
1543
1687
  return `${pkg.getPublishName() || pkg.getName()}_atlas${pageIndex}${suffix}.png`;
1544
1688
  }
1689
+ function resolveStandaloneAtlasOutputFileName(pkg, resource, branchName) {
1690
+ const baseName = `${pkg.getPublishName() || pkg.getName()}_atlas_${getPublishedItemId(resource)}`;
1691
+ const suffix = branchName ? `_${branchName}` : "";
1692
+ if (isImageResource$1(resource)) return `${baseName}${suffix}${extname$1(resolveImageFileName$1(resource)) || ".png"}`;
1693
+ return `${baseName}${suffix}.png`;
1694
+ }
1695
+ function resolveStandaloneAtlasSize(width, height, sizeMode, options) {
1696
+ if (sizeMode === "npot") return {
1697
+ width,
1698
+ height
1699
+ };
1700
+ if (sizeMode === "multipleOf4") return {
1701
+ width: roundUpToMultiple(width, 4),
1702
+ height: roundUpToMultiple(height, 4)
1703
+ };
1704
+ return resolveDirectOutputAtlasSize(width, height, options);
1705
+ }
1545
1706
  function resolveImageFileName$1(resource) {
1546
1707
  const extras = resource.getExtras();
1547
1708
  return resource.getFileName() || extras._fileName || resource.getName();
1548
1709
  }
1710
+ function extname$1(fileName) {
1711
+ const normalized = fileName.replace(/\\/g, "/");
1712
+ const lastSlash = normalized.lastIndexOf("/");
1713
+ const lastDot = normalized.lastIndexOf(".");
1714
+ if (lastDot <= lastSlash) return "";
1715
+ return normalized.slice(lastDot);
1716
+ }
1717
+ function insertFileNameSuffix(fileName, suffix) {
1718
+ const extension = extname$1(fileName);
1719
+ if (!extension) return `${fileName}${suffix}`;
1720
+ return `${fileName.slice(0, -extension.length)}${suffix}${extension}`;
1721
+ }
1549
1722
  function nextPow2(value) {
1550
1723
  if (value <= 1) return 1;
1551
1724
  return 2 ** Math.ceil(Math.log2(value));
1552
1725
  }
1726
+ function roundUpToMultiple(value, base) {
1727
+ if (value <= 0) return 0;
1728
+ return Math.ceil(value / base) * base;
1729
+ }
1553
1730
  function sortResourcesByOrder(resources, orderMap, inputOrderMap) {
1554
1731
  const ordered = [...resources];
1555
1732
  ordered.sort((left, right) => {
@@ -1565,14 +1742,71 @@ function sortResourcesByOrder(resources, orderMap, inputOrderMap) {
1565
1742
  });
1566
1743
  return ordered;
1567
1744
  }
1745
+ function getResourceTextureSetMode(resource) {
1746
+ if (isImageResource$1(resource)) return parseTextureSetMode(resource.getTextureSetMode?.());
1747
+ return parseTextureSetMode(resource.getTextureSetMode?.());
1748
+ }
1749
+ function groupStandaloneInputs(doc, inputs, options) {
1750
+ const autoInputs = [];
1751
+ const fixedInputsByPage = /* @__PURE__ */ new Map();
1752
+ const standaloneGroups = /* @__PURE__ */ new Map();
1753
+ const reservedPageIndexes = /* @__PURE__ */ new Set();
1754
+ const discoveredBranchNames = [...new Set(inputs.map((input) => getInputBranchName(input)).filter((branchName) => !!branchName))];
1755
+ const orderedBranchNames = doc.getRoot().listBranches().filter((branchName) => discoveredBranchNames.includes(branchName));
1756
+ for (const branchName of discoveredBranchNames) if (!orderedBranchNames.includes(branchName)) orderedBranchNames.push(branchName);
1757
+ const branchOrdinalByName = /* @__PURE__ */ new Map();
1758
+ branchOrdinalByName.set("", 0);
1759
+ if (options.separatedAtlasForBranch) {
1760
+ let ordinal = 1;
1761
+ for (const branchName of orderedBranchNames) branchOrdinalByName.set(branchName, ordinal++);
1762
+ } else for (const branchName of orderedBranchNames) branchOrdinalByName.set(branchName, 0);
1763
+ for (const input of inputs) {
1764
+ const branchName = getInputBranchName(input);
1765
+ const branchOrdinal = branchOrdinalByName.get(branchName) ?? 0;
1766
+ const mode = getResourceTextureSetMode(input.resource);
1767
+ if (mode.kind === "standalone") {
1768
+ const key = `${branchName}\u0000${getPublishedItemId(input.resource)}`;
1769
+ const existing = standaloneGroups.get(key);
1770
+ if (existing) existing.inputs.push(input);
1771
+ else standaloneGroups.set(key, {
1772
+ resource: input.resource,
1773
+ branchName,
1774
+ branchOrdinal,
1775
+ sizeMode: mode.sizeMode,
1776
+ inputs: [input]
1777
+ });
1778
+ continue;
1779
+ }
1780
+ if (mode.kind === "page") {
1781
+ reservedPageIndexes.add(mode.pageIndex);
1782
+ const key = `${branchName}\u0000${mode.pageIndex}`;
1783
+ const existing = fixedInputsByPage.get(key);
1784
+ if (existing) existing.inputs.push(input);
1785
+ else fixedInputsByPage.set(key, {
1786
+ pageIndex: mode.pageIndex,
1787
+ branchName,
1788
+ branchOrdinal,
1789
+ inputs: [input]
1790
+ });
1791
+ continue;
1792
+ }
1793
+ autoInputs.push(input);
1794
+ }
1795
+ return {
1796
+ autoInputs,
1797
+ fixedPageGroups: [...fixedInputsByPage.values()].sort((left, right) => left.branchOrdinal - right.branchOrdinal || left.pageIndex - right.pageIndex),
1798
+ standaloneGroups: [...standaloneGroups.values()].sort((left, right) => left.branchOrdinal - right.branchOrdinal || getPublishedItemId(left.resource).localeCompare(getPublishedItemId(right.resource))),
1799
+ reservedPageIndexes
1800
+ };
1801
+ }
1568
1802
  /**
1569
1803
  * Trim transparent edges from an image using sharp.
1570
1804
  * Returns the trimmed buffer, dimensions, and offsets.
1571
1805
  * Falls back to the original image if trim fails (e.g. no alpha channel, no transparent edges).
1572
1806
  */
1573
- async function _trimImage(encoder, filePath, originalWidth, originalHeight) {
1807
+ async function _trimImage(encoder, input, originalWidth, originalHeight) {
1574
1808
  try {
1575
- const trimResult = await encoder(filePath).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1809
+ const trimResult = await encoder(input).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1576
1810
  if (!isResolvedBuffer(trimResult)) throw new Error("atlas: encoder raw alpha trim did not return resolved metadata.");
1577
1811
  const { data, info } = trimResult;
1578
1812
  const width = info.width;
@@ -1601,7 +1835,7 @@ async function _trimImage(encoder, filePath, originalWidth, originalHeight) {
1601
1835
  const trimmedWidth = maxX - minX + 1;
1602
1836
  const trimmedHeight = maxY - minY + 1;
1603
1837
  return {
1604
- buffer: await encoder(filePath).extract({
1838
+ buffer: await encoder(input).extract({
1605
1839
  left: minX,
1606
1840
  top: minY,
1607
1841
  width: trimmedWidth,
@@ -1616,7 +1850,7 @@ async function _trimImage(encoder, filePath, originalWidth, originalHeight) {
1616
1850
  };
1617
1851
  } catch {
1618
1852
  return {
1619
- buffer: await encoder(filePath).png().toBuffer(),
1853
+ buffer: await encoder(input).png().toBuffer(),
1620
1854
  width: originalWidth,
1621
1855
  height: originalHeight,
1622
1856
  offsetX: 0,
@@ -1640,7 +1874,10 @@ function _resolveImagePath(resource, pkg, basePath) {
1640
1874
  async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, logger) {
1641
1875
  let origW = resource.getWidth() ?? 0;
1642
1876
  let origH = resource.getHeight() ?? 0;
1877
+ const declaredWidth = origW;
1878
+ const declaredHeight = origH;
1643
1879
  let sourceHasAlpha = false;
1880
+ let rasterizedBuffer;
1644
1881
  if (encoder && options.basePath) {
1645
1882
  const filePath = _resolveImagePath(resource, pkg, options.basePath);
1646
1883
  try {
@@ -1652,6 +1889,10 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
1652
1889
  resource.setHeight(origH);
1653
1890
  }
1654
1891
  sourceHasAlpha = metadata.hasAlpha === true || metadata.channels === 4;
1892
+ if (/\.svg$/i.test(resolveImageFileName$1(resource)) && declaredWidth > 0 && declaredHeight > 0) {
1893
+ rasterizedBuffer = await encoder(filePath).resize(declaredWidth, declaredHeight, { fit: "fill" }).png().toBuffer();
1894
+ sourceHasAlpha = true;
1895
+ }
1655
1896
  } catch {
1656
1897
  if (origW === 0 || origH === 0) {
1657
1898
  logger.warn(`atlas: Could not read image "${filePath}", skipping.`);
@@ -1665,7 +1906,7 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
1665
1906
  if (doTrim && sourceHasAlpha && options.basePath && encoder) {
1666
1907
  const filePath = _resolveImagePath(resource, pkg, options.basePath);
1667
1908
  try {
1668
- const trimResult = await _trimImage(encoder, filePath, origW, origH);
1909
+ const trimResult = await _trimImage(encoder, rasterizedBuffer ?? filePath, origW, origH);
1669
1910
  packW = trimResult.width;
1670
1911
  packH = trimResult.height;
1671
1912
  offX = trimResult.offsetX;
@@ -1685,6 +1926,7 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
1685
1926
  offsetY: offY,
1686
1927
  resource,
1687
1928
  trimBuffer: trimBuf,
1929
+ rasterizedBuffer,
1688
1930
  sourceKind: "image"
1689
1931
  });
1690
1932
  }
@@ -2117,6 +2359,62 @@ const FGUI_TYPESCRIPT_BINDER_TEMPLATE = `{{generatedMark}}
2117
2359
  }
2118
2360
  `;
2119
2361
  //#endregion
2362
+ //#region src/plugins/loader.ts
2363
+ const importNative = new Function("id", "return import(id)");
2364
+ async function loadPlugins(doc, pluginsDir) {
2365
+ if (!pluginsDir) return [];
2366
+ const fs = await importNative("node:fs/promises");
2367
+ const path = await importNative("node:path");
2368
+ let entries;
2369
+ try {
2370
+ entries = await fs.readdir(pluginsDir, { withFileTypes: true });
2371
+ } catch {
2372
+ return [];
2373
+ }
2374
+ const plugins = [];
2375
+ for (const entry of entries) {
2376
+ if (!entry.isDirectory()) continue;
2377
+ const pluginDir = path.join(pluginsDir, entry.name);
2378
+ try {
2379
+ const manifest = await readPluginManifest(fs, path, pluginDir);
2380
+ if (!manifest) continue;
2381
+ const plugin = await loadPlugin(resolvePluginMain(path, pluginDir, manifest));
2382
+ plugins.push({
2383
+ name: manifest.name,
2384
+ plugin
2385
+ });
2386
+ } catch (error) {
2387
+ doc.getLogger().warn(`publish: Plugin "${entry.name}" was skipped: ${formatPluginError(error)}`);
2388
+ }
2389
+ }
2390
+ return plugins;
2391
+ }
2392
+ function formatPluginError(error) {
2393
+ return error instanceof Error ? error.message : String(error);
2394
+ }
2395
+ async function readPluginManifest(fs, path, pluginDir) {
2396
+ const manifestPath = path.join(pluginDir, "package.json");
2397
+ const content = await fs.readFile(manifestPath, "utf-8");
2398
+ const manifest = JSON.parse(content);
2399
+ if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
2400
+ if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
2401
+ return manifest;
2402
+ }
2403
+ function resolvePluginMain(path, pluginDir, manifest) {
2404
+ const mainPath = path.resolve(pluginDir, manifest.main);
2405
+ const relative = path.relative(pluginDir, mainPath);
2406
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Codegen plugin "${manifest.name}" main must resolve inside its plugin directory.`);
2407
+ return mainPath;
2408
+ }
2409
+ async function loadPlugin(mainPath) {
2410
+ const mod = await createJiti(import.meta.url).import(mainPath);
2411
+ const defaultExport = mod.default;
2412
+ return isObject(defaultExport) ? defaultExport : mod;
2413
+ }
2414
+ function isObject(value) {
2415
+ return value !== null && typeof value === "object";
2416
+ }
2417
+ //#endregion
2120
2418
  //#region src/codegen.ts
2121
2419
  const AUTO_GENERATED_CODE_MARK = "/** This is an automatically generated class by FairyGUI. Please do not modify it. **/";
2122
2420
  const DEFAULT_CLASS_NAME_PREFIX = "UI_";
@@ -2152,6 +2450,18 @@ async function publishCodeGeneration(doc, options) {
2152
2450
  const logger = doc.getLogger();
2153
2451
  const settings = resolveCodeGenerationSettings(doc);
2154
2452
  if (!settings.allowGenCode) return;
2453
+ const plugins = options.plugins?.filter((plugin) => typeof plugin.plugin.genCode === "function") ?? [];
2454
+ if (plugins.length > 0) {
2455
+ let handled = false;
2456
+ for (const plugin of plugins) try {
2457
+ await plugin.plugin.genCode(doc, settings, options);
2458
+ handled = true;
2459
+ logger.info(`publish: Generated code using plugin "${plugin.name}"`);
2460
+ } catch (error) {
2461
+ logger.warn(`publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`);
2462
+ }
2463
+ if (handled) return;
2464
+ }
2155
2465
  for (const pkg of options.packages) {
2156
2466
  if (!pkg.getGenCode()) continue;
2157
2467
  const plan = resolvePackageCodegenPlan(pkg, settings, options);
@@ -2250,9 +2560,9 @@ async function cleanupGeneratedFiles(directory, fs, extension = ".cs") {
2250
2560
  }
2251
2561
  }
2252
2562
  function buildCodegenClasses(doc, pkg, plan) {
2253
- const exportedComponents = pkg.listComponents().filter((component) => component.getExported()).sort((left, right) => left.getId().localeCompare(right.getId()));
2563
+ const codegenComponents = pkg.listComponents().sort((left, right) => left.getId().localeCompare(right.getId()));
2254
2564
  const generatedById = /* @__PURE__ */ new Map();
2255
- for (const component of exportedComponents) {
2565
+ for (const component of codegenComponents) {
2256
2566
  const encodedClassName = `${plan.settings.classNamePrefix}${normalizeTypeName(component.getName()) || "Component"}`;
2257
2567
  generatedById.set(component.getId(), {
2258
2568
  classId: component.getId(),
@@ -2265,7 +2575,13 @@ function buildCodegenClasses(doc, pkg, plan) {
2265
2575
  members: []
2266
2576
  });
2267
2577
  }
2268
- for (const component of exportedComponents) {
2578
+ for (const component of codegenComponents) {
2579
+ const classInfo = generatedById.get(component.getId());
2580
+ if (!classInfo) continue;
2581
+ classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
2582
+ }
2583
+ for (const [componentId, classInfo] of generatedById) if (classInfo.members.every((member) => member.ignored)) generatedById.delete(componentId);
2584
+ for (const component of codegenComponents) {
2269
2585
  const classInfo = generatedById.get(component.getId());
2270
2586
  if (!classInfo) continue;
2271
2587
  classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
@@ -2279,7 +2595,12 @@ function buildCodegenMembers(doc, pkg, component, plan, generatedById) {
2279
2595
  let childIndex = 0;
2280
2596
  let transitionIndex = 0;
2281
2597
  for (const controller of component.listControllers()) members.push(createMember(ownerType, "controller", "Controller", controller.getName(), controllerIndex++, plan));
2282
- for (const child of component.listChildren()) members.push(createMember(ownerType, "child", resolveChildType(doc, pkg, child, generatedById), child.getName(), childIndex++, plan));
2598
+ for (const child of component.listChildren()) {
2599
+ if (!isRuntimeChild(child)) continue;
2600
+ const index = childIndex++;
2601
+ const resolvedChild = resolveChildType(doc, pkg, child, generatedById);
2602
+ members.push(createMember(ownerType, "child", resolvedChild.type, child.getName(), index, plan, resolvedChild.referencedComponent));
2603
+ }
2283
2604
  for (const transition of component.listTransitions()) members.push(createMember(ownerType, "transition", "Transition", transition.getName(), transitionIndex++, plan));
2284
2605
  const usedNames = /* @__PURE__ */ new Map();
2285
2606
  for (const member of members) {
@@ -2291,7 +2612,10 @@ function buildCodegenMembers(doc, pkg, component, plan, generatedById) {
2291
2612
  }
2292
2613
  return members;
2293
2614
  }
2294
- function createMember(ownerType, kind, type, originalName, index, plan) {
2615
+ function isRuntimeChild(child) {
2616
+ return child.propertyType !== "GGroup" || child.getAdvanced?.() === true;
2617
+ }
2618
+ function createMember(ownerType, kind, type, originalName, index, plan, referencedComponent) {
2295
2619
  const ignored = plan.settings.ignoreNoname && isDefaultMemberName(ownerType, kind, originalName);
2296
2620
  return {
2297
2621
  index,
@@ -2299,30 +2623,41 @@ function createMember(ownerType, kind, type, originalName, index, plan) {
2299
2623
  name: applyMemberNamePrefix(originalName, plan.settings.memberNamePrefix),
2300
2624
  originalName,
2301
2625
  type,
2302
- ignored
2626
+ ignored,
2627
+ referencedComponent
2303
2628
  };
2304
2629
  }
2305
2630
  function resolveChildType(doc, pkg, child, generatedById) {
2306
2631
  const src = child.getSrc?.();
2307
2632
  if (src) {
2308
- const localResource = resolveChildSourceComponent(doc, pkg, src);
2309
- if (localResource) return generatedById.get(localResource.getId())?.encodedClassName ?? resolveComponentBaseType(localResource);
2633
+ let referencedComponent = null;
2634
+ if (src.startsWith("ui://")) {
2635
+ const rest = src.slice(5);
2636
+ const pkgId = rest.slice(0, 8);
2637
+ const resourceId = rest.slice(8);
2638
+ const targetPackage = doc.getRoot().listPackages().find((candidate) => candidate.getId() === pkgId);
2639
+ const targetResource = targetPackage?.getResourceById(resourceId);
2640
+ if (targetPackage && targetResource?.propertyType === "Component") referencedComponent = {
2641
+ component: targetResource,
2642
+ package: targetPackage
2643
+ };
2644
+ } else {
2645
+ const packageId = child.getPackageId?.();
2646
+ const targetPackage = packageId ? doc.getRoot().listPackages().find((candidate) => candidate.getId() === packageId) : pkg;
2647
+ const targetResource = targetPackage?.getResourceById(src);
2648
+ if (targetPackage && targetResource?.propertyType === "Component") referencedComponent = {
2649
+ component: targetResource,
2650
+ package: targetPackage
2651
+ };
2652
+ }
2653
+ if (referencedComponent) return {
2654
+ type: (referencedComponent.package === pkg ? generatedById.get(referencedComponent.component.getId()) : void 0)?.encodedClassName ?? resolveComponentBaseType(referencedComponent.component),
2655
+ referencedComponent
2656
+ };
2310
2657
  }
2311
2658
  const instanceExtType = child.getInstanceExtType?.();
2312
- if (instanceExtType) return `G${instanceExtType}`;
2313
- return child.propertyType;
2314
- }
2315
- function resolveChildSourceComponent(doc, pkg, src) {
2316
- if (!src) return null;
2317
- if (src.startsWith("ui://")) {
2318
- const rest = src.slice(5);
2319
- const pkgId = rest.slice(0, 8);
2320
- const resourceId = rest.slice(8);
2321
- const targetResource = doc.getRoot().listPackages().find((candidate) => candidate.getId() === pkgId)?.getResourceById(resourceId);
2322
- return targetResource?.propertyType === "Component" ? targetResource : null;
2323
- }
2324
- const localResource = pkg.getResourceById(src);
2325
- return localResource?.propertyType === "Component" ? localResource : null;
2659
+ if (instanceExtType) return { type: `G${instanceExtType}` };
2660
+ return { type: child.propertyType };
2326
2661
  }
2327
2662
  function resolveComponentBaseType(component) {
2328
2663
  const extensionType = component.getExtensionType();
@@ -2393,21 +2728,21 @@ function renderFguiTypescriptMemberAssignment(member, getMemberByName, variant)
2393
2728
  return getMemberByName ? `\t\tthis.${member.name} = <${translatedType}><any>(this.getChild("${escapeTypeScriptString(member.originalName)}"));` : `\t\tthis.${member.name} = <${translatedType}><any>(this.getChildAt(${member.index}));`;
2394
2729
  }
2395
2730
  function resolveCodePath(codePath, basePath, fs) {
2396
- if (isAbsolutePath(codePath)) return trimTrailingSlashes$1(codePath);
2731
+ if (isAbsolutePath(codePath)) return trimTrailingSlashes$2(codePath);
2397
2732
  const projectBasePath = resolveProjectBasePath(basePath);
2398
- return projectBasePath ? trimTrailingSlashes$1(fs.join(projectBasePath, codePath)) : trimTrailingSlashes$1(codePath);
2733
+ return projectBasePath ? trimTrailingSlashes$2(fs.join(projectBasePath, codePath)) : trimTrailingSlashes$2(codePath);
2399
2734
  }
2400
2735
  function resolveProjectBasePath(basePath) {
2401
2736
  if (!basePath) return "";
2402
- const normalized = trimTrailingSlashes$1(basePath);
2737
+ const normalized = trimTrailingSlashes$2(basePath);
2403
2738
  const assetsMatch = normalized.match(/^(.*)[/\\]assets(?:_[^/\\]+)?$/i);
2404
2739
  if (assetsMatch?.[1]) return assetsMatch[1];
2405
2740
  return dirname$2(normalized);
2406
2741
  }
2407
2742
  function dirname$2(filePath) {
2408
- return trimTrailingSlashes$1(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
2743
+ return trimTrailingSlashes$2(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
2409
2744
  }
2410
- function trimTrailingSlashes$1(value) {
2745
+ function trimTrailingSlashes$2(value) {
2411
2746
  return value.replace(/[/\\]+$/, "");
2412
2747
  }
2413
2748
  function isAbsolutePath(value) {
@@ -2416,9 +2751,15 @@ function isAbsolutePath(value) {
2416
2751
  function isDefaultMemberName(ownerType, kind, name) {
2417
2752
  if (kind === "controller") return (ownerType === "GButton" || ownerType === "GComboBox") && name === "button";
2418
2753
  if (kind === "transition") return false;
2419
- if (ownerType === "GButton" || ownerType === "GLabel" || ownerType === "GComboBox") return name === "title" || name === "icon";
2420
- if (ownerType === "GProgressBar") return name === "bar" || name === "bar_v" || name === "title" || name === "ani";
2421
- if (ownerType === "GSlider") return name === "bar" || name === "bar_v" || name === "grip" || name === "title" || name === "ani";
2754
+ if (ownerType === "GButton" || ownerType === "GLabel" || ownerType === "GComboBox") {
2755
+ if (name === "title" || name === "icon") return true;
2756
+ }
2757
+ if (ownerType === "GProgressBar") {
2758
+ if (name === "bar" || name === "bar_v" || name === "title" || name === "ani") return true;
2759
+ }
2760
+ if (ownerType === "GSlider") {
2761
+ if (name === "bar" || name === "bar_v" || name === "grip" || name === "title" || name === "ani") return true;
2762
+ }
2422
2763
  return /^n\d+(?:_.*)?$/i.test(name);
2423
2764
  }
2424
2765
  function applyMemberNamePrefix(name, prefix) {
@@ -2707,11 +3048,11 @@ function inferPackageName(fileName) {
2707
3048
  if (/\.fui$/i.test(fileName)) return fileName.replace(/\.fui$/i, "");
2708
3049
  return fileName.replace(/\.bin$/i, "");
2709
3050
  }
2710
- function trimTrailingSlashes(value) {
3051
+ function trimTrailingSlashes$1(value) {
2711
3052
  return value.replace(/[/\\]+$/, "");
2712
3053
  }
2713
3054
  function normalizeComparablePath(value) {
2714
- const normalized = trimTrailingSlashes(value).replace(/\\/g, "/");
3055
+ const normalized = trimTrailingSlashes$1(value).replace(/\\/g, "/");
2715
3056
  const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
2716
3057
  const drivePrefix = driveMatch?.[1].toLowerCase() ?? "";
2717
3058
  const remainder = driveMatch ? driveMatch[2] ?? "" : normalized;
@@ -2731,14 +3072,14 @@ function normalizeComparablePath(value) {
2731
3072
  return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
2732
3073
  }
2733
3074
  function dirname$1(filePath) {
2734
- return trimTrailingSlashes(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
3075
+ return trimTrailingSlashes$1(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
2735
3076
  }
2736
3077
  function basename(filePath) {
2737
- return trimTrailingSlashes(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
3078
+ return trimTrailingSlashes$1(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
2738
3079
  }
2739
3080
  function resolveOutputProjectPath(output, fs) {
2740
3081
  if (/\.fairy$/i.test(output)) return output;
2741
- const normalizedOutput = trimTrailingSlashes(output);
3082
+ const normalizedOutput = trimTrailingSlashes$1(output);
2742
3083
  const projectName = basename(normalizedOutput) || "Restored";
2743
3084
  return fs.join(normalizedOutput, `${projectName}.fairy`);
2744
3085
  }
@@ -2784,7 +3125,7 @@ async function prepareRestoreOutputDir(inputDir, outputDir, outputProjectPath, f
2784
3125
  await fs.mkdir(outputDir);
2785
3126
  }
2786
3127
  async function restore(options) {
2787
- const sourceDir = trimTrailingSlashes(options.inputDir);
3128
+ const sourceDir = trimTrailingSlashes$1(options.inputDir);
2788
3129
  const outputIsProjectFile = /\.fairy$/i.test(options.output);
2789
3130
  const outputProjectPath = resolveOutputProjectPath(options.output, options.fs);
2790
3131
  await prepareRestoreOutputDir(sourceDir, dirname$1(outputProjectPath) || ".", outputProjectPath, options.fs, options.force === true, outputIsProjectFile);
@@ -2926,16 +3267,23 @@ var RestoreWorkflow = class {
2926
3267
  async _ensureLooseImageResource(doc, pkg, owner, sourceDir, fileName) {
2927
3268
  const resources = pkg.listResources();
2928
3269
  const existing = this._findResourceByFile(resources, owner, "ImageResource", fileName);
2929
- if (existing) return existing;
2930
3270
  const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
2931
- if (!sourcePath) return null;
3271
+ if (!sourcePath) return existing ?? null;
3272
+ if (existing) {
3273
+ existing.setExtras?.({
3274
+ ...existing.getExtras?.() ?? {},
3275
+ _publishedFile: fileBaseName(sourcePath),
3276
+ _restoreAsLooseImage: true
3277
+ });
3278
+ return existing;
3279
+ }
2932
3280
  const resource = doc.createImageResource(stripExtension(fileName));
2933
3281
  resource.setId(generateId()).setPath(owner.getPath?.() ?? "/").setBranch(owner.getBranch?.() ?? "").setBranchItemIds(owner.getBranchItemIds?.() ?? []).setExported(false).setFileName(fileName);
2934
3282
  resource.setExtras?.({
2935
3283
  ...resource.getExtras?.() ?? {},
2936
3284
  _publishedFile: fileBaseName(sourcePath),
2937
3285
  _suppressPackageSize: true,
2938
- _syntheticLooseImage: true
3286
+ _restoreAsLooseImage: true
2939
3287
  });
2940
3288
  pkg.addResource(resource);
2941
3289
  return resource;
@@ -3109,13 +3457,13 @@ var RestoreWorkflow = class {
3109
3457
  }
3110
3458
  async _copyLooseResources(pkg, options, warnings) {
3111
3459
  for (const resource of pkg.listResources()) {
3112
- const syntheticLooseImage = resource.getExtras?.()?._syntheticLooseImage === true;
3460
+ const restoreAsLooseImage = resource.getExtras?.()?._restoreAsLooseImage === true;
3113
3461
  if (![
3114
3462
  "SoundResource",
3115
3463
  "MiscResource",
3116
3464
  "SpineResource",
3117
3465
  "DragonBonesResource"
3118
- ].includes(resource.propertyType) && !syntheticLooseImage) continue;
3466
+ ].includes(resource.propertyType) && !restoreAsLooseImage) continue;
3119
3467
  const fileName = resourceFileName(resource);
3120
3468
  if (!fileName) continue;
3121
3469
  const sourcePath = await this._resolveSourceFile(options.sourceDir, this._sourceFileCandidates(pkg, resourcePublishedFileName(resource), fileName));
@@ -3285,6 +3633,26 @@ var RestoreWorkflow = class {
3285
3633
  };
3286
3634
  //#endregion
3287
3635
  //#region src/publish.ts
3636
+ async function runPublishPluginHook(plugins, hook, doc, options) {
3637
+ const logger = doc.getLogger();
3638
+ for (const plugin of plugins) {
3639
+ const fn = plugin.plugin[hook];
3640
+ if (typeof fn !== "function") continue;
3641
+ try {
3642
+ await fn(doc, options);
3643
+ } catch (error) {
3644
+ logger.warn(`publish: Plugin "${plugin.name}" ${hook} failed: ${formatPluginError(error)}`);
3645
+ }
3646
+ }
3647
+ }
3648
+ function resolvePublishPluginsDir(doc, options) {
3649
+ const fs = options.fs;
3650
+ const projectDir = doc.getProjectDir?.() ?? "";
3651
+ if (projectDir) return fs?.join ? fs.join(projectDir, "plugins") : `${projectDir.replace(/[/\\]+$/, "")}/plugins`;
3652
+ const projectBasePath = resolveProjectBasePath(options.basePath);
3653
+ if (!projectBasePath) return "";
3654
+ return fs?.join ? fs.join(projectBasePath, "plugins") : `${projectBasePath.replace(/[/\\]+$/, "")}/plugins`;
3655
+ }
3288
3656
  const UNITY_PROJECT_TYPE = ProjectType.Unity;
3289
3657
  const COCOS_CREATOR_PROJECT_TYPE = ProjectType.CocosCreator;
3290
3658
  function resolveDefaultPublishFileExtension(projectType, publishSettings) {
@@ -3334,6 +3702,19 @@ function resolvePublishOptions(doc, overrides = {}) {
3334
3702
  atlas: atlasOptions
3335
3703
  };
3336
3704
  }
3705
+ function trimTrailingSlashes(value) {
3706
+ return value.replace(/[/\\]+$/, "");
3707
+ }
3708
+ function isAbsolutePathLike(value) {
3709
+ return /^(?:[a-zA-Z]:[/\\]|[/\\]{1,2})/u.test(value);
3710
+ }
3711
+ function joinPathSegments(left, right) {
3712
+ const normalizedLeft = trimTrailingSlashes(left);
3713
+ const normalizedRight = right.replace(/^[/\\]+/, "");
3714
+ if (!normalizedLeft) return normalizedRight;
3715
+ if (!normalizedRight) return normalizedLeft;
3716
+ return `${normalizedLeft}${normalizedLeft.includes("\\") ? "\\" : "/"}${normalizedRight}`;
3717
+ }
3337
3718
  function dirname(filePath) {
3338
3719
  return filePath.replace(/[/\\]+$/, "").match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
3339
3720
  }
@@ -3364,6 +3745,9 @@ function isImageResource(resource) {
3364
3745
  function isMovieClipResource(resource) {
3365
3746
  return resource.propertyType === "MovieClipResource";
3366
3747
  }
3748
+ function isHighResolutionResource(resource) {
3749
+ return isImageResource(resource) || isMovieClipResource(resource);
3750
+ }
3367
3751
  function isMiscResource(resource) {
3368
3752
  return resource.propertyType === "MiscResource";
3369
3753
  }
@@ -3446,13 +3830,14 @@ function extname(fileName) {
3446
3830
  if (lastDot <= lastSlash) return "";
3447
3831
  return normalized.slice(lastDot);
3448
3832
  }
3449
- function resolvePublishedMiscFileName(resource) {
3833
+ function resolvePublishedMiscFileName(resource, projectType) {
3450
3834
  const file = resource.getFile();
3835
+ if (projectType !== UNITY_PROJECT_TYPE) return file;
3451
3836
  if (file.toLowerCase().endsWith(".atlas")) return `${file}.txt`;
3452
3837
  return file;
3453
3838
  }
3454
- function resolvePublishedSkeletonFileName(resource) {
3455
- if (isSpineResource(resource) && resource.getFile().toLowerCase().endsWith(".skel")) return `${resource.getFile()}.bytes`;
3839
+ function resolvePublishedSkeletonFileName(resource, projectType) {
3840
+ if (projectType === UNITY_PROJECT_TYPE && isSpineResource(resource) && resource.getFile().toLowerCase().endsWith(".skel")) return `${resource.getFile()}.bytes`;
3456
3841
  return resource.getFile();
3457
3842
  }
3458
3843
  function setPublishedFileExtra(resource, fileName) {
@@ -3484,6 +3869,70 @@ function getBranchName(resource) {
3484
3869
  function buildBranchResourceKey(resource) {
3485
3870
  return `${resource.propertyType}|${resource.getPath() ?? ""}|${resource.getName() ?? ""}`;
3486
3871
  }
3872
+ const HIGH_RESOLUTION_LEVELS = [
3873
+ {
3874
+ scale: 2,
3875
+ bit: 1,
3876
+ slot: 0
3877
+ },
3878
+ {
3879
+ scale: 3,
3880
+ bit: 2,
3881
+ slot: 1
3882
+ },
3883
+ {
3884
+ scale: 4,
3885
+ bit: 4,
3886
+ slot: 2
3887
+ }
3888
+ ];
3889
+ function buildHighResolutionResourceKey(resource, name = resource.getName()) {
3890
+ return `${resource.propertyType}|${resource.getBranch?.() ?? ""}|${resource.getPath() ?? ""}|${name}`;
3891
+ }
3892
+ function isHighResolutionVariantName(name) {
3893
+ return /@(?:2|3|4)x(?:\.[^./\\]+)?$/iu.test(name);
3894
+ }
3895
+ function appendHighResolutionScaleToName(name, scale) {
3896
+ const extensionIndex = name.lastIndexOf(".");
3897
+ if (extensionIndex > 0) return `${name.slice(0, extensionIndex)}@${scale}x${name.slice(extensionIndex)}`;
3898
+ return `${name}@${scale}x`;
3899
+ }
3900
+ function trimTrailingMissingHighResolutionIds(ids) {
3901
+ while (ids.length > 0 && !ids[ids.length - 1]) ids.pop();
3902
+ return ids;
3903
+ }
3904
+ function collectHighResolutionItemIds(resources, publishedResourceIds, includeHighResolution) {
3905
+ const result = /* @__PURE__ */ new Map();
3906
+ if (includeHighResolution <= 0) return result;
3907
+ const highResolutionResourceByKey = /* @__PURE__ */ new Map();
3908
+ for (const resource of resources) {
3909
+ if (!isHighResolutionResource(resource)) continue;
3910
+ highResolutionResourceByKey.set(buildHighResolutionResourceKey(resource), resource);
3911
+ }
3912
+ for (const resource of resources) {
3913
+ if (!isHighResolutionResource(resource)) continue;
3914
+ if (!publishedResourceIds.has(resource.getId())) continue;
3915
+ if (isHighResolutionVariantName(resource.getName())) continue;
3916
+ const ids = [];
3917
+ for (const level of HIGH_RESOLUTION_LEVELS) {
3918
+ if ((includeHighResolution & level.bit) === 0) {
3919
+ ids[level.slot] = null;
3920
+ continue;
3921
+ }
3922
+ const highResolutionResource = highResolutionResourceByKey.get(buildHighResolutionResourceKey(resource, appendHighResolutionScaleToName(resource.getName(), level.scale)));
3923
+ if (!highResolutionResource) {
3924
+ ids[level.slot] = null;
3925
+ continue;
3926
+ }
3927
+ const highResolutionId = highResolutionResource.getId();
3928
+ publishedResourceIds.add(highResolutionId);
3929
+ ids[level.slot] = highResolutionId;
3930
+ }
3931
+ trimTrailingMissingHighResolutionIds(ids);
3932
+ if (ids.length > 0) result.set(resource.getId(), ids);
3933
+ }
3934
+ return result;
3935
+ }
3487
3936
  function collectPackagePublishContext(pkg, options) {
3488
3937
  const pkgId = pkg.getId();
3489
3938
  const resources = pkg.listResources();
@@ -3491,6 +3940,24 @@ function collectPackagePublishContext(pkg, options) {
3491
3940
  const referencedIds = /* @__PURE__ */ new Set();
3492
3941
  const pixelHitTestImageIds = /* @__PURE__ */ new Set();
3493
3942
  const spriteItemIds = /* @__PURE__ */ new Set();
3943
+ const collectExportedResourceIds = (sourceResources, sourcePublishedResourceIds) => {
3944
+ const exportedResourceIds = new Set(sourcePublishedResourceIds);
3945
+ const resourcesById = new Map(sourceResources.map((resource) => [resource.getId(), resource]));
3946
+ let changed = true;
3947
+ while (changed) {
3948
+ changed = false;
3949
+ for (const resourceId of [...exportedResourceIds]) {
3950
+ const resource = resourcesById.get(resourceId);
3951
+ if (!resource || !isSkeletonResource(resource)) continue;
3952
+ for (const requiredId of resource.getRequireIds()) {
3953
+ if (!requiredId || exportedResourceIds.has(requiredId)) continue;
3954
+ exportedResourceIds.add(requiredId);
3955
+ changed = true;
3956
+ }
3957
+ }
3958
+ }
3959
+ return exportedResourceIds;
3960
+ };
3494
3961
  for (const atlas of pkg.listAtlases()) for (const sprite of atlas.listSprites()) spriteItemIds.add(sprite.getItemId());
3495
3962
  for (const resource of resources) {
3496
3963
  if (!isComponentResource(resource)) continue;
@@ -3517,6 +3984,7 @@ function collectPackagePublishContext(pkg, options) {
3517
3984
  child.getSelectedIcon?.(),
3518
3985
  child.getDropdown?.(),
3519
3986
  child.getSound?.(),
3987
+ child.getInstanceSound?.(),
3520
3988
  child.getInstanceIcon?.(),
3521
3989
  child.getInstanceSelectedIcon?.(),
3522
3990
  child.getVtScrollBarRes?.(),
@@ -3574,19 +4042,8 @@ function collectPackagePublishContext(pkg, options) {
3574
4042
  }
3575
4043
  if (resource.getExported() || referencedIds.has(resourceId)) publishedResourceIds.add(resourceId);
3576
4044
  }
3577
- let changed = true;
3578
- while (changed) {
3579
- changed = false;
3580
- for (const resource of resources) {
3581
- if (!isSkeletonResource(resource)) continue;
3582
- if (!publishedResourceIds.has(resource.getId())) continue;
3583
- for (const requiredId of resource.getRequireIds()) {
3584
- if (!requiredId || publishedResourceIds.has(requiredId)) continue;
3585
- publishedResourceIds.add(requiredId);
3586
- changed = true;
3587
- }
3588
- }
3589
- }
4045
+ for (const resourceId of collectExportedResourceIds(resources, publishedResourceIds)) publishedResourceIds.add(resourceId);
4046
+ const highResolutionItemIds = collectHighResolutionItemIds(resources, publishedResourceIds, options.includeHighResolution);
3590
4047
  if (!options.includeBranches) {
3591
4048
  const mainByKey = /* @__PURE__ */ new Map();
3592
4049
  const activeBranchByKey = /* @__PURE__ */ new Map();
@@ -3634,7 +4091,9 @@ function collectPackagePublishContext(pkg, options) {
3634
4091
  return {
3635
4092
  referencedIds,
3636
4093
  publishedResourceIds,
4094
+ exportedResourceIds: collectExportedResourceIds(resources, publishedResourceIds),
3637
4095
  pixelHitTestImageIds,
4096
+ highResolutionItemIds,
3638
4097
  effectiveResourceIds,
3639
4098
  includeBranches: false
3640
4099
  };
@@ -3642,7 +4101,9 @@ function collectPackagePublishContext(pkg, options) {
3642
4101
  return {
3643
4102
  referencedIds,
3644
4103
  publishedResourceIds,
4104
+ exportedResourceIds: collectExportedResourceIds(resources, publishedResourceIds),
3645
4105
  pixelHitTestImageIds,
4106
+ highResolutionItemIds,
3646
4107
  effectiveResourceIds: new Map([...publishedResourceIds].map((resourceId) => [resourceId, resourceId])),
3647
4108
  includeBranches: true
3648
4109
  };
@@ -3691,28 +4152,36 @@ async function applyPixelHitTests(pkg, imageIds, basePath, encoder) {
3691
4152
  }
3692
4153
  }
3693
4154
  async function annotatePackagePublishArtifacts(pkg, basePath, encoder, options) {
3694
- const { publishedResourceIds, pixelHitTestImageIds, effectiveResourceIds, includeBranches } = collectPackagePublishContext(pkg, options);
3695
- for (const resource of pkg.listResources()) setPublishedIdExtra(resource, effectiveResourceIds.get(resource.getId()) ?? null);
4155
+ const { publishedResourceIds, exportedResourceIds, pixelHitTestImageIds, highResolutionItemIds, effectiveResourceIds, includeBranches } = collectPackagePublishContext(pkg, options);
4156
+ for (const resource of pkg.listResources()) {
4157
+ setPublishedIdExtra(resource, effectiveResourceIds.get(resource.getId()) ?? null);
4158
+ if (isHighResolutionResource(resource)) resource.setHighResolutionItemIds(highResolutionItemIds.get(resource.getId()) ?? []);
4159
+ }
3696
4160
  await applyPixelHitTests(pkg, pixelHitTestImageIds, basePath, encoder);
3697
4161
  const extras = pkg.getExtras() ?? {};
3698
4162
  pkg.setExtras({
3699
4163
  ...extras,
3700
4164
  publishedResourceIds: [...publishedResourceIds].sort((a, b) => a.localeCompare(b)),
4165
+ exportedResourceIds: [...exportedResourceIds].sort((a, b) => a.localeCompare(b)),
3701
4166
  publishedIncludeBranches: includeBranches,
3702
4167
  publishedEffectiveResourceIds: Object.fromEntries(effectiveResourceIds)
3703
4168
  });
3704
4169
  for (const resource of pkg.listResources()) {
3705
4170
  if (isMiscResource(resource)) {
3706
- setPublishedFileExtra(resource, resolvePublishedMiscFileName(resource));
4171
+ setPublishedFileExtra(resource, resolvePublishedMiscFileName(resource, options.projectType));
3707
4172
  continue;
3708
4173
  }
3709
- if (isSkeletonResource(resource)) setPublishedFileExtra(resource, resolvePublishedSkeletonFileName(resource));
4174
+ if (isSkeletonResource(resource)) setPublishedFileExtra(resource, resolvePublishedSkeletonFileName(resource, options.projectType));
3710
4175
  }
3711
4176
  }
3712
4177
  function getAnnotatedPublishedResourceIds(pkg) {
3713
4178
  const extras = pkg.getExtras() ?? {};
3714
4179
  return new Set(extras.publishedResourceIds ?? []);
3715
4180
  }
4181
+ function getAnnotatedExportedResourceIds(pkg) {
4182
+ const extras = pkg.getExtras() ?? {};
4183
+ return new Set(extras.exportedResourceIds ?? []);
4184
+ }
3716
4185
  function getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds) {
3717
4186
  const imageIds = /* @__PURE__ */ new Set();
3718
4187
  const resourcesById = new Map(pkg.listResources().map((resource) => [resource.getId(), resource]));
@@ -3751,18 +4220,18 @@ async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw, lo
3751
4220
  }
3752
4221
  }
3753
4222
  async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw, logger) {
3754
- const publishedResourceIds = getAnnotatedPublishedResourceIds(pkg);
3755
- const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds);
3756
- if (publishedResourceIds.size === 0) return;
4223
+ const exportedResourceIds = getAnnotatedExportedResourceIds(pkg);
4224
+ const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, exportedResourceIds);
4225
+ if (exportedResourceIds.size === 0) return;
3757
4226
  if (!basePath || !readFileRaw) {
3758
4227
  if (pkg.listResources().some((resource) => {
3759
- return (isMiscResource(resource) || isSkeletonResource(resource)) && publishedResourceIds.has(resource.getId()) || skeletonDependencyImageIds.has(resource.getId());
4228
+ return (isMiscResource(resource) || isSkeletonResource(resource)) && exportedResourceIds.has(resource.getId()) || skeletonDependencyImageIds.has(resource.getId());
3760
4229
  })) logger.warn(`publish: External resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`);
3761
4230
  return;
3762
4231
  }
3763
4232
  for (const resource of pkg.listResources()) {
3764
4233
  const resourceId = resource.getId();
3765
- const isSkeletonExternal = publishedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
4234
+ const isSkeletonExternal = exportedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
3766
4235
  const isSkeletonImageDependency = skeletonDependencyImageIds.has(resourceId) && isImageResource(resource);
3767
4236
  if (!isSkeletonExternal && !isSkeletonImageDependency) continue;
3768
4237
  let sourcePath;
@@ -3808,16 +4277,102 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
3808
4277
  */
3809
4278
  function publish(options) {
3810
4279
  return createTransform("publish", async (doc) => {
4280
+ const resolveConfiguredOutputPath = (value, projectBasePath) => {
4281
+ const trimmed = value?.trim();
4282
+ if (!trimmed) return void 0;
4283
+ if (isAbsolutePathLike(trimmed) || !projectBasePath) return trimTrailingSlashes(trimmed);
4284
+ return trimTrailingSlashes(options.fs ? options.fs.join(projectBasePath, trimmed) : joinPathSegments(projectBasePath, trimmed));
4285
+ };
4286
+ const resolveProjectPublishConfig = () => {
4287
+ const publishSettings = (doc.getRoot().getSettings?.() ?? {}).publish ?? {};
4288
+ const resolved = resolvePublishOptions(doc, {
4289
+ compressed: options.compressed,
4290
+ fileExtension: options.fileExtension,
4291
+ packages: options.packages,
4292
+ atlas: options.atlas
4293
+ });
4294
+ const includeBranches = (publishSettings.branchProcessing ?? 0) === 0;
4295
+ return {
4296
+ ...resolved,
4297
+ projectType: doc.getRoot().getProjectType(),
4298
+ includeBranches,
4299
+ activeBranch: includeBranches ? "" : options.branch ?? "",
4300
+ includeHighResolution: publishSettings.includeHighResolution ?? 0,
4301
+ separatedAtlasForBranch: includeBranches && publishSettings.seperatedAtlasForBranch === true,
4302
+ globalOutputPath: publishSettings.path?.trim() ?? "",
4303
+ globalBranchOutputPath: publishSettings.branchPath?.trim() ?? ""
4304
+ };
4305
+ };
4306
+ const resolvePackagePublishPlan = (pkg, config, projectBasePath) => {
4307
+ let outputDir;
4308
+ if (options.output) outputDir = trimTrailingSlashes(options.output);
4309
+ else {
4310
+ const candidates = [];
4311
+ if (!config.includeBranches && config.activeBranch) candidates.push(pkg.getPublishBranchPath(), config.globalBranchOutputPath);
4312
+ candidates.push(pkg.getPublishPath(), config.globalOutputPath);
4313
+ for (const candidate of candidates) {
4314
+ const resolved = resolveConfiguredOutputPath(candidate, projectBasePath);
4315
+ if (!resolved) continue;
4316
+ outputDir = resolved;
4317
+ break;
4318
+ }
4319
+ }
4320
+ const publishName = pkg.getPublishName() || pkg.getName();
4321
+ return {
4322
+ pkg,
4323
+ outputDir,
4324
+ publishName,
4325
+ fileName: resolvePublishFileName(publishName, config.fileExtension),
4326
+ compressed: config.compressed,
4327
+ fileExtension: config.fileExtension,
4328
+ includeBranches: config.includeBranches,
4329
+ activeBranch: config.activeBranch,
4330
+ includeHighResolution: config.includeHighResolution,
4331
+ separatedAtlasForBranch: config.separatedAtlasForBranch,
4332
+ atlas: config.atlas
4333
+ };
4334
+ };
4335
+ const createNoopPublishFs = () => ({
4336
+ async writeFileRaw() {},
4337
+ async mkdir() {},
4338
+ join(...paths) {
4339
+ return paths.join("/");
4340
+ }
4341
+ });
4342
+ const publishPackage = async (plan, writerFs, packageIndex) => {
4343
+ const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(plan.fileExtension);
4344
+ await atlas({
4345
+ ...plan.atlas,
4346
+ ...options.atlas ?? {},
4347
+ separatedAtlasForBranch: plan.separatedAtlasForBranch,
4348
+ encoder: options.encoder,
4349
+ basePath: options.basePath,
4350
+ outputPath: options.fs ? plan.outputDir : void 0,
4351
+ mkdir: options.fs ? options.fs.mkdir : void 0,
4352
+ readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
4353
+ packages: [plan.pkg.getName()],
4354
+ ...atlasRuntimeOptions
4355
+ })(doc);
4356
+ if (!options.fs) return;
4357
+ if (!plan.outputDir) throw new Error("publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.");
4358
+ await options.fs.mkdir(plan.outputDir);
4359
+ const filePath = options.fs.join(plan.outputDir, plan.fileName);
4360
+ const bwOptions = {
4361
+ compressed: plan.compressed,
4362
+ packageIndex
4363
+ };
4364
+ await new BinaryWriter(writerFs).write(doc, filePath, bwOptions);
4365
+ await exportPackageSounds(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
4366
+ await exportPackageExternalResources(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
4367
+ logger.info(`publish: Written ${plan.fileName}`);
4368
+ };
3811
4369
  const root = doc.getRoot();
3812
4370
  const logger = doc.getLogger();
3813
- const publishSettings = (root.getSettings?.() ?? {}).publish ?? {};
3814
- const resolved = resolvePublishOptions(doc, {
3815
- compressed: options.compressed,
3816
- fileExtension: options.fileExtension,
3817
- packages: options.packages,
3818
- atlas: options.atlas
3819
- });
3820
- const ext = resolved.fileExtension;
4371
+ const projectBasePath = resolveProjectBasePath(options.basePath) || doc.getProjectDir?.() || "";
4372
+ const pluginsDir = resolvePublishPluginsDir(doc, options);
4373
+ const plugins = pluginsDir ? await loadPlugins(doc, pluginsDir) : [];
4374
+ await runPublishPluginHook(plugins, "onPublishStart", doc, options);
4375
+ const resolved = resolveProjectPublishConfig();
3821
4376
  let allPackages = root.listPackages();
3822
4377
  if (resolved.packages && resolved.packages.length > 0) {
3823
4378
  const names = new Set(resolved.packages);
@@ -3825,57 +4380,42 @@ function publish(options) {
3825
4380
  }
3826
4381
  if (allPackages.length === 0) {
3827
4382
  logger.warn("publish: No packages to publish.");
4383
+ await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
3828
4384
  return;
3829
4385
  }
3830
- const includeBranches = (publishSettings.branchProcessing ?? 0) === 0;
3831
- const activeBranch = includeBranches ? "" : options.branch ?? "";
3832
- const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(ext);
3833
4386
  const allDocPackages = root.listPackages();
3834
4387
  const pkgMap = /* @__PURE__ */ new Map();
3835
4388
  for (const p of allDocPackages) pkgMap.set(p.getId(), p);
3836
4389
  for (const pkg of allPackages) {
3837
- _computeDependencies(pkg, pkgMap);
4390
+ _computeDependencies(doc, pkg, pkgMap);
3838
4391
  await annotatePackagePublishArtifacts(pkg, options.basePath, options.encoder, {
3839
- includeBranches,
3840
- activeBranch
4392
+ projectType: resolved.projectType,
4393
+ includeBranches: resolved.includeBranches,
4394
+ activeBranch: resolved.activeBranch,
4395
+ includeHighResolution: resolved.includeHighResolution
3841
4396
  });
3842
4397
  }
3843
- await atlas({
3844
- ...resolved.atlas,
3845
- ...options.atlas ?? {},
3846
- separatedAtlasForBranch: includeBranches && publishSettings.seperatedAtlasForBranch === true,
3847
- encoder: options.encoder,
3848
- basePath: options.basePath,
3849
- outputPath: options.fs ? options.output : void 0,
3850
- mkdir: options.fs ? options.fs.mkdir : void 0,
3851
- readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
3852
- ...atlasRuntimeOptions
3853
- })(doc);
4398
+ const plans = allPackages.map((pkg) => resolvePackagePublishPlan(pkg, resolved, projectBasePath));
3854
4399
  if (!options.fs) {
3855
4400
  logger.info(`publish: No fs provided — layout computed for ${allPackages.length} package(s), skipping file output.`);
4401
+ const noopWriterFs = toBinaryWriterFileSystem(createNoopPublishFs());
4402
+ for (const plan of plans) await publishPackage(plan, noopWriterFs, allDocPackages.indexOf(plan.pkg));
4403
+ await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
3856
4404
  return;
3857
4405
  }
3858
- await options.fs.mkdir(options.output);
4406
+ const unresolvedPlan = plans.find((plan) => !plan.outputDir);
4407
+ if (unresolvedPlan) throw new Error(`publish: no output directory resolved for package "${unresolvedPlan.pkg.getName()}". Provide --output, or configure global publish.path / package publishPath.`);
3859
4408
  const writerFs = toBinaryWriterFileSystem(options.fs);
3860
- for (const pkg of allPackages) {
3861
- const pkgIndex = allDocPackages.indexOf(pkg);
3862
- const fileName = resolvePublishFileName(pkg.getPublishName() || pkg.getName(), ext);
3863
- const filePath = options.fs.join(options.output, fileName);
3864
- const bwOptions = {
3865
- compressed: resolved.compressed,
3866
- packageIndex: pkgIndex
3867
- };
3868
- await new BinaryWriter(writerFs).write(doc, filePath, bwOptions);
3869
- await exportPackageSounds(pkg, options.output, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
3870
- await exportPackageExternalResources(pkg, options.output, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
3871
- logger.info(`publish: Written ${fileName}`);
3872
- }
4409
+ for (const plan of plans) await publishPackage(plan, writerFs, allDocPackages.indexOf(plan.pkg));
3873
4410
  await publishCodeGeneration(doc, {
3874
4411
  basePath: options.basePath,
3875
4412
  fs: options.fs,
3876
- packages: allPackages
4413
+ packages: allPackages,
4414
+ plugins
3877
4415
  });
3878
- logger.info(`publish: Published ${allPackages.length} package(s) to ${options.output}`);
4416
+ const publishedTargets = [...new Set(plans.map((plan) => plan.outputDir).filter((value) => Boolean(value)))];
4417
+ logger.info(publishedTargets.length > 0 ? `publish: Published ${allPackages.length} package(s) to ${publishedTargets.join(", ")}` : `publish: Published ${allPackages.length} package(s)`);
4418
+ await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
3879
4419
  });
3880
4420
  }
3881
4421
  /**
@@ -3883,25 +4423,104 @@ function publish(options) {
3883
4423
  * The editor only adds dependencies for packages referenced via bitmap font URLs.
3884
4424
  * @internal
3885
4425
  */
3886
- function _computeDependencies(pkg, pkgMap) {
4426
+ function _computeDependencies(doc, pkg, pkgMap) {
3887
4427
  const referencedPkgIds = /* @__PURE__ */ new Set();
3888
- function scanFontUrl(font) {
3889
- if (!font) return;
3890
- const fontStr = Array.isArray(font) ? font[0] : String(font);
3891
- if (typeof fontStr !== "string" || !fontStr.startsWith("ui://")) return;
3892
- const rest = fontStr.slice(5);
3893
- if (rest.length >= 8) {
3894
- const depPkgId = rest.slice(0, 8);
3895
- if (depPkgId !== pkg.getId()) referencedPkgIds.add(depPkgId);
4428
+ const pkgId = pkg.getId();
4429
+ const packageOrder = new Map(doc.getRoot().listPackages().map((entry, index) => [entry.getId(), index]));
4430
+ const addDependencyPackageId = (dependencyPkgId) => {
4431
+ const normalized = dependencyPkgId?.trim() ?? "";
4432
+ if (!normalized || normalized === pkgId) return;
4433
+ referencedPkgIds.add(normalized);
4434
+ };
4435
+ const extractPackageIdFromUiUrl = (value) => {
4436
+ if (!value.startsWith("ui://")) return null;
4437
+ const rest = value.slice(5);
4438
+ if (!rest) return null;
4439
+ const slashIndex = rest.indexOf("/");
4440
+ if (slashIndex >= 0) return rest.slice(0, slashIndex) || null;
4441
+ if (rest.length >= 8) return rest.slice(0, 8);
4442
+ return null;
4443
+ };
4444
+ const addDependencyPackageIdFromUiValue = (value) => {
4445
+ if (!value || typeof value !== "string") return;
4446
+ addDependencyPackageId(extractPackageIdFromUiUrl(value));
4447
+ };
4448
+ const addDependencyPackageIdsFromText = (value) => {
4449
+ if (!value || typeof value !== "string") return;
4450
+ const matches = value.matchAll(/ui:\/\/([0-9a-z]{8})/giu);
4451
+ for (const match of matches) addDependencyPackageId(match[1] ?? "");
4452
+ };
4453
+ const addDependencyPackageIdsFromUnknown = (value) => {
4454
+ if (Array.isArray(value)) {
4455
+ for (const entry of value) addDependencyPackageIdsFromUnknown(entry);
4456
+ return;
3896
4457
  }
3897
- }
4458
+ if (typeof value === "string") {
4459
+ addDependencyPackageIdFromUiValue(value);
4460
+ addDependencyPackageIdsFromText(value);
4461
+ }
4462
+ };
4463
+ const addDependencyFontRef = (value) => {
4464
+ if (Array.isArray(value)) {
4465
+ for (const entry of value) addDependencyPackageIdFromUiValue(entry);
4466
+ return;
4467
+ }
4468
+ addDependencyPackageIdFromUiValue(value ?? void 0);
4469
+ };
3898
4470
  for (const res of pkg.listResources()) {
3899
4471
  if (res.propertyType !== "Component") continue;
3900
- for (const child of res.listChildren?.() ?? []) scanFontUrl(child.getFont?.());
4472
+ const component = res;
4473
+ for (const child of component.listChildren?.() ?? []) {
4474
+ addDependencyPackageId(child.getPackageId?.());
4475
+ addDependencyFontRef(child.getFont?.());
4476
+ addDependencyPackageIdsFromText(child.getText?.());
4477
+ for (const ref of [
4478
+ child.getUrl?.(),
4479
+ child.getDefaultItem?.(),
4480
+ child.getIcon?.(),
4481
+ child.getSelectedIcon?.(),
4482
+ child.getDropdown?.(),
4483
+ child.getSound?.(),
4484
+ child.getInstanceSound?.(),
4485
+ child.getInstanceIcon?.(),
4486
+ child.getInstanceSelectedIcon?.(),
4487
+ child.getVtScrollBarRes?.(),
4488
+ child.getHzScrollBarRes?.(),
4489
+ child.getHeaderRes?.(),
4490
+ child.getFooterRes?.()
4491
+ ]) addDependencyPackageIdFromUiValue(ref);
4492
+ for (const item of child.getInstanceComboItems?.() ?? []) addDependencyPackageIdFromUiValue(item.icon ?? void 0);
4493
+ for (const item of child.getListItems?.() ?? []) {
4494
+ addDependencyPackageIdFromUiValue(item.icon ?? void 0);
4495
+ addDependencyPackageIdFromUiValue(item.url ?? void 0);
4496
+ }
4497
+ for (const gear of child.listGears?.() ?? []) {
4498
+ addDependencyPackageIdsFromUnknown(gear.getValues?.());
4499
+ addDependencyPackageIdsFromUnknown(gear.getDefaultValue?.());
4500
+ }
4501
+ }
4502
+ addDependencyFontRef(component.getFont?.());
4503
+ for (const ref of [
4504
+ component.getDropdown?.(),
4505
+ component.getHeaderRes?.(),
4506
+ component.getFooterRes?.(),
4507
+ component.getVtScrollBarRes?.(),
4508
+ component.getHzScrollBarRes?.(),
4509
+ component.getSound?.()
4510
+ ]) addDependencyPackageIdFromUiValue(ref);
4511
+ for (const transition of component.listTransitions?.() ?? []) for (const item of transition.listItems?.() ?? []) {
4512
+ addDependencyPackageIdsFromUnknown(item.getStartValue?.());
4513
+ addDependencyPackageIdsFromUnknown(item.getEndValue?.());
4514
+ }
3901
4515
  }
3902
4516
  for (const dep of pkg.listDependencies()) pkg.removeDependency(dep);
3903
4517
  if (referencedPkgIds.size > 0) {
3904
- const sortedIds = [...referencedPkgIds].sort((a, b) => a.localeCompare(b));
4518
+ const sortedIds = [...referencedPkgIds].sort((a, b) => {
4519
+ const orderA = packageOrder.get(a) ?? Number.MAX_SAFE_INTEGER;
4520
+ const orderB = packageOrder.get(b) ?? Number.MAX_SAFE_INTEGER;
4521
+ if (orderA !== orderB) return orderA - orderB;
4522
+ return a.localeCompare(b);
4523
+ });
3905
4524
  for (const refId of sortedIds) {
3906
4525
  const depPkg = pkgMap.get(refId);
3907
4526
  if (depPkg) pkg.addDependency(depPkg);
@@ -3909,37 +4528,4 @@ function _computeDependencies(pkg, pkgMap) {
3909
4528
  }
3910
4529
  }
3911
4530
  //#endregion
3912
- //#region src/uam-transaction.ts
3913
- function mapTransactionErrorStage(error) {
3914
- switch (error.code) {
3915
- case "transaction_unsupported": return "preflight";
3916
- case "invalid_uam": return "preflight";
3917
- case "execution_failure": return error.opIndex === void 0 && error.issues !== void 0 ? "postflight" : "execution";
3918
- case "selector_ambiguity": return error.opIndex === void 0 ? "preflight" : "execution";
3919
- }
3920
- }
3921
- function applyUamTransactionApp(input) {
3922
- try {
3923
- return {
3924
- ok: true,
3925
- project: applyUamTransaction(input.project, input.operations)
3926
- };
3927
- } catch (error) {
3928
- if (error instanceof UamTransactionError) return {
3929
- ok: false,
3930
- error: {
3931
- code: error.code,
3932
- stage: mapTransactionErrorStage(error),
3933
- message: error.message,
3934
- opIndex: error.opIndex,
3935
- opId: error.opId,
3936
- opKind: error.opKind,
3937
- selector: error.selector,
3938
- issues: error.issues
3939
- }
3940
- };
3941
- throw error;
3942
- }
3943
- }
3944
- //#endregion
3945
- export { AUTO_GENERATED_CODE_MARK, ValidationSeverity, applyUamTransactionApp, atlas, createTransform, inspect, prune, publish, publishCodeGeneration, rename, resolvePublishOptions, restore, validate };
4531
+ export { AUTO_GENERATED_CODE_MARK, ValidationSeverity, applyUamTransactionApp, atlas, buildCodegenClasses, createTransform, decodeText, encodeText, inspect, prune, publish, publishCodeGeneration, rename, resolvePackageCodegenPlan, resolvePublishOptions, restore, validate };