pixelkiln 0.47.0 → 0.48.0

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/cli.js CHANGED
@@ -1374,6 +1374,8 @@ var LockHistoryEntrySchema = z.object({
1374
1374
  downloadedAt: z.string().nullable().default(null),
1375
1375
  cost: z.number().finite().nonnegative().default(0),
1376
1376
  costUnit: z.string().min(1).default("generations"),
1377
+ /** What the provider actually billed, read from the completed job; null when never read. */
1378
+ billed: z.object({ amount: z.number(), unit: z.string() }).nullable().default(null),
1377
1379
  provider: z.string().default("pixellab"),
1378
1380
  postprocess: PostprocessSchema.optional(),
1379
1381
  /** When the replacement was submitted. */
@@ -1465,6 +1467,14 @@ var LockEntrySchema = z.object({
1465
1467
  cost: z.number().finite().nonnegative().default(0),
1466
1468
  /** Unit for `cost`. Defaults preserve pre-unit PixelLab lockfiles. */
1467
1469
  costUnit: z.string().min(1).default("generations"),
1470
+ /**
1471
+ * What the provider actually billed, read from the completed job rather
1472
+ * than estimated at submit time. Null until a poll reads it, and for a
1473
+ * provider or generator `poll` does not read it from at all; `cost` above
1474
+ * is still what a wave budget spends against, since the real number is
1475
+ * only known after the fact.
1476
+ */
1477
+ billed: z.object({ amount: z.number(), unit: z.string() }).nullable().default(null),
1468
1478
  /** Which provider produced this. Absent on entries written before providers. */
1469
1479
  provider: z.string().default("pixellab"),
1470
1480
  /** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
@@ -5193,7 +5203,13 @@ var CharacterListSchema = z2.object({ characters: z2.array(CharacterSchema), tot
5193
5203
  var BackgroundJobSchema = z2.object({
5194
5204
  id: z2.string(),
5195
5205
  status: z2.string(),
5196
- last_response: z2.record(z2.unknown()).nullable().optional()
5206
+ last_response: z2.record(z2.unknown()).nullable().optional(),
5207
+ /**
5208
+ * The billed amount, confirmed live for a `map` object and a standard
5209
+ * character base (docs/ENDPOINTS.md, "Limits and billing"); duplicated at
5210
+ * `last_response.billing_usage` on the jobs observed so far.
5211
+ */
5212
+ usage: UsageSchema
5197
5213
  }).passthrough();
5198
5214
  var DeleteAnimationsSchema = z2.object({ success: z2.boolean(), deleted_count: z2.number().int().default(0), error: z2.string().nullable().optional() }).passthrough();
5199
5215
  function validateResponse(schema, raw, operation) {
@@ -5647,6 +5663,14 @@ function pixelLabObjectUrl(generator, objectId) {
5647
5663
  }
5648
5664
  return null;
5649
5665
  }
5666
+ function billedFromUsage(usage) {
5667
+ if (!usage) return null;
5668
+ if (usage.type === "usd" || usage.type === void 0 && typeof usage.usd === "number") {
5669
+ return typeof usage.usd === "number" ? { amount: usage.usd, unit: "usd" } : null;
5670
+ }
5671
+ if (typeof usage.generations === "number") return { amount: usage.generations, unit: "generations" };
5672
+ return null;
5673
+ }
5650
5674
  function characterCost(spec) {
5651
5675
  const character = spec.character;
5652
5676
  if (!character) return generationCost(spec.width, spec.height, "1dir");
@@ -6017,6 +6041,7 @@ var PixelLabProvider = class _PixelLabProvider {
6017
6041
  if (character.status !== "completed" || !character.rotation_urls) return { status: "processing" };
6018
6042
  const sources = rotationSources(character);
6019
6043
  if (!sources.length) return { status: "failed", error: "character completed with no rotation images" };
6044
+ const backgroundJobId = context?.metadata?.character?.backgroundJobId;
6020
6045
  return {
6021
6046
  status: "ready",
6022
6047
  objectId: character.id,
@@ -6032,7 +6057,8 @@ var PixelLabProvider = class _PixelLabProvider {
6032
6057
  size: character.size,
6033
6058
  updatedAt: character.updated_at ?? null
6034
6059
  }
6035
- }
6060
+ },
6061
+ billed: await this.billedForJob(backgroundJobId)
6036
6062
  };
6037
6063
  }
6038
6064
  /**
@@ -6044,7 +6070,7 @@ var PixelLabProvider = class _PixelLabProvider {
6044
6070
  */
6045
6071
  async pollCharacterAnimation(job, context) {
6046
6072
  const fps = context?.spec?.character?.animation?.fps ?? 8;
6047
- const review = (frames, animationId, groupId) => ({
6073
+ const review = (frames, animationId, groupId, billed) => ({
6048
6074
  status: "review-set",
6049
6075
  objectId: `${job.characterId}#${groupId ?? animationId ?? job.name}`,
6050
6076
  frameUrls: frames,
@@ -6060,7 +6086,8 @@ var PixelLabProvider = class _PixelLabProvider {
6060
6086
  animationName: job.name,
6061
6087
  direction: job.direction
6062
6088
  }
6063
- }
6089
+ },
6090
+ billed
6064
6091
  });
6065
6092
  let cleanedUp = job.jobIds.length === 0;
6066
6093
  for (const id of job.jobIds) {
@@ -6080,13 +6107,13 @@ var PixelLabProvider = class _PixelLabProvider {
6080
6107
  if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
6081
6108
  const character2 = await this.client.getCharacter(job.characterId);
6082
6109
  const group = findAnimation(character2, job.name, job.direction, animationId);
6083
- return review(frames, animationId, group?.groupId ?? null);
6110
+ return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
6084
6111
  }
6085
6112
  }
6086
6113
  const recorded = context?.metadata?.character?.animationId ?? null;
6087
6114
  const character = await this.client.getCharacter(job.characterId);
6088
6115
  const found = findAnimation(character, job.name, job.direction, recorded);
6089
- if (found) return review(found.frames, found.animationId, found.groupId);
6116
+ if (found) return review(found.frames, found.animationId, found.groupId, null);
6090
6117
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
6091
6118
  }
6092
6119
  async submit(spec, styleImages, context) {
@@ -6119,7 +6146,7 @@ var PixelLabProvider = class _PixelLabProvider {
6119
6146
  // unlike 1dir's {type, base64, format} payload.
6120
6147
  styleImages: styleImages.map(({ base64, width, height }) => ({ base64, width, height }))
6121
6148
  });
6122
- return { jobId: res2.tile_id };
6149
+ return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
6123
6150
  }
6124
6151
  if (spec.generator === "1dir") {
6125
6152
  const res2 = await this.client.create1Direction({
@@ -6128,7 +6155,7 @@ var PixelLabProvider = class _PixelLabProvider {
6128
6155
  view: spec.view === "sidescroller" ? "sidescroller" : "top-down",
6129
6156
  styleImages
6130
6157
  });
6131
- return { jobId: res2.object_id };
6158
+ return { jobId: res2.object_id, metadata: { backgroundJobId: res2.background_job_id } };
6132
6159
  }
6133
6160
  const res = await this.client.createMapObject({
6134
6161
  description: spec.prompt,
@@ -6140,7 +6167,22 @@ var PixelLabProvider = class _PixelLabProvider {
6140
6167
  detail: spec.detail,
6141
6168
  seed: spec.seed
6142
6169
  });
6143
- return { jobId: res.object_id };
6170
+ return { jobId: res.object_id, metadata: { backgroundJobId: res.background_job_id } };
6171
+ }
6172
+ /**
6173
+ * What a background job actually billed, once its own record is still
6174
+ * around to ask; see `billedFromUsage`. A stale or already-cleaned-up job
6175
+ * (a map object's record expires in hours; see `pollMap`) means "not
6176
+ * known", not a failure worth surfacing.
6177
+ */
6178
+ async billedForJob(backgroundJobId) {
6179
+ if (!backgroundJobId) return null;
6180
+ try {
6181
+ const job = await this.client.getBackgroundJob(backgroundJobId);
6182
+ return billedFromUsage(job.usage);
6183
+ } catch {
6184
+ return null;
6185
+ }
6144
6186
  }
6145
6187
  async poll(jobId, generator, context) {
6146
6188
  if (generator === "pixflux") {
@@ -6154,16 +6196,17 @@ var PixelLabProvider = class _PixelLabProvider {
6154
6196
  error: "pixflux result is no longer cached locally; re-run submit for this asset"
6155
6197
  };
6156
6198
  }
6157
- if (generator === "map") return this.pollMap(jobId);
6158
- if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature));
6199
+ if (generator === "map") return this.pollMap(jobId, context);
6200
+ if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
6159
6201
  if (generator === "character") return this.pollCharacter(jobId, context);
6202
+ const backgroundJobId = context?.metadata?.backgroundJobId;
6160
6203
  const obj = await this.client.getObject(jobId);
6161
6204
  if (obj.status === "review") {
6162
- return { status: "review", candidateUrls: obj.frame_urls ?? [] };
6205
+ return { status: "review", candidateUrls: obj.frame_urls ?? [], billed: await this.billedForJob(backgroundJobId) };
6163
6206
  }
6164
6207
  if (obj.status === "completed") {
6165
6208
  const url = firstUrl(obj.rotation_urls) ?? obj.preview_url ?? null;
6166
- return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [] };
6209
+ return { status: "ready", objectId: obj.id, sourceUrl: url, sources: url ? [{ url }] : [], billed: await this.billedForJob(backgroundJobId) };
6167
6210
  }
6168
6211
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
6169
6212
  return {
@@ -6179,7 +6222,8 @@ var PixelLabProvider = class _PixelLabProvider {
6179
6222
  * `/objects` four months on, with `/map-objects` returning 404 for the same
6180
6223
  * id. So a 404 here is not evidence the work is lost.
6181
6224
  */
6182
- async pollMap(jobId) {
6225
+ async pollMap(jobId, context) {
6226
+ const backgroundJobId = context?.metadata?.backgroundJobId;
6183
6227
  try {
6184
6228
  const obj = await this.client.getMapObject(jobId);
6185
6229
  if (obj.status === "completed" && obj.download_url) {
@@ -6187,7 +6231,8 @@ var PixelLabProvider = class _PixelLabProvider {
6187
6231
  status: "ready",
6188
6232
  objectId: jobId,
6189
6233
  sourceUrl: obj.download_url,
6190
- sources: [{ url: obj.download_url }]
6234
+ sources: [{ url: obj.download_url }],
6235
+ billed: await this.billedForJob(backgroundJobId)
6191
6236
  };
6192
6237
  }
6193
6238
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
@@ -6198,7 +6243,7 @@ var PixelLabProvider = class _PixelLabProvider {
6198
6243
  const survivor = await this.client.getObject(jobId).catch(() => null);
6199
6244
  const url = firstUrl(survivor?.rotation_urls) ?? survivor?.preview_url ?? null;
6200
6245
  if (survivor?.status === "completed" && url) {
6201
- return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }] };
6246
+ return { status: "ready", objectId: survivor.id, sourceUrl: url, sources: [{ url }], billed: null };
6202
6247
  }
6203
6248
  return {
6204
6249
  status: "failed",
@@ -6211,12 +6256,14 @@ var PixelLabProvider = class _PixelLabProvider {
6211
6256
  * `storage_urls` once finished. There is no `status` field to read and no
6212
6257
  * progress percentage on offer, so "processing" here carries no ETA.
6213
6258
  */
6214
- async pollTiles(tileId, connectable) {
6259
+ async pollTiles(tileId, connectable, context) {
6215
6260
  try {
6216
6261
  const set = await this.client.getTilesPro(tileId);
6217
6262
  const tiles = tilesInIndexOrder(set.storage_urls);
6218
6263
  if (!tiles.length) return { status: "failed", error: "tiles job returned no storage urls" };
6219
- if (!connectable) return { status: "review", candidateUrls: tiles.map((tile) => tile.url) };
6264
+ const backgroundJobId = context?.metadata?.backgroundJobId;
6265
+ const billed = await this.billedForJob(backgroundJobId);
6266
+ if (!connectable) return { status: "review", candidateUrls: tiles.map((tile) => tile.url), billed };
6220
6267
  return {
6221
6268
  status: "ready",
6222
6269
  objectId: tileId,
@@ -6228,7 +6275,8 @@ var PixelLabProvider = class _PixelLabProvider {
6228
6275
  metadata: {
6229
6276
  tileKind: set.kind,
6230
6277
  ...set.tile_rules ? { tileRules: set.tile_rules } : {}
6231
- }
6278
+ },
6279
+ billed
6232
6280
  };
6233
6281
  } catch (err) {
6234
6282
  if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
@@ -10722,6 +10770,7 @@ function retireGeneration(entry, retiredAt) {
10722
10770
  downloadedAt: entry.downloadedAt,
10723
10771
  cost: entry.cost,
10724
10772
  costUnit: entry.costUnit,
10773
+ billed: entry.billed,
10725
10774
  provider: entry.provider,
10726
10775
  postprocess: entry.postprocess,
10727
10776
  retiredAt
@@ -10812,6 +10861,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
10812
10861
  downloadedAt: generation.downloadedAt,
10813
10862
  cost: generation.cost,
10814
10863
  costUnit: generation.costUnit,
10864
+ billed: generation.billed,
10815
10865
  provider: generation.provider,
10816
10866
  postprocess: generation.postprocess,
10817
10867
  history
@@ -10870,6 +10920,7 @@ function describeHistory(media, entry, cacheDir) {
10870
10920
  height: generation.height,
10871
10921
  cost: generation.cost,
10872
10922
  costUnit: generation.costUnit,
10923
+ billed: generation.billed,
10873
10924
  submittedAt: generation.submittedAt,
10874
10925
  downloadedAt: generation.downloadedAt,
10875
10926
  retiredAt: generation.retiredAt,
@@ -11113,6 +11164,7 @@ async function buildGallerySnapshot(opts) {
11113
11164
  fps: quality?.frameSet?.fps ?? metadataFps(entry) ?? spec.quality?.fps ?? null,
11114
11165
  cost: entry?.cost ?? 0,
11115
11166
  costUnit: entry?.costUnit ?? spec.costUnit,
11167
+ billed: entry?.billed ?? null,
11116
11168
  estimatedCost: spec.cost,
11117
11169
  candidates: spec.candidates,
11118
11170
  submittedAt: entry?.submittedAt ?? null,
@@ -11176,6 +11228,7 @@ async function buildGallerySnapshot(opts) {
11176
11228
  fps: metadataFps(entry),
11177
11229
  cost: entry.cost,
11178
11230
  costUnit: entry.costUnit,
11231
+ billed: entry.billed,
11179
11232
  estimatedCost: null,
11180
11233
  candidates: null,
11181
11234
  submittedAt: entry.submittedAt,
@@ -11818,7 +11871,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
11818
11871
  ...entry.providerMetadata[provider.id],
11819
11872
  ...state.metadata
11820
11873
  }
11821
- } : entry.providerMetadata
11874
+ } : entry.providerMetadata,
11875
+ billed: state.billed ?? entry.billed
11822
11876
  });
11823
11877
  result.review++;
11824
11878
  log2(
@@ -11838,7 +11892,8 @@ async function poll(provider, lock, lockPath, opts = {}) {
11838
11892
  ...state.metadata
11839
11893
  }
11840
11894
  } : entry.providerMetadata,
11841
- error: null
11895
+ error: null,
11896
+ billed: state.billed ?? entry.billed
11842
11897
  });
11843
11898
  result.completed++;
11844
11899
  log2(` ready ${key}`);
@@ -13746,6 +13801,10 @@ async function runLifecycle(args, wave, spent) {
13746
13801
  }
13747
13802
 
13748
13803
  // src/cli/commands/history.ts
13804
+ function billedNote(cost, costUnit, billed) {
13805
+ if (!billed || billed.unit === costUnit && billed.amount === cost) return "";
13806
+ return ` (billed ${formatCost(billed.unit, billed.amount)})`;
13807
+ }
13749
13808
  async function runHistory(args) {
13750
13809
  const { loaded, specs, lock } = await openProject2(args);
13751
13810
  const limit = historyLimit(loaded.manifest);
@@ -13756,7 +13815,7 @@ async function runHistory(args) {
13756
13815
  limit,
13757
13816
  assets: selected.map(({ spec, entry }) => ({
13758
13817
  key: lockKey(spec.styleId, spec.assetId),
13759
- current: entry ? { objectId: entry.objectId, outputs: entry.outputs, downloadedAt: entry.downloadedAt, cost: entry.cost, costUnit: entry.costUnit } : null,
13818
+ current: entry ? { objectId: entry.objectId, outputs: entry.outputs, downloadedAt: entry.downloadedAt, cost: entry.cost, costUnit: entry.costUnit, billed: entry.billed } : null,
13760
13819
  history: entry?.history ?? []
13761
13820
  }))
13762
13821
  }, null, 2));
@@ -13770,10 +13829,10 @@ async function runHistory(args) {
13770
13829
  const key = lockKey(spec.styleId, spec.assetId);
13771
13830
  log(`
13772
13831
  ${key}`);
13773
- log(` current ${entry.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${entry.downloadedAt ?? ""} ${entry.objectId ?? ""}`);
13832
+ log(` current ${entry.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${entry.downloadedAt ?? ""} ${entry.objectId ?? ""}${billedNote(entry.cost, entry.costUnit, entry.billed)}`);
13774
13833
  for (const [i, generation] of entry.history.entries()) {
13775
13834
  const changed = generation.prompt !== entry.prompt ? " (different prompt)" : "";
13776
- log(` #${String(i + 1).padEnd(2)} ${generation.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${generation.downloadedAt ?? ""} ${generation.objectId ?? ""}${changed}`);
13835
+ log(` #${String(i + 1).padEnd(2)} ${generation.outputs[0]?.sha256.slice(0, 12) ?? "\u2014"} ${generation.downloadedAt ?? ""} ${generation.objectId ?? ""}${changed}${billedNote(generation.cost, generation.costUnit, generation.billed)}`);
13777
13836
  }
13778
13837
  }
13779
13838
  if (!shown) log(` no asset has a previous generation recorded${selected.length ? "" : " (nothing selected)"}`);
@@ -14468,21 +14527,29 @@ async function runPlan(args) {
14468
14527
  actionable: group.actionable.map((item) => item.key)
14469
14528
  })),
14470
14529
  actionable: plan.actionable.map((i) => i.key),
14471
- items: plan.items.map(({ key, state, reason, quality, spec }) => ({
14472
- key,
14473
- state,
14474
- reason,
14475
- ...spec.revision ? {
14476
- revision: {
14477
- mode: spec.revision.mode,
14478
- from: spec.revision.sourceAssetId,
14479
- sourceSha256: spec.revision.sourceSha256,
14480
- ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
14481
- ...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
14482
- }
14483
- } : {},
14484
- ...quality ? { quality: { state: quality.state, reason: quality.reason } } : {}
14485
- }))
14530
+ items: plan.items.map(({ key, state, reason, quality, spec }) => {
14531
+ const entry = lock.entries[key];
14532
+ const billedDiffers = entry?.billed && (entry.billed.unit !== entry.costUnit || entry.billed.amount !== entry.cost);
14533
+ return {
14534
+ key,
14535
+ state,
14536
+ reason,
14537
+ ...spec.revision ? {
14538
+ revision: {
14539
+ mode: spec.revision.mode,
14540
+ from: spec.revision.sourceAssetId,
14541
+ sourceSha256: spec.revision.sourceSha256,
14542
+ ...spec.revision.maskSha256 ? { maskSha256: spec.revision.maskSha256 } : {},
14543
+ ...spec.revision.strength == null ? {} : { strength: spec.revision.strength }
14544
+ }
14545
+ } : {},
14546
+ ...quality ? { quality: { state: quality.state, reason: quality.reason } } : {},
14547
+ // The estimate a wave budget spent against; present alongside the
14548
+ // provider's actual charge only when `poll` has read one and it
14549
+ // differs, since that is the only time the gap is worth a look.
14550
+ ...billedDiffers ? { cost: entry.cost, costUnit: entry.costUnit, billed: entry.billed } : {}
14551
+ };
14552
+ })
14486
14553
  }, null, 2));
14487
14554
  } else {
14488
14555
  printPlan(plan);
@@ -14601,15 +14668,27 @@ function packSprites(inputs, options = {}) {
14601
14668
  const sheetH = rows * cellH;
14602
14669
  const rgba = Buffer.alloc(sheetW * sheetH * 4);
14603
14670
  const frames = [];
14671
+ const pivot = options.pivot ?? "top-left";
14604
14672
  sprites.forEach((sprite, i) => {
14605
- const ox = i % columns * cellW;
14606
- const oy = Math.floor(i / columns) * cellH;
14673
+ const cellOx = i % columns * cellW;
14674
+ const cellOy = Math.floor(i / columns) * cellH;
14675
+ const offsetX = pivot === "bottom-center" ? Math.floor((cellW - sprite.width) / 2) : 0;
14676
+ const offsetY = pivot === "bottom-center" ? cellH - sprite.height : 0;
14677
+ const ox = cellOx + offsetX;
14678
+ const oy = cellOy + offsetY;
14607
14679
  for (let y = 0; y < sprite.height; y++) {
14608
14680
  const src = y * sprite.width * 4;
14609
14681
  const dst = ((oy + y) * sheetW + ox) * 4;
14610
14682
  sprite.pixels.copy(rgba, dst, src, src + sprite.width * 4);
14611
14683
  }
14612
- frames.push({ id: sprite.id, x: ox, y: oy, width: sprite.width, height: sprite.height });
14684
+ frames.push({
14685
+ id: sprite.id,
14686
+ x: ox,
14687
+ y: oy,
14688
+ width: sprite.width,
14689
+ height: sprite.height,
14690
+ ...offsetX || offsetY ? { offsetX, offsetY } : {}
14691
+ });
14613
14692
  });
14614
14693
  return {
14615
14694
  png: encodeRgbaPng(sheetW, sheetH, rgba),
@@ -15126,12 +15205,15 @@ function renderAsepriteSheet(atlas, opts) {
15126
15205
  }
15127
15206
  const frames = {};
15128
15207
  for (const frame of atlas.frames) {
15208
+ const offsetX = frame.offsetX ?? 0;
15209
+ const offsetY = frame.offsetY ?? 0;
15210
+ const pivoted = offsetX !== 0 || offsetY !== 0;
15129
15211
  frames[frame.id] = {
15130
15212
  frame: { x: frame.x, y: frame.y, w: frame.width, h: frame.height },
15131
15213
  rotated: false,
15132
- trimmed: false,
15133
- spriteSourceSize: { x: 0, y: 0, w: frame.width, h: frame.height },
15134
- sourceSize: { w: frame.width, h: frame.height },
15214
+ trimmed: pivoted,
15215
+ spriteSourceSize: pivoted ? { x: offsetX, y: offsetY, w: frame.width, h: frame.height } : { x: 0, y: 0, w: frame.width, h: frame.height },
15216
+ sourceSize: pivoted ? { w: atlas.cell.width, h: atlas.cell.height } : { w: frame.width, h: frame.height },
15135
15217
  duration: durations.get(frame.id) ?? ASEPRITE_DEFAULT_DURATION_MS
15136
15218
  };
15137
15219
  }
@@ -15236,16 +15318,18 @@ async function runPack(args) {
15236
15318
  const manifestDir = path38.dirname(path38.resolve(args.manifest));
15237
15319
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
15238
15320
  for (const styleId of styleIds) {
15321
+ const style = loaded.manifest.styles[styleId];
15322
+ const pivot = style?.generator === "character" ? "bottom-center" : "top-left";
15239
15323
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
15240
15324
  const qualitySources = await requireApprovedQualitySources(packagingSpecs, lock);
15241
15325
  const { png, atlas, skipped, sources } = packStyle(lock, styleId, manifestDir, {
15242
15326
  columns: args.columns,
15243
15327
  outputRoles: args.outputRoles,
15244
15328
  primaryOnly: args.primaryOnly,
15329
+ pivot,
15245
15330
  sourceOverrides: qualitySources,
15246
15331
  sources: manifestSources(loaded.manifest, styleId)
15247
15332
  });
15248
- const style = loaded.manifest.styles[styleId];
15249
15333
  const base = args.out ? path38.resolve(args.out.replace(/\.(?:png|json|tres)$/i, "")) : path38.resolve(manifestDir, style.outDir, `${styleId}-sheet`);
15250
15334
  const format = sheetFormat(args);
15251
15335
  const { extension, document } = renderSheetDocument(format, atlas, { imageName: path38.basename(`${base}.png`) });
@@ -15272,6 +15356,7 @@ async function runPack(args) {
15272
15356
  format,
15273
15357
  order: "id",
15274
15358
  outputRoles: [...args.outputRoles].sort(),
15359
+ pivot,
15275
15360
  primaryOnly: args.primaryOnly,
15276
15361
  style: styleId
15277
15362
  }