pixelkiln 0.53.4 → 0.55.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
@@ -653,6 +653,8 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
653
653
  tileView: spec.tileView,
654
654
  tileFeature: spec.tileFeature,
655
655
  outlineMode: spec.outlineMode,
656
+ isometricTileSize: spec.isometricTileSize,
657
+ isometricTileShape: spec.isometricTileShape,
656
658
  styleImages: styleImageHashes,
657
659
  // A character family: the engine and rotations of a base, the parent's
658
660
  // generated bytes for a state or animation (so a regenerated parent
@@ -681,6 +683,23 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
681
683
  concept: spec.character.concept?.sha256,
682
684
  styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
683
685
  } : void 0,
686
+ // An objectPro family: same shape as character above, minus the
687
+ // fields it has none of (mode/template/proportions/isometric/concept/
688
+ // styleAnchor) — see ResolvedObjectPro.
689
+ objectPro: spec.objectPro ? {
690
+ kind: spec.objectPro.kind,
691
+ directions: spec.objectPro.directions,
692
+ parent: spec.objectPro.parentAssetId,
693
+ parentSha256: spec.objectPro.parentSha256 ?? null,
694
+ state: spec.objectPro.state,
695
+ animation: spec.objectPro.animation ? {
696
+ ...spec.objectPro.animation,
697
+ startFrame: spec.objectPro.animation.startFrame?.sha256,
698
+ endFrame: spec.objectPro.animation.endFrame?.sha256
699
+ } : void 0,
700
+ styleTraits: spec.objectPro.styleTraits,
701
+ reference: spec.objectPro.reference?.sha256
702
+ } : void 0,
684
703
  // A mirror's bytes come from its source's recorded outputs; the plan
685
704
  // compares those directly, so only the choice of source is identity.
686
705
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -707,7 +726,7 @@ import path2 from "path";
707
726
  // src/types.ts
708
727
  import { z } from "zod";
709
728
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
710
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro"]);
729
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
711
730
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
712
731
  var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
713
732
  function tileVariationCount(descriptions) {
@@ -741,6 +760,9 @@ function tilesCost(tileSize, variations) {
741
760
  if (px <= 2048) return 25;
742
761
  return 40;
743
762
  }
763
+ function isometricTileCost() {
764
+ return 1;
765
+ }
744
766
  function terrainTileCount(transitionSize) {
745
767
  return transitionSize === 1 ? 25 : 16;
746
768
  }
@@ -1070,6 +1092,28 @@ var StyleObjectSchema = z.object({
1070
1092
  /** `terrain` generator only. Camera angle; the API default is `high
1071
1093
  * top-down`. */
1072
1094
  terrainView: z.enum(["low top-down", "high top-down"]).optional(),
1095
+ /**
1096
+ * `isometricTile` generator only. `/create-isometric-tile`'s own tile
1097
+ * grid size, distinct from `size` (the generation canvas, 16-64px — the
1098
+ * API's own guidance is that sizes above 24px "often produce better
1099
+ * quality results"). The API default is 16.
1100
+ */
1101
+ isometricTileSize: z.union([z.literal(16), z.literal(32)]).optional(),
1102
+ /**
1103
+ * `isometricTile` generator only. Vertical thickness, i.e. how much
1104
+ * height/elevation variation the tile can represent on a map: `"thin
1105
+ * tile"` (~15% of canvas height), `"thick tile"` (~25%), or `"block"`
1106
+ * (~50%, the API default).
1107
+ */
1108
+ isometricTileShape: z.enum(["thin tile", "thick tile", "block"]).optional(),
1109
+ /**
1110
+ * `objectPro` generator only. Rotations captured for a base: 1 (the API
1111
+ * still draws and stores a single facing) or the full 8. The API default
1112
+ * is 8. Unlike `character`, `objectPro` has no skeleton/template modes
1113
+ * and so no `mode`/`template` fields of its own — `/create-object-pro-flash`
1114
+ * is the only base-creation path.
1115
+ */
1116
+ objectDirections: z.union([z.literal(1), z.literal(8)]).optional(),
1073
1117
  /**
1074
1118
  * `pixflux` only. Whether to strip the generated background.
1075
1119
  *
@@ -1274,25 +1318,41 @@ var AssetSchema = z.object({
1274
1318
  revision: RevisionSchema.optional(),
1275
1319
  /**
1276
1320
  * The provider's own id for art that already exists on the account, so
1277
- * `adopt` can map it without matching bytes: an object id, a character
1278
- * id, or `<character id>#<animation group id>` for a character animation.
1279
- * Not part of the spec's identity.
1321
+ * `adopt` can map it without matching bytes: an object id (an
1322
+ * `objectPro` base and state included, since they share PixelLab's
1323
+ * `/objects` identity space), a character id, or `<id>#<animation group
1324
+ * id>` for a character or `objectPro` animation. Not part of the spec's
1325
+ * identity.
1280
1326
  */
1281
1327
  remoteId: z.string().min(1).optional(),
1282
- /** `character` styles: this asset is a pose or outfit of another character asset. */
1328
+ /**
1329
+ * `character` or `objectPro` styles: this asset is a pose or edit of
1330
+ * another asset in the same family. `objectPro` ignores
1331
+ * `paletteFromReference`/`canvas` — the API has no equivalent, and
1332
+ * always inherits the parent's exact canvas.
1333
+ */
1283
1334
  state: CharacterStateSchema.optional(),
1284
- /** `character` styles: this asset is a loop of another character asset in one direction. */
1335
+ /**
1336
+ * `character` or `objectPro` styles: this asset is a loop of another
1337
+ * asset in the same family, in one direction. `objectPro` has no
1338
+ * skeleton/template concept, so `template`/`subject`/`outline`/
1339
+ * `shading`/`detail` are rejected on an `objectPro` animation rather
1340
+ * than silently ignored — describe the motion in the asset's own
1341
+ * `prompt` instead.
1342
+ */
1285
1343
  animation: CharacterAnimationSchema.optional(),
1286
1344
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
1287
1345
  proportions: CharacterProportionsSchema.optional(),
1288
1346
  /**
1289
- * `character` styles, bases only: the character's own sprite, which
1290
- * PixelLab rotates into the other directions instead of drawing from
1291
- * the prompt. A manifest-relative PNG or JPEG of the south-facing
1292
- * sprite, or an object keyed by direction (`{ "south": ..., "east":
1293
- * ... }`); quadrupeds in `standard` mode need south and east, and only
1294
- * `standard` takes more than south. `standard` wants each image at the
1295
- * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
1347
+ * `character` or `objectPro` styles, bases only: the subject's own
1348
+ * sprite, which PixelLab rotates into the other directions instead of
1349
+ * drawing from the prompt. A manifest-relative PNG or JPEG of the
1350
+ * south-facing sprite, or an object keyed by direction (`{ "south":
1351
+ * ..., "east": ... }`); quadrupeds in `standard` character mode need
1352
+ * south and east, and only `standard` takes more than south. `objectPro`
1353
+ * only ever reads the south image, since `/create-object-pro-flash`
1354
+ * rotates from one frame. `standard` wants each image at the style's
1355
+ * size; v3 accepts up to 256px, pro up to 168px. The prompt
1296
1356
  * still guides the result.
1297
1357
  */
1298
1358
  reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
@@ -3778,6 +3838,7 @@ async function inspectRevisionReadiness(spec, lock) {
3778
3838
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
3779
3839
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
3780
3840
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
3841
+ if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
3781
3842
  if (!spec.revision) return null;
3782
3843
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
3783
3844
  }
@@ -3819,6 +3880,24 @@ async function inspectCharacterParent(spec, lock, seen) {
3819
3880
  }
3820
3881
  return { ready: true, reason: `${label} is current` };
3821
3882
  }
3883
+ async function inspectObjectProParent(spec, lock, seen) {
3884
+ const object = spec.objectPro;
3885
+ const parentSpec = object.parentSpec;
3886
+ const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
3887
+ const dependency = await inspectParent(parentSpec, lock, seen);
3888
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
3889
+ if (!object.parentSha256 || !object.parentFile || !existsSync4(object.parentFile)) {
3890
+ return { ready: false, reason: `${label} has no generated south-facing file yet` };
3891
+ }
3892
+ if (await sha256File(object.parentFile) !== object.parentSha256) {
3893
+ return { ready: false, reason: `${label} changed after the manifest was resolved` };
3894
+ }
3895
+ const parentEntry = lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)];
3896
+ if (!parentEntry?.objectId) {
3897
+ return { ready: false, reason: `${label} has no PixelLab object id recorded` };
3898
+ }
3899
+ return { ready: true, reason: `${label} is current` };
3900
+ }
3822
3901
  async function inspectRevision(spec, lock, seen) {
3823
3902
  const revision = spec.revision;
3824
3903
  if (!revision) return { ready: true, reason: "asset has no revision inputs" };
@@ -3870,6 +3949,10 @@ async function inspectParent(spec, lock, seen) {
3870
3949
  const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
3871
3950
  if (!inputs.ready) return inputs;
3872
3951
  }
3952
+ if (spec.objectPro?.parentSpec) {
3953
+ const inputs = await inspectObjectProParent(spec, lock, nextSeen);
3954
+ if (!inputs.ready) return inputs;
3955
+ }
3873
3956
  if (spec.quality) {
3874
3957
  const quality = await inspectQualityProfile(spec, lock);
3875
3958
  return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
@@ -3914,7 +3997,8 @@ function dependencies(spec) {
3914
3997
  spec.mirror?.sourceSpec,
3915
3998
  spec.revision?.sourceSpec,
3916
3999
  spec.character?.parentSpec,
3917
- spec.character?.styleAnchor?.spec
4000
+ spec.character?.styleAnchor?.spec,
4001
+ spec.objectPro?.parentSpec
3918
4002
  ].filter((parent) => Boolean(parent));
3919
4003
  }
3920
4004
  function resumeCommandForStatus(status) {
@@ -5249,13 +5333,36 @@ var TilesetGetSchema = z2.object({
5249
5333
  }).passthrough(),
5250
5334
  usage: UsageSchema
5251
5335
  }).passthrough();
5336
+ var IsometricTileSubmitSchema = z2.object({
5337
+ tile_id: z2.string().min(1),
5338
+ background_job_id: z2.string().min(1),
5339
+ status: z2.literal("processing").default("processing")
5340
+ }).passthrough();
5341
+ var IsometricTileGetSchema = z2.object({
5342
+ image: z2.object({ base64: z2.string().min(1), format: z2.string().default("png") }).passthrough(),
5343
+ usage: UsageSchema
5344
+ }).passthrough();
5252
5345
  var RevisionJobSubmitSchema = z2.object({
5253
5346
  background_job_id: z2.string().min(1),
5254
5347
  status: z2.string().default("processing")
5255
5348
  }).passthrough();
5349
+ var ObjectAnimationGroupSchema = z2.object({
5350
+ animation_group_id: z2.string().min(1),
5351
+ display_name: z2.string().nullable().default(null),
5352
+ description: z2.string().default(""),
5353
+ frame_count: z2.number().int().default(0),
5354
+ directions: z2.array(
5355
+ z2.object({
5356
+ direction: z2.string(),
5357
+ storage_urls: z2.record(z2.unknown()).default({}),
5358
+ created_at: z2.string()
5359
+ }).passthrough()
5360
+ ).default([])
5361
+ }).passthrough();
5256
5362
  var PixelLabObjectSchema = z2.object({
5257
5363
  id: z2.string().min(1),
5258
5364
  name: z2.string().nullable().default(null),
5365
+ state_name: z2.string().nullable().optional(),
5259
5366
  prompt: z2.string().default(""),
5260
5367
  size: z2.object({ width: z2.number().int().positive(), height: z2.number().int().positive() }),
5261
5368
  directions: z2.number().default(0),
@@ -5267,7 +5374,9 @@ var PixelLabObjectSchema = z2.object({
5267
5374
  tags: z2.array(z2.string()).default([]),
5268
5375
  status: z2.string().nullable().default(null),
5269
5376
  progress_percent: z2.number().nullable().optional(),
5270
- eta_seconds: z2.number().nullable().optional()
5377
+ eta_seconds: z2.number().nullable().optional(),
5378
+ group_id: z2.string().nullable().optional(),
5379
+ animations: z2.array(ObjectAnimationGroupSchema).default([])
5271
5380
  }).passthrough();
5272
5381
  var MapObjectSchema = z2.object({
5273
5382
  object_id: z2.string().min(1),
@@ -5292,6 +5401,28 @@ var AnimateSubmitSchema = z2.object({
5292
5401
  status: z2.string().default("processing"),
5293
5402
  usage: UsageSchema
5294
5403
  }).passthrough();
5404
+ var ObjectProSubmitSchema = z2.object({
5405
+ object_id: z2.string().min(1),
5406
+ background_job_id: z2.string().min(1),
5407
+ status: z2.string().default("processing"),
5408
+ usage: UsageSchema
5409
+ }).passthrough();
5410
+ var DirectionSubmissionSchema = z2.object({
5411
+ direction: z2.string(),
5412
+ status: z2.string(),
5413
+ background_job_id: z2.string().nullable().default(null),
5414
+ animation_id: z2.string().nullable().default(null)
5415
+ }).passthrough();
5416
+ var AnimateObjectSubmitSchema = z2.object({
5417
+ animation_group_id: z2.string().min(1),
5418
+ object_id: z2.string().min(1),
5419
+ mode: z2.string(),
5420
+ frame_count: z2.number().int(),
5421
+ display_name: z2.string().nullable().default(null),
5422
+ description: z2.string().default(""),
5423
+ submissions: z2.array(DirectionSubmissionSchema),
5424
+ usage: UsageSchema
5425
+ }).passthrough();
5295
5426
  var CharacterAnimationDirectionSchema = z2.object({
5296
5427
  direction: z2.string(),
5297
5428
  frame_count: z2.number().int().min(0),
@@ -5534,6 +5665,33 @@ var PixelLabClient = class {
5534
5665
  );
5535
5666
  return { tileset: res.tileset, usage: res.usage ?? null };
5536
5667
  }
5668
+ /** A single standalone isometric tile — no connectable set, no candidates. */
5669
+ async createIsometricTile(args) {
5670
+ const body = { description: args.description };
5671
+ if (args.imageWidth != null && args.imageHeight != null) {
5672
+ body.image_size = { width: args.imageWidth, height: args.imageHeight };
5673
+ }
5674
+ if (args.tileSize != null) body.isometric_tile_size = args.tileSize;
5675
+ if (args.tileShape) body.isometric_tile_shape = args.tileShape;
5676
+ if (args.outline) body.outline = args.outline;
5677
+ if (args.shading) body.shading = args.shading;
5678
+ if (args.detail) body.detail = args.detail;
5679
+ if (args.seed != null) body.seed = args.seed;
5680
+ return validateResponse(
5681
+ IsometricTileSubmitSchema,
5682
+ await this.request("/create-isometric-tile", { method: "POST", body: JSON.stringify(body) }),
5683
+ "create isometric tile"
5684
+ );
5685
+ }
5686
+ /** Throws PixelLabError(423) while the tile is still processing. */
5687
+ async getIsometricTile(tileId) {
5688
+ const res = await validateResponse(
5689
+ IsometricTileGetSchema,
5690
+ await this.request(`/isometric-tiles/${tileId}`),
5691
+ "get isometric tile"
5692
+ );
5693
+ return { image: res.image, usage: res.usage ?? null };
5694
+ }
5537
5695
  /**
5538
5696
  * Synchronous single-image generation. Returns the PNG inline rather than a
5539
5697
  * job id, and is the only endpoint that honours a forced palette;
@@ -5567,6 +5725,81 @@ var PixelLabClient = class {
5567
5725
  "get object"
5568
5726
  );
5569
5727
  }
5728
+ /**
5729
+ * A standalone object with no skeleton, one call draws the south sprite
5730
+ * (or takes the author's) and rotates it with v3 — the character
5731
+ * pro-flash pattern, minus `template_id` (objects have no skeleton) and
5732
+ * with a real `n_directions` choice (1 or 8, character pro-flash is
5733
+ * always 8). Async only: `/create-object-pro-flash` has no sync response.
5734
+ */
5735
+ async createObjectProFlash(args) {
5736
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5737
+ const traits = args.styleTraits;
5738
+ const body = {
5739
+ description: args.description,
5740
+ n_directions: args.directions,
5741
+ ...args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
5742
+ ...args.view ? { view: args.view } : {},
5743
+ ...args.seed != null ? { seed: args.seed } : {},
5744
+ ...args.styleReference && args.styleReferenceSize ? {
5745
+ style_image: { image: encode(args.styleReference), size: args.styleReferenceSize },
5746
+ ...traits ? {
5747
+ style_options: {
5748
+ ...traits.palette !== void 0 ? { color_palette: traits.palette } : {},
5749
+ ...traits.outline !== void 0 ? { outline: traits.outline } : {},
5750
+ ...traits.detail !== void 0 ? { detail: traits.detail } : {},
5751
+ ...traits.shading !== void 0 ? { shading: traits.shading } : {}
5752
+ }
5753
+ } : {}
5754
+ } : {}
5755
+ };
5756
+ return validateResponse(
5757
+ ObjectProSubmitSchema,
5758
+ await this.request("/create-object-pro-flash", { method: "POST", body: JSON.stringify(body) }),
5759
+ "create-object-pro-flash"
5760
+ );
5761
+ }
5762
+ /** A text edit of an existing object, applied to its whole rotation set and stored as a sibling. Always inherits the parent's exact canvas — no size override exists on this endpoint. */
5763
+ async createObjectProState(args) {
5764
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/states`, {
5765
+ method: "POST",
5766
+ body: JSON.stringify({
5767
+ edit_description: args.editDescription,
5768
+ ...args.stateName ? { state_name: args.stateName } : {},
5769
+ ...args.seed != null ? { seed: args.seed } : {}
5770
+ })
5771
+ });
5772
+ return validateResponse(ObjectProSubmitSchema, raw, "create object state");
5773
+ }
5774
+ /**
5775
+ * One direction of one object's animation. Unlike character animation,
5776
+ * this endpoint's own `replace_existing` flag regenerates an
5777
+ * already-animated direction directly (character has no such flag and
5778
+ * pixelkiln instead deletes the prior take itself before resubmitting;
5779
+ * see `submitCharacter`). A 1-direction object must not receive
5780
+ * `directions` at all -- the API 400s if it does.
5781
+ */
5782
+ async animateObject(args) {
5783
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
5784
+ const body = {
5785
+ mode: args.mode,
5786
+ ...args.directions === 8 ? { directions: [args.direction] } : {},
5787
+ ...args.animationDescription ? { animation_description: args.animationDescription } : {},
5788
+ ...args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
5789
+ ...args.displayName ? { display_name: args.displayName } : {},
5790
+ ...args.frameCount != null ? { frame_count: args.frameCount } : {},
5791
+ ...args.keepFirstFrame === false ? { keep_first_frame: false } : {},
5792
+ ...args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
5793
+ ...args.endFrame ? { end_frame: encode(args.endFrame) } : {},
5794
+ ...args.enhancePrompt ? { enhance_prompt: true } : {},
5795
+ ...args.replaceExisting ? { replace_existing: true } : {}
5796
+ };
5797
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/animations`, {
5798
+ method: "POST",
5799
+ body: JSON.stringify(body)
5800
+ });
5801
+ return validateResponse(AnimateObjectSubmitSchema, raw, "animate object");
5802
+ }
5570
5803
  async getMapObject(objectId) {
5571
5804
  return validateResponse(
5572
5805
  MapObjectSchema,
@@ -5966,6 +6199,29 @@ function proFlashCharacterCost(width, height, fromReference) {
5966
6199
  function characterAnimationName(spec) {
5967
6200
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
5968
6201
  }
6202
+ function objectAnimationName(spec) {
6203
+ return `pixelkiln:${spec.styleId}/${spec.assetId}`;
6204
+ }
6205
+ function proFlashObjectCost(width, height, directions, fromReference) {
6206
+ const side = Math.max(32, Math.ceil(Math.max(width, height) / 4) * 4);
6207
+ const rotations = Math.max(1, Math.ceil(side * side * directions / 65536));
6208
+ if (fromReference) return rotations;
6209
+ const image = side <= 96 ? 5 : side <= 208 ? 6 : 9;
6210
+ return image + rotations;
6211
+ }
6212
+ function objectProCost(spec) {
6213
+ const object = spec.objectPro;
6214
+ if (!object) return generationCost(spec.width, spec.height, "1dir");
6215
+ const px = spec.width * spec.height;
6216
+ if (object.kind === "animation") {
6217
+ const animation = object.animation;
6218
+ if (animation.mode === "pro") return generationCost(spec.width, spec.height, "1dir");
6219
+ return Math.max(1, Math.ceil(px * animation.frames / 65536));
6220
+ }
6221
+ if (object.kind === "state") return generationCost(spec.width, spec.height, "1dir");
6222
+ const south = object.reference;
6223
+ return south ? proFlashObjectCost(south.width, south.height, object.directions, true) : proFlashObjectCost(spec.width, spec.height, object.directions, false);
6224
+ }
5969
6225
  var CHARACTER_TEMPLATES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
5970
6226
  var PRO_FLASH_TEMPLATES = [...CHARACTER_TEMPLATES, "custom"];
5971
6227
  function encodeAnimationJob(job) {
@@ -5982,6 +6238,20 @@ function decodeAnimationJob(jobId) {
5982
6238
  }
5983
6239
  return null;
5984
6240
  }
6241
+ function encodeObjectAnimationJob(job) {
6242
+ return `object-animation:${Buffer.from(JSON.stringify(job)).toString("base64url")}`;
6243
+ }
6244
+ function decodeObjectAnimationJob(jobId) {
6245
+ if (!jobId.startsWith("object-animation:")) return null;
6246
+ try {
6247
+ const value = JSON.parse(Buffer.from(jobId.slice("object-animation:".length), "base64url").toString("utf8"));
6248
+ if (typeof value?.objectId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
6249
+ return { objectId: value.objectId, name: value.name, direction: value.direction, jobId: typeof value.jobId === "string" ? value.jobId : null };
6250
+ }
6251
+ } catch {
6252
+ }
6253
+ return null;
6254
+ }
5985
6255
  var PixelLabProvider = class _PixelLabProvider {
5986
6256
  constructor(client2) {
5987
6257
  this.client = client2;
@@ -6000,7 +6270,7 @@ var PixelLabProvider = class _PixelLabProvider {
6000
6270
  return new _PixelLabProvider(new PixelLabClient("download-only"));
6001
6271
  }
6002
6272
  supports(generator) {
6003
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character";
6273
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro";
6004
6274
  }
6005
6275
  /**
6006
6276
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -6036,12 +6306,15 @@ var PixelLabProvider = class _PixelLabProvider {
6036
6306
  const height = spec.revision.sourceHeight ?? spec.height;
6037
6307
  return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
6038
6308
  }
6039
- if (spec.generator === "tiles" || spec.generator === "terrain") {
6309
+ if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
6040
6310
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
6041
6311
  }
6042
6312
  if (spec.generator === "character") {
6043
6313
  return { unit: "generations", amount: characterCost(spec), candidates: 1 };
6044
6314
  }
6315
+ if (spec.generator === "objectPro") {
6316
+ return { unit: "generations", amount: objectProCost(spec), candidates: 1 };
6317
+ }
6045
6318
  return {
6046
6319
  unit: "generations",
6047
6320
  amount: generationCost(spec.width, spec.height, spec.generator),
@@ -6067,19 +6340,19 @@ var PixelLabProvider = class _PixelLabProvider {
6067
6340
  );
6068
6341
  }
6069
6342
  if (spec.generator === "map") {
6070
- requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
6071
- requirePixelLabOption("outline", spec.outline, [
6343
+ requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
6344
+ requirePixelLabOption("map", "outline", spec.outline, [
6072
6345
  "single color outline",
6073
6346
  "selective outline",
6074
6347
  "lineless"
6075
6348
  ]);
6076
- requirePixelLabOption("shading", spec.shading, [
6349
+ requirePixelLabOption("map", "shading", spec.shading, [
6077
6350
  "flat shading",
6078
6351
  "basic shading",
6079
6352
  "medium shading",
6080
6353
  "detailed shading"
6081
6354
  ]);
6082
- requirePixelLabOption("detail", spec.detail, [
6355
+ requirePixelLabOption("map", "detail", spec.detail, [
6083
6356
  "low detail",
6084
6357
  "medium detail",
6085
6358
  "high detail"
@@ -6087,7 +6360,7 @@ var PixelLabProvider = class _PixelLabProvider {
6087
6360
  }
6088
6361
  if (spec.generator === "terrain") {
6089
6362
  if (spec.outline) {
6090
- requirePixelLabOption("outline", spec.outline, [
6363
+ requirePixelLabOption("terrain", "outline", spec.outline, [
6091
6364
  "single color black outline",
6092
6365
  "single color outline",
6093
6366
  "selective outline",
@@ -6095,7 +6368,7 @@ var PixelLabProvider = class _PixelLabProvider {
6095
6368
  ]);
6096
6369
  }
6097
6370
  if (spec.shading) {
6098
- requirePixelLabOption("shading", spec.shading, [
6371
+ requirePixelLabOption("terrain", "shading", spec.shading, [
6099
6372
  "flat shading",
6100
6373
  "basic shading",
6101
6374
  "medium shading",
@@ -6104,7 +6377,7 @@ var PixelLabProvider = class _PixelLabProvider {
6104
6377
  ]);
6105
6378
  }
6106
6379
  if (spec.detail) {
6107
- requirePixelLabOption("detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
6380
+ requirePixelLabOption("terrain", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
6108
6381
  }
6109
6382
  if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
6110
6383
  throw new Error(
@@ -6125,7 +6398,32 @@ var PixelLabProvider = class _PixelLabProvider {
6125
6398
  );
6126
6399
  }
6127
6400
  }
6401
+ if (spec.generator === "isometricTile") {
6402
+ if (spec.outline) {
6403
+ requirePixelLabOption("isometricTile", "outline", spec.outline, [
6404
+ "single color outline",
6405
+ "selective outline",
6406
+ "lineless"
6407
+ ]);
6408
+ }
6409
+ if (spec.shading) {
6410
+ requirePixelLabOption("isometricTile", "shading", spec.shading, [
6411
+ "flat shading",
6412
+ "basic shading",
6413
+ "medium shading",
6414
+ "detailed shading",
6415
+ "highly detailed shading"
6416
+ ]);
6417
+ }
6418
+ if (spec.detail) {
6419
+ requirePixelLabOption("isometricTile", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
6420
+ }
6421
+ if (spec.width < 16 || spec.width > 64) {
6422
+ throw new Error(`PixelLab isometricTile: size must be 16 to 64 pixels, got ${spec.width}`);
6423
+ }
6424
+ }
6128
6425
  if (spec.generator === "character") this.validateCharacter(spec, styleImages);
6426
+ if (spec.generator === "objectPro") this.validateObjectPro(spec, styleImages);
6129
6427
  if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
6130
6428
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
6131
6429
  }
@@ -6139,6 +6437,11 @@ var PixelLabProvider = class _PixelLabProvider {
6139
6437
  "PixelLab imagePro does not support style images yet; /generate-image-v2's own reference_images and style_image are not modeled here"
6140
6438
  );
6141
6439
  }
6440
+ if (spec.generator === "isometricTile" && styleImages.length) {
6441
+ throw new Error(
6442
+ "PixelLab isometricTile does not support style images; /create-isometric-tile has no such field (its own init_image/color_image are not modeled here yet)"
6443
+ );
6444
+ }
6142
6445
  for (const image of styleImages) {
6143
6446
  if (image.width > 256 || image.height > 256) {
6144
6447
  throw new Error(
@@ -6281,6 +6584,63 @@ var PixelLabProvider = class _PixelLabProvider {
6281
6584
  }
6282
6585
  }
6283
6586
  }
6587
+ validateObjectPro(spec, styleImages = []) {
6588
+ const object = spec.objectPro;
6589
+ if (!object) throw new Error(`${spec.styleId}/${spec.assetId} has no objectPro shape`);
6590
+ const label = `${spec.styleId}/${spec.assetId}`;
6591
+ if (object.kind === "base" && object.reference && styleImages.length) {
6592
+ throw new Error(`${label}: a base rotating its own reference sprite takes no style image`);
6593
+ }
6594
+ if (object.reference && (object.reference.width > 256 || object.reference.height > 256)) {
6595
+ throw new Error(`${label}: reference is ${object.reference.width}x${object.reference.height}; PixelLab objectPro takes up to 256px`);
6596
+ }
6597
+ if (styleImages.length) {
6598
+ if (styleImages.length > 1) throw new Error(`PixelLab objectPro takes one style image`);
6599
+ const image = styleImages[0];
6600
+ if (image.width > 256 || image.height > 256) {
6601
+ throw new Error(`PixelLab objectPro style image is ${image.width}x${image.height}; the limit is 256px`);
6602
+ }
6603
+ if (image.width > spec.width || image.height > spec.height) {
6604
+ throw new Error(
6605
+ `PixelLab objectPro style image is ${image.width}x${image.height} but the object is ${spec.width}x${spec.height}; crop the image to its subject or raise the style's size`
6606
+ );
6607
+ }
6608
+ }
6609
+ if (object.kind === "base" && (spec.width < 16 || spec.height < 16 || spec.width > 256 || spec.height > 256)) {
6610
+ throw new Error(`PixelLab objectPro bases must be between 16 and 256 pixels`);
6611
+ }
6612
+ if (object.kind === "base" && (spec.width % 4 || spec.height % 4)) {
6613
+ throw new Error(`PixelLab objectPro bases are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
6614
+ }
6615
+ if (object.kind !== "base" && !object.parentAssetId) {
6616
+ throw new Error(`${spec.styleId}/${spec.assetId} is a ${object.kind} with no parent`);
6617
+ }
6618
+ if (object.animation) {
6619
+ const allowed = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
6620
+ if (!allowed.includes(object.animation.direction)) {
6621
+ throw new Error(
6622
+ `${label} animates "${object.animation.direction}", but its object has ${object.directions} direction${object.directions === 1 ? "" : "s"}: ${allowed.join(", ")}`
6623
+ );
6624
+ }
6625
+ if (!spec.prompt.trim()) {
6626
+ throw new Error(`${label} needs a prompt describing the motion`);
6627
+ }
6628
+ const { startFrame, endFrame } = object.animation;
6629
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
6630
+ if (image && (image.width > 256 || image.height > 256)) {
6631
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
6632
+ }
6633
+ }
6634
+ if (endFrame) {
6635
+ const start = startFrame ?? (object.parentFile && existsSync7(object.parentFile) ? imageMetadata(readFileSync2(object.parentFile)) : null);
6636
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
6637
+ throw new Error(
6638
+ `${label}: end frame is ${endFrame.width}x${endFrame.height} but the ${startFrame ? "start frame" : "object's rotation"} is ${start.width}x${start.height}; PixelLab interpolates between frames of one size`
6639
+ );
6640
+ }
6641
+ }
6642
+ }
6643
+ }
6284
6644
  /**
6285
6645
  * A base is one request that answers with the character id; a state is
6286
6646
  * the same with the parent's id; an animation names itself after the
@@ -6374,6 +6734,78 @@ var PixelLabProvider = class _PixelLabProvider {
6374
6734
  metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
6375
6735
  };
6376
6736
  }
6737
+ /**
6738
+ * Same shape as `submitCharacter`, minus the mode branching (objectPro has
6739
+ * exactly one base-creation path) and minus the client-side "find and
6740
+ * delete the prior take" dance for animation: `/objects/{id}/animations`
6741
+ * has its own `replace_existing` flag, so this always passes it rather
6742
+ * than fetching the object first to check.
6743
+ */
6744
+ async submitObjectPro(spec, styleImages, context) {
6745
+ const object = spec.objectPro;
6746
+ if (object.kind === "base") {
6747
+ const styleImage = styleImages[0];
6748
+ const res2 = await this.client.createObjectProFlash({
6749
+ description: spec.prompt,
6750
+ directions: object.directions,
6751
+ size: spec.width,
6752
+ view: spec.view,
6753
+ seed: spec.seed,
6754
+ reference: object.reference ? readPose(spec, "reference sprite", object.reference) : void 0,
6755
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
6756
+ styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
6757
+ styleTraits: object.styleTraits
6758
+ });
6759
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "base", objectId: res2.object_id, directions: object.directions, backgroundJobId: res2.background_job_id } } };
6760
+ }
6761
+ const parentId = context?.parentObjectId;
6762
+ if (!parentId) {
6763
+ throw new Error(
6764
+ `${spec.styleId}/${spec.assetId} is a ${object.kind} of ${object.parentAssetId}, which has no PixelLab object id in the lockfile yet; generate the parent first`
6765
+ );
6766
+ }
6767
+ if (object.kind === "state") {
6768
+ const res2 = await this.client.createObjectProState({
6769
+ objectId: parentId,
6770
+ editDescription: spec.prompt,
6771
+ stateName: spec.assetId.slice(0, 50),
6772
+ seed: spec.seed
6773
+ });
6774
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "state", objectId: res2.object_id, parentObjectId: parentId, directions: object.directions, backgroundJobId: res2.background_job_id } } };
6775
+ }
6776
+ const animation = object.animation;
6777
+ const name = objectAnimationName(spec);
6778
+ const res = await this.client.animateObject({
6779
+ objectId: parentId,
6780
+ directions: object.directions,
6781
+ direction: animation.direction,
6782
+ animationDescription: spec.prompt.trim() || void 0,
6783
+ displayName: name,
6784
+ mode: animation.mode === "pro" ? "pro" : "v3",
6785
+ frameCount: animation.frames,
6786
+ keepFirstFrame: animation.keepFirstFrame,
6787
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
6788
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
6789
+ enhancePrompt: animation.enhancePrompt,
6790
+ replaceExisting: true
6791
+ });
6792
+ const submission = res.submissions.find((s) => s.direction === animation.direction) ?? res.submissions[0];
6793
+ const job = { objectId: parentId, name, direction: animation.direction, jobId: submission?.background_job_id ?? null };
6794
+ return {
6795
+ jobId: encodeObjectAnimationJob(job),
6796
+ metadata: {
6797
+ objectPro: {
6798
+ kind: "animation",
6799
+ objectId: parentId,
6800
+ animationName: name,
6801
+ direction: animation.direction,
6802
+ mode: animation.mode,
6803
+ animationGroupId: res.animation_group_id,
6804
+ animationId: submission?.animation_id ?? null
6805
+ }
6806
+ }
6807
+ };
6808
+ }
6377
6809
  async pollCharacter(jobId, context) {
6378
6810
  const animation = decodeAnimationJob(jobId);
6379
6811
  if (animation) return this.pollCharacterAnimation(animation, context);
@@ -6457,10 +6889,93 @@ var PixelLabProvider = class _PixelLabProvider {
6457
6889
  if (found) return review(found.frames, found.animationId, found.groupId, null);
6458
6890
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
6459
6891
  }
6892
+ async pollObjectPro(jobId, context) {
6893
+ const animation = decodeObjectAnimationJob(jobId);
6894
+ if (animation) return this.pollObjectProAnimation(animation, context);
6895
+ const object = await this.client.getObject(jobId);
6896
+ if (object.status === "failed") return { status: "failed", error: "object generation failed upstream" };
6897
+ if (object.status !== "completed" || !object.rotation_urls) return { status: "processing" };
6898
+ const sources = objectRotationSources(object);
6899
+ if (!sources.length) return { status: "failed", error: "object completed with no rotation images" };
6900
+ const backgroundJobId = context?.metadata?.objectPro?.backgroundJobId;
6901
+ return {
6902
+ status: "ready",
6903
+ objectId: object.id,
6904
+ sourceUrl: sources[0].url,
6905
+ sources,
6906
+ metadata: {
6907
+ objectPro: {
6908
+ objectId: object.id,
6909
+ groupId: object.group_id ?? null,
6910
+ name: object.name,
6911
+ stateName: object.state_name ?? null,
6912
+ directions: object.directions,
6913
+ size: object.size
6914
+ }
6915
+ },
6916
+ billed: await this.billedForJob(backgroundJobId)
6917
+ };
6918
+ }
6919
+ /**
6920
+ * Same resilience shape as `pollCharacterAnimation`: the background job is
6921
+ * the authoritative record while it lasts, the object's own `animations`
6922
+ * list (matched by the `pixelkiln:` display name this asset submitted, or
6923
+ * a recorded animation id) is the fallback once PixelLab cleans the job
6924
+ * up. Never declares failure from absence alone -- a `rate_limited`
6925
+ * `DirectionSubmission` has no background job id at all, so this keeps
6926
+ * reporting "processing" rather than guessing a job that never existed
6927
+ * has failed.
6928
+ */
6929
+ async pollObjectProAnimation(job, context) {
6930
+ const fps = context?.spec?.objectPro?.animation?.fps ?? 8;
6931
+ const review = (frames, animationId, groupId, billed) => ({
6932
+ status: "review-set",
6933
+ objectId: `${job.objectId}#${groupId ?? animationId ?? job.name}`,
6934
+ frameUrls: frames,
6935
+ sources: frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` })),
6936
+ fps,
6937
+ metadata: {
6938
+ frameSet: { fps, count: frames.length },
6939
+ objectPro: {
6940
+ kind: "animation",
6941
+ objectId: job.objectId,
6942
+ animationId,
6943
+ animationGroupId: groupId,
6944
+ animationName: job.name,
6945
+ direction: job.direction
6946
+ }
6947
+ },
6948
+ billed
6949
+ });
6950
+ if (job.jobId) {
6951
+ const status = await this.client.getBackgroundJob(job.jobId).catch((err) => {
6952
+ if (err instanceof PixelLabError && err.status === 404) return null;
6953
+ throw err;
6954
+ });
6955
+ if (status) {
6956
+ if (status.status === "failed") return { status: "failed", error: "animation generation failed upstream" };
6957
+ if (status.status !== "completed") return { status: "processing" };
6958
+ const done = status.last_response ?? {};
6959
+ const frames = done.storage_urls?.frames;
6960
+ const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
6961
+ if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
6962
+ const object2 = await this.client.getObject(job.objectId);
6963
+ const group = findObjectAnimation(object2, job.name, job.direction, animationId);
6964
+ return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
6965
+ }
6966
+ }
6967
+ }
6968
+ const recorded = context?.metadata?.objectPro?.animationId ?? null;
6969
+ const object = await this.client.getObject(job.objectId);
6970
+ const found = findObjectAnimation(object, job.name, job.direction, recorded);
6971
+ if (found) return review(found.frames, found.animationId, found.groupId, null);
6972
+ return { status: "processing" };
6973
+ }
6460
6974
  async submit(spec, styleImages, context) {
6461
6975
  this.validate(spec, styleImages);
6462
6976
  if (spec.revision) return this.submitRevision(spec);
6463
6977
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
6978
+ if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
6464
6979
  if (spec.generator === "pixflux") {
6465
6980
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
6466
6981
  const { png } = await this.client.createImagePixflux({
@@ -6521,6 +7036,20 @@ var PixelLabProvider = class _PixelLabProvider {
6521
7036
  });
6522
7037
  return { jobId: res2.tileset_id, metadata: { backgroundJobId: res2.background_job_id } };
6523
7038
  }
7039
+ if (spec.generator === "isometricTile") {
7040
+ const res2 = await this.client.createIsometricTile({
7041
+ description: spec.prompt,
7042
+ imageWidth: spec.width,
7043
+ imageHeight: spec.height,
7044
+ tileSize: spec.isometricTileSize,
7045
+ tileShape: spec.isometricTileShape,
7046
+ outline: spec.outline,
7047
+ shading: spec.shading,
7048
+ detail: spec.detail,
7049
+ seed: spec.seed
7050
+ });
7051
+ return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
7052
+ }
6524
7053
  if (spec.generator === "imagePro") {
6525
7054
  const res2 = await this.client.createImagePro({
6526
7055
  description: spec.prompt,
@@ -6637,8 +7166,10 @@ var PixelLabProvider = class _PixelLabProvider {
6637
7166
  if (generator === "map") return this.pollMap(jobId, context);
6638
7167
  if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
6639
7168
  if (generator === "terrain") return this.pollTerrain(jobId, context);
7169
+ if (generator === "isometricTile") return this.pollIsometricTile(jobId, context);
6640
7170
  if (generator === "imagePro") return this.pollImagePro(jobId);
6641
7171
  if (generator === "character") return this.pollCharacter(jobId, context);
7172
+ if (generator === "objectPro") return this.pollObjectPro(jobId, context);
6642
7173
  const backgroundJobId = context?.metadata?.backgroundJobId;
6643
7174
  const obj = await this.client.getObject(jobId);
6644
7175
  if (obj.status === "review") {
@@ -6847,6 +7378,31 @@ var PixelLabProvider = class _PixelLabProvider {
6847
7378
  throw err;
6848
7379
  }
6849
7380
  }
7381
+ /**
7382
+ * `/create-isometric-tile` reports progress the same way `/create-tileset`
7383
+ * does (423 while drawing, 200 once finished, no `status` field to poll),
7384
+ * and its image also comes back embedded as base64 rather than a
7385
+ * `storage_urls` link. One call is one tile with nothing to review, so
7386
+ * this goes straight to "ready" like a connectable `tiles`/`terrain` set.
7387
+ */
7388
+ async pollIsometricTile(tileId, context) {
7389
+ try {
7390
+ const { image, usage } = await this.client.getIsometricTile(tileId);
7391
+ const backgroundJobId = context?.metadata?.backgroundJobId;
7392
+ const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
7393
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
7394
+ writeFileSync(file, Buffer.from(image.base64, "base64"));
7395
+ return {
7396
+ status: "ready",
7397
+ objectId: tileId,
7398
+ sourceUrl: `file://${file}`,
7399
+ billed
7400
+ };
7401
+ } catch (err) {
7402
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
7403
+ throw err;
7404
+ }
7405
+ }
6850
7406
  async selectCandidate(jobId, index, commonTag, generator) {
6851
7407
  if (generator === "tiles") {
6852
7408
  const set = await this.client.getTilesPro(jobId);
@@ -6963,9 +7519,9 @@ function remoteCharacter(character) {
6963
7519
  animationCount: character.animation_count
6964
7520
  };
6965
7521
  }
6966
- function requirePixelLabOption(name, value, allowed) {
7522
+ function requirePixelLabOption(generator, name, value, allowed) {
6967
7523
  if (value != null && !allowed.includes(value)) {
6968
- throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
7524
+ throw new Error(`PixelLab ${generator} ${name} must be one of: ${allowed.join(", ")}`);
6969
7525
  }
6970
7526
  }
6971
7527
  function tilesInIndexOrder(urls) {
@@ -7031,6 +7587,28 @@ function findAnimation(character, name, direction, animationId) {
7031
7587
  }
7032
7588
  return null;
7033
7589
  }
7590
+ function objectRotationSources(object) {
7591
+ const order = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
7592
+ const sources = [];
7593
+ for (const direction of order) {
7594
+ const url = object.rotation_urls?.[direction];
7595
+ if (url) sources.push({ url, role: direction });
7596
+ }
7597
+ return sources;
7598
+ }
7599
+ function findObjectAnimation(object, name, direction, animationId) {
7600
+ for (const group of object.animations) {
7601
+ const match = group.directions.find((d) => d.direction === direction);
7602
+ const frames = match ? match.storage_urls.frames : void 0;
7603
+ if (!match || !Array.isArray(frames) || !frames.length || !frames.every((f) => typeof f === "string")) continue;
7604
+ const byName = group.display_name === name;
7605
+ const byId = animationId !== null && frames.some((url) => url.includes(`/animations/${animationId}/`));
7606
+ if (!byName && !byId) continue;
7607
+ const fromUrl = /\/animations\/([^/]+)\//.exec(frames[0])?.[1] ?? null;
7608
+ return { frames, groupId: group.animation_group_id, animationId: animationId ?? fromUrl };
7609
+ }
7610
+ return null;
7611
+ }
7034
7612
 
7035
7613
  // src/providers/retrodiffusion.ts
7036
7614
  var DEFAULT_BASE_URL2 = "https://api.retrodiffusion.ai/v1";
@@ -8206,14 +8784,22 @@ async function resolveSpecs(loaded, filter) {
8206
8784
  size = style.terrainTileSize ?? 16;
8207
8785
  width = size;
8208
8786
  height = size;
8787
+ } else if (generator === "isometricTile") {
8788
+ size = asset.size ?? style.size ?? 32;
8789
+ width = size;
8790
+ height = size;
8791
+ } else if (generator === "objectPro") {
8792
+ size = asset.size ?? style.size ?? 64;
8793
+ width = size;
8794
+ height = size;
8209
8795
  } else {
8210
8796
  width = asset.width ?? style.size ?? 64;
8211
8797
  height = asset.height ?? style.size ?? 64;
8212
8798
  size = Math.max(width, height);
8213
8799
  }
8214
- if ((asset.state || asset.animation) && generator !== "character") {
8800
+ if ((asset.state || asset.animation) && generator !== "character" && generator !== "objectPro") {
8215
8801
  throw new Error(
8216
- `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character style; "${styleId}" generates ${generator}`
8802
+ `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
8217
8803
  );
8218
8804
  }
8219
8805
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
@@ -8234,7 +8820,7 @@ async function resolveSpecs(loaded, filter) {
8234
8820
  terrainUpperDescription = wrap(parsed.upper);
8235
8821
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
8236
8822
  prompt = subject.trim();
8237
- } else if (generator === "character" && characterKind !== "base") {
8823
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
8238
8824
  prompt = subject.trim();
8239
8825
  } else {
8240
8826
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
@@ -8299,8 +8885,11 @@ async function resolveSpecs(loaded, filter) {
8299
8885
  terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
8300
8886
  terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
8301
8887
  terrainView: generator === "terrain" ? style.terrainView : void 0,
8888
+ isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
8889
+ isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
8302
8890
  ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8303
- cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generationCost(width, height, generator),
8891
+ ...generator === "objectPro" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
8892
+ cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
8304
8893
  costUnit: "generations",
8305
8894
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
8306
8895
  };
@@ -8365,6 +8954,18 @@ async function resolveSpecs(loaded, filter) {
8365
8954
  } else if (sourceSpec.character) {
8366
8955
  resolved.character = { ...sourceSpec.character };
8367
8956
  }
8957
+ if (sourceSpec.objectPro?.kind === "animation") {
8958
+ const facing = sourceSpec.objectPro.animation.direction;
8959
+ const mirrored = MIRRORED_DIRECTION[facing];
8960
+ if (mirrored === facing) {
8961
+ throw new Error(
8962
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
8963
+ );
8964
+ }
8965
+ resolved.objectPro = { ...sourceSpec.objectPro, animation: { ...sourceSpec.objectPro.animation, direction: mirrored } };
8966
+ } else if (sourceSpec.objectPro) {
8967
+ resolved.objectPro = { ...sourceSpec.objectPro };
8968
+ }
8368
8969
  resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
8369
8970
  resolved.generator = sourceSpec.generator;
8370
8971
  resolved.width = sourceSpec.width;
@@ -8406,6 +9007,23 @@ async function resolveSpecs(loaded, filter) {
8406
9007
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
8407
9008
  };
8408
9009
  }
9010
+ if (resolved.objectPro && characterParent) {
9011
+ const parentSpec = await finalize(characterParent);
9012
+ if (!parentSpec.objectPro) {
9013
+ throw new Error(`assets.${assetId}: ${characterParent} is not an objectPro asset`);
9014
+ }
9015
+ const parentDirections = parentSpec.objectPro.directions;
9016
+ const parentFile = memberPath(parentSpec.outFile, "south", 0, parentDirections, MediaType.PNG);
9017
+ resolved.objectPro = {
9018
+ ...resolved.objectPro,
9019
+ parentAssetId: characterParent,
9020
+ parentSpec,
9021
+ parentFile,
9022
+ parentSha256: existsSync8(parentFile) ? await sha256File(parentFile) : null,
9023
+ // A state and its animations keep the base's own rotation count.
9024
+ directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
9025
+ };
9026
+ }
8409
9027
  if (asset.revision) {
8410
9028
  if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
8411
9029
  throw new Error(
@@ -8588,6 +9206,60 @@ async function resolveCharacterShape(asset, style, kind, files) {
8588
9206
  }
8589
9207
  return shape;
8590
9208
  }
9209
+ async function resolveObjectProShape(assetId, asset, style, kind, files) {
9210
+ const directions = style.objectDirections ?? 8;
9211
+ const shape = {
9212
+ kind,
9213
+ directions,
9214
+ ...style.styleTraits ? { styleTraits: style.styleTraits } : {}
9215
+ };
9216
+ if (asset.reference) {
9217
+ const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
9218
+ if (south) {
9219
+ const image = await files.load(south, "Reference sprite");
9220
+ shape.reference = { path: path13.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9221
+ }
9222
+ }
9223
+ if (asset.state) {
9224
+ shape.state = {
9225
+ paletteFromReference: asset.state.paletteFromReference,
9226
+ ...asset.state.canvas ? { canvas: asset.state.canvas } : {}
9227
+ };
9228
+ }
9229
+ if (asset.animation) {
9230
+ for (const [field, value] of Object.entries({
9231
+ template: asset.animation.template,
9232
+ subject: asset.animation.subject,
9233
+ outline: asset.animation.outline,
9234
+ shading: asset.animation.shading,
9235
+ detail: asset.animation.detail
9236
+ })) {
9237
+ if (value !== void 0) {
9238
+ throw new Error(
9239
+ `assets.${assetId}: objectPro has no skeleton/template concept, so animation.${field} does not apply; describe the motion in the asset's own prompt instead`
9240
+ );
9241
+ }
9242
+ }
9243
+ if (asset.animation.mode === "template") {
9244
+ throw new Error(`assets.${assetId}: objectPro animations have no "template" mode; use "v3" or "pro"`);
9245
+ }
9246
+ const pose = async (rel, what) => {
9247
+ const image = await files.load(rel, what);
9248
+ return { path: path13.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
9249
+ };
9250
+ shape.animation = {
9251
+ mode: asset.animation.mode ?? "v3",
9252
+ direction: asset.animation.direction,
9253
+ frames: asset.animation.frames ?? 8,
9254
+ fps: asset.animation.fps,
9255
+ keepFirstFrame: asset.animation.keepFirstFrame,
9256
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
9257
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
9258
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
9259
+ };
9260
+ }
9261
+ return shape;
9262
+ }
8591
9263
 
8592
9264
  // src/pipeline/fetch.ts
8593
9265
  import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
@@ -12710,7 +13382,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
12710
13382
  lastSubmitAt = Date.now();
12711
13383
  try {
12712
13384
  const refs = styleImages.get(spec.styleId) ?? [];
12713
- const parentEntry = spec.character?.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
13385
+ const parentSpec = spec.character?.parentSpec ?? spec.objectPro?.parentSpec;
13386
+ const parentEntry = parentSpec ? lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)] : void 0;
12714
13387
  const anchor = spec.character?.styleAnchor;
12715
13388
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
12716
13389
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];