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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/types.ts
2
2
  import { z } from "zod";
3
3
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
4
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro"]);
4
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
5
5
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
6
6
  var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
7
7
  function tileVariationCount(descriptions) {
@@ -35,6 +35,9 @@ function tilesCost(tileSize, variations) {
35
35
  if (px <= 2048) return 25;
36
36
  return 40;
37
37
  }
38
+ function isometricTileCost() {
39
+ return 1;
40
+ }
38
41
  function terrainTileCount(transitionSize) {
39
42
  return transitionSize === 1 ? 25 : 16;
40
43
  }
@@ -364,6 +367,28 @@ var StyleObjectSchema = z.object({
364
367
  /** `terrain` generator only. Camera angle; the API default is `high
365
368
  * top-down`. */
366
369
  terrainView: z.enum(["low top-down", "high top-down"]).optional(),
370
+ /**
371
+ * `isometricTile` generator only. `/create-isometric-tile`'s own tile
372
+ * grid size, distinct from `size` (the generation canvas, 16-64px — the
373
+ * API's own guidance is that sizes above 24px "often produce better
374
+ * quality results"). The API default is 16.
375
+ */
376
+ isometricTileSize: z.union([z.literal(16), z.literal(32)]).optional(),
377
+ /**
378
+ * `isometricTile` generator only. Vertical thickness, i.e. how much
379
+ * height/elevation variation the tile can represent on a map: `"thin
380
+ * tile"` (~15% of canvas height), `"thick tile"` (~25%), or `"block"`
381
+ * (~50%, the API default).
382
+ */
383
+ isometricTileShape: z.enum(["thin tile", "thick tile", "block"]).optional(),
384
+ /**
385
+ * `objectPro` generator only. Rotations captured for a base: 1 (the API
386
+ * still draws and stores a single facing) or the full 8. The API default
387
+ * is 8. Unlike `character`, `objectPro` has no skeleton/template modes
388
+ * and so no `mode`/`template` fields of its own — `/create-object-pro-flash`
389
+ * is the only base-creation path.
390
+ */
391
+ objectDirections: z.union([z.literal(1), z.literal(8)]).optional(),
367
392
  /**
368
393
  * `pixflux` only. Whether to strip the generated background.
369
394
  *
@@ -568,25 +593,41 @@ var AssetSchema = z.object({
568
593
  revision: RevisionSchema.optional(),
569
594
  /**
570
595
  * The provider's own id for art that already exists on the account, so
571
- * `adopt` can map it without matching bytes: an object id, a character
572
- * id, or `<character id>#<animation group id>` for a character animation.
573
- * Not part of the spec's identity.
596
+ * `adopt` can map it without matching bytes: an object id (an
597
+ * `objectPro` base and state included, since they share PixelLab's
598
+ * `/objects` identity space), a character id, or `<id>#<animation group
599
+ * id>` for a character or `objectPro` animation. Not part of the spec's
600
+ * identity.
574
601
  */
575
602
  remoteId: z.string().min(1).optional(),
576
- /** `character` styles: this asset is a pose or outfit of another character asset. */
603
+ /**
604
+ * `character` or `objectPro` styles: this asset is a pose or edit of
605
+ * another asset in the same family. `objectPro` ignores
606
+ * `paletteFromReference`/`canvas` — the API has no equivalent, and
607
+ * always inherits the parent's exact canvas.
608
+ */
577
609
  state: CharacterStateSchema.optional(),
578
- /** `character` styles: this asset is a loop of another character asset in one direction. */
610
+ /**
611
+ * `character` or `objectPro` styles: this asset is a loop of another
612
+ * asset in the same family, in one direction. `objectPro` has no
613
+ * skeleton/template concept, so `template`/`subject`/`outline`/
614
+ * `shading`/`detail` are rejected on an `objectPro` animation rather
615
+ * than silently ignored — describe the motion in the asset's own
616
+ * `prompt` instead.
617
+ */
579
618
  animation: CharacterAnimationSchema.optional(),
580
619
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
581
620
  proportions: CharacterProportionsSchema.optional(),
582
621
  /**
583
- * `character` styles, bases only: the character's own sprite, which
584
- * PixelLab rotates into the other directions instead of drawing from
585
- * the prompt. A manifest-relative PNG or JPEG of the south-facing
586
- * sprite, or an object keyed by direction (`{ "south": ..., "east":
587
- * ... }`); quadrupeds in `standard` mode need south and east, and only
588
- * `standard` takes more than south. `standard` wants each image at the
589
- * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
622
+ * `character` or `objectPro` styles, bases only: the subject's own
623
+ * sprite, which PixelLab rotates into the other directions instead of
624
+ * drawing from the prompt. A manifest-relative PNG or JPEG of the
625
+ * south-facing sprite, or an object keyed by direction (`{ "south":
626
+ * ..., "east": ... }`); quadrupeds in `standard` character mode need
627
+ * south and east, and only `standard` takes more than south. `objectPro`
628
+ * only ever reads the south image, since `/create-object-pro-flash`
629
+ * rotates from one frame. `standard` wants each image at the style's
630
+ * size; v3 accepts up to 256px, pro up to 168px. The prompt
590
631
  * still guides the result.
591
632
  */
592
633
  reference: z.union([z.string().min(1), z.record(CharacterDirectionSchema, z.string().min(1))]).optional(),
@@ -1068,13 +1109,36 @@ var TilesetGetSchema = z2.object({
1068
1109
  }).passthrough(),
1069
1110
  usage: UsageSchema
1070
1111
  }).passthrough();
1112
+ var IsometricTileSubmitSchema = z2.object({
1113
+ tile_id: z2.string().min(1),
1114
+ background_job_id: z2.string().min(1),
1115
+ status: z2.literal("processing").default("processing")
1116
+ }).passthrough();
1117
+ var IsometricTileGetSchema = z2.object({
1118
+ image: z2.object({ base64: z2.string().min(1), format: z2.string().default("png") }).passthrough(),
1119
+ usage: UsageSchema
1120
+ }).passthrough();
1071
1121
  var RevisionJobSubmitSchema = z2.object({
1072
1122
  background_job_id: z2.string().min(1),
1073
1123
  status: z2.string().default("processing")
1074
1124
  }).passthrough();
1125
+ var ObjectAnimationGroupSchema = z2.object({
1126
+ animation_group_id: z2.string().min(1),
1127
+ display_name: z2.string().nullable().default(null),
1128
+ description: z2.string().default(""),
1129
+ frame_count: z2.number().int().default(0),
1130
+ directions: z2.array(
1131
+ z2.object({
1132
+ direction: z2.string(),
1133
+ storage_urls: z2.record(z2.unknown()).default({}),
1134
+ created_at: z2.string()
1135
+ }).passthrough()
1136
+ ).default([])
1137
+ }).passthrough();
1075
1138
  var PixelLabObjectSchema = z2.object({
1076
1139
  id: z2.string().min(1),
1077
1140
  name: z2.string().nullable().default(null),
1141
+ state_name: z2.string().nullable().optional(),
1078
1142
  prompt: z2.string().default(""),
1079
1143
  size: z2.object({ width: z2.number().int().positive(), height: z2.number().int().positive() }),
1080
1144
  directions: z2.number().default(0),
@@ -1086,7 +1150,9 @@ var PixelLabObjectSchema = z2.object({
1086
1150
  tags: z2.array(z2.string()).default([]),
1087
1151
  status: z2.string().nullable().default(null),
1088
1152
  progress_percent: z2.number().nullable().optional(),
1089
- eta_seconds: z2.number().nullable().optional()
1153
+ eta_seconds: z2.number().nullable().optional(),
1154
+ group_id: z2.string().nullable().optional(),
1155
+ animations: z2.array(ObjectAnimationGroupSchema).default([])
1090
1156
  }).passthrough();
1091
1157
  var MapObjectSchema = z2.object({
1092
1158
  object_id: z2.string().min(1),
@@ -1111,6 +1177,28 @@ var AnimateSubmitSchema = z2.object({
1111
1177
  status: z2.string().default("processing"),
1112
1178
  usage: UsageSchema
1113
1179
  }).passthrough();
1180
+ var ObjectProSubmitSchema = z2.object({
1181
+ object_id: z2.string().min(1),
1182
+ background_job_id: z2.string().min(1),
1183
+ status: z2.string().default("processing"),
1184
+ usage: UsageSchema
1185
+ }).passthrough();
1186
+ var DirectionSubmissionSchema = z2.object({
1187
+ direction: z2.string(),
1188
+ status: z2.string(),
1189
+ background_job_id: z2.string().nullable().default(null),
1190
+ animation_id: z2.string().nullable().default(null)
1191
+ }).passthrough();
1192
+ var AnimateObjectSubmitSchema = z2.object({
1193
+ animation_group_id: z2.string().min(1),
1194
+ object_id: z2.string().min(1),
1195
+ mode: z2.string(),
1196
+ frame_count: z2.number().int(),
1197
+ display_name: z2.string().nullable().default(null),
1198
+ description: z2.string().default(""),
1199
+ submissions: z2.array(DirectionSubmissionSchema),
1200
+ usage: UsageSchema
1201
+ }).passthrough();
1114
1202
  var CharacterAnimationDirectionSchema = z2.object({
1115
1203
  direction: z2.string(),
1116
1204
  frame_count: z2.number().int().min(0),
@@ -1353,6 +1441,33 @@ var PixelLabClient = class {
1353
1441
  );
1354
1442
  return { tileset: res.tileset, usage: res.usage ?? null };
1355
1443
  }
1444
+ /** A single standalone isometric tile — no connectable set, no candidates. */
1445
+ async createIsometricTile(args) {
1446
+ const body = { description: args.description };
1447
+ if (args.imageWidth != null && args.imageHeight != null) {
1448
+ body.image_size = { width: args.imageWidth, height: args.imageHeight };
1449
+ }
1450
+ if (args.tileSize != null) body.isometric_tile_size = args.tileSize;
1451
+ if (args.tileShape) body.isometric_tile_shape = args.tileShape;
1452
+ if (args.outline) body.outline = args.outline;
1453
+ if (args.shading) body.shading = args.shading;
1454
+ if (args.detail) body.detail = args.detail;
1455
+ if (args.seed != null) body.seed = args.seed;
1456
+ return validateResponse(
1457
+ IsometricTileSubmitSchema,
1458
+ await this.request("/create-isometric-tile", { method: "POST", body: JSON.stringify(body) }),
1459
+ "create isometric tile"
1460
+ );
1461
+ }
1462
+ /** Throws PixelLabError(423) while the tile is still processing. */
1463
+ async getIsometricTile(tileId) {
1464
+ const res = await validateResponse(
1465
+ IsometricTileGetSchema,
1466
+ await this.request(`/isometric-tiles/${tileId}`),
1467
+ "get isometric tile"
1468
+ );
1469
+ return { image: res.image, usage: res.usage ?? null };
1470
+ }
1356
1471
  /**
1357
1472
  * Synchronous single-image generation. Returns the PNG inline rather than a
1358
1473
  * job id, and is the only endpoint that honours a forced palette;
@@ -1386,6 +1501,81 @@ var PixelLabClient = class {
1386
1501
  "get object"
1387
1502
  );
1388
1503
  }
1504
+ /**
1505
+ * A standalone object with no skeleton, one call draws the south sprite
1506
+ * (or takes the author's) and rotates it with v3 — the character
1507
+ * pro-flash pattern, minus `template_id` (objects have no skeleton) and
1508
+ * with a real `n_directions` choice (1 or 8, character pro-flash is
1509
+ * always 8). Async only: `/create-object-pro-flash` has no sync response.
1510
+ */
1511
+ async createObjectProFlash(args) {
1512
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1513
+ const traits = args.styleTraits;
1514
+ const body = {
1515
+ description: args.description,
1516
+ n_directions: args.directions,
1517
+ ...args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
1518
+ ...args.view ? { view: args.view } : {},
1519
+ ...args.seed != null ? { seed: args.seed } : {},
1520
+ ...args.styleReference && args.styleReferenceSize ? {
1521
+ style_image: { image: encode(args.styleReference), size: args.styleReferenceSize },
1522
+ ...traits ? {
1523
+ style_options: {
1524
+ ...traits.palette !== void 0 ? { color_palette: traits.palette } : {},
1525
+ ...traits.outline !== void 0 ? { outline: traits.outline } : {},
1526
+ ...traits.detail !== void 0 ? { detail: traits.detail } : {},
1527
+ ...traits.shading !== void 0 ? { shading: traits.shading } : {}
1528
+ }
1529
+ } : {}
1530
+ } : {}
1531
+ };
1532
+ return validateResponse(
1533
+ ObjectProSubmitSchema,
1534
+ await this.request("/create-object-pro-flash", { method: "POST", body: JSON.stringify(body) }),
1535
+ "create-object-pro-flash"
1536
+ );
1537
+ }
1538
+ /** 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. */
1539
+ async createObjectProState(args) {
1540
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/states`, {
1541
+ method: "POST",
1542
+ body: JSON.stringify({
1543
+ edit_description: args.editDescription,
1544
+ ...args.stateName ? { state_name: args.stateName } : {},
1545
+ ...args.seed != null ? { seed: args.seed } : {}
1546
+ })
1547
+ });
1548
+ return validateResponse(ObjectProSubmitSchema, raw, "create object state");
1549
+ }
1550
+ /**
1551
+ * One direction of one object's animation. Unlike character animation,
1552
+ * this endpoint's own `replace_existing` flag regenerates an
1553
+ * already-animated direction directly (character has no such flag and
1554
+ * pixelkiln instead deletes the prior take itself before resubmitting;
1555
+ * see `submitCharacter`). A 1-direction object must not receive
1556
+ * `directions` at all -- the API 400s if it does.
1557
+ */
1558
+ async animateObject(args) {
1559
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1560
+ const body = {
1561
+ mode: args.mode,
1562
+ ...args.directions === 8 ? { directions: [args.direction] } : {},
1563
+ ...args.animationDescription ? { animation_description: args.animationDescription } : {},
1564
+ ...args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
1565
+ ...args.displayName ? { display_name: args.displayName } : {},
1566
+ ...args.frameCount != null ? { frame_count: args.frameCount } : {},
1567
+ ...args.keepFirstFrame === false ? { keep_first_frame: false } : {},
1568
+ ...args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
1569
+ ...args.endFrame ? { end_frame: encode(args.endFrame) } : {},
1570
+ ...args.enhancePrompt ? { enhance_prompt: true } : {},
1571
+ ...args.replaceExisting ? { replace_existing: true } : {}
1572
+ };
1573
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/animations`, {
1574
+ method: "POST",
1575
+ body: JSON.stringify(body)
1576
+ });
1577
+ return validateResponse(AnimateObjectSubmitSchema, raw, "animate object");
1578
+ }
1389
1579
  async getMapObject(objectId) {
1390
1580
  return validateResponse(
1391
1581
  MapObjectSchema,
@@ -2331,6 +2521,8 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
2331
2521
  tileView: spec.tileView,
2332
2522
  tileFeature: spec.tileFeature,
2333
2523
  outlineMode: spec.outlineMode,
2524
+ isometricTileSize: spec.isometricTileSize,
2525
+ isometricTileShape: spec.isometricTileShape,
2334
2526
  styleImages: styleImageHashes,
2335
2527
  // A character family: the engine and rotations of a base, the parent's
2336
2528
  // generated bytes for a state or animation (so a regenerated parent
@@ -2359,6 +2551,23 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
2359
2551
  concept: spec.character.concept?.sha256,
2360
2552
  styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
2361
2553
  } : void 0,
2554
+ // An objectPro family: same shape as character above, minus the
2555
+ // fields it has none of (mode/template/proportions/isometric/concept/
2556
+ // styleAnchor) — see ResolvedObjectPro.
2557
+ objectPro: spec.objectPro ? {
2558
+ kind: spec.objectPro.kind,
2559
+ directions: spec.objectPro.directions,
2560
+ parent: spec.objectPro.parentAssetId,
2561
+ parentSha256: spec.objectPro.parentSha256 ?? null,
2562
+ state: spec.objectPro.state,
2563
+ animation: spec.objectPro.animation ? {
2564
+ ...spec.objectPro.animation,
2565
+ startFrame: spec.objectPro.animation.startFrame?.sha256,
2566
+ endFrame: spec.objectPro.animation.endFrame?.sha256
2567
+ } : void 0,
2568
+ styleTraits: spec.objectPro.styleTraits,
2569
+ reference: spec.objectPro.reference?.sha256
2570
+ } : void 0,
2362
2571
  // A mirror's bytes come from its source's recorded outputs; the plan
2363
2572
  // compares those directly, so only the choice of source is identity.
2364
2573
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -2426,6 +2635,29 @@ function proFlashCharacterCost(width, height, fromReference) {
2426
2635
  function characterAnimationName(spec) {
2427
2636
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
2428
2637
  }
2638
+ function objectAnimationName(spec) {
2639
+ return `pixelkiln:${spec.styleId}/${spec.assetId}`;
2640
+ }
2641
+ function proFlashObjectCost(width, height, directions, fromReference) {
2642
+ const side = Math.max(32, Math.ceil(Math.max(width, height) / 4) * 4);
2643
+ const rotations = Math.max(1, Math.ceil(side * side * directions / 65536));
2644
+ if (fromReference) return rotations;
2645
+ const image = side <= 96 ? 5 : side <= 208 ? 6 : 9;
2646
+ return image + rotations;
2647
+ }
2648
+ function objectProCost(spec) {
2649
+ const object = spec.objectPro;
2650
+ if (!object) return generationCost(spec.width, spec.height, "1dir");
2651
+ const px = spec.width * spec.height;
2652
+ if (object.kind === "animation") {
2653
+ const animation = object.animation;
2654
+ if (animation.mode === "pro") return generationCost(spec.width, spec.height, "1dir");
2655
+ return Math.max(1, Math.ceil(px * animation.frames / 65536));
2656
+ }
2657
+ if (object.kind === "state") return generationCost(spec.width, spec.height, "1dir");
2658
+ const south = object.reference;
2659
+ return south ? proFlashObjectCost(south.width, south.height, object.directions, true) : proFlashObjectCost(spec.width, spec.height, object.directions, false);
2660
+ }
2429
2661
  var CHARACTER_TEMPLATES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
2430
2662
  var PRO_FLASH_TEMPLATES = [...CHARACTER_TEMPLATES, "custom"];
2431
2663
  function encodeAnimationJob(job) {
@@ -2442,6 +2674,20 @@ function decodeAnimationJob(jobId) {
2442
2674
  }
2443
2675
  return null;
2444
2676
  }
2677
+ function encodeObjectAnimationJob(job) {
2678
+ return `object-animation:${Buffer.from(JSON.stringify(job)).toString("base64url")}`;
2679
+ }
2680
+ function decodeObjectAnimationJob(jobId) {
2681
+ if (!jobId.startsWith("object-animation:")) return null;
2682
+ try {
2683
+ const value = JSON.parse(Buffer.from(jobId.slice("object-animation:".length), "base64url").toString("utf8"));
2684
+ if (typeof value?.objectId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
2685
+ return { objectId: value.objectId, name: value.name, direction: value.direction, jobId: typeof value.jobId === "string" ? value.jobId : null };
2686
+ }
2687
+ } catch {
2688
+ }
2689
+ return null;
2690
+ }
2445
2691
  var PixelLabProvider = class _PixelLabProvider {
2446
2692
  constructor(client2) {
2447
2693
  this.client = client2;
@@ -2460,7 +2706,7 @@ var PixelLabProvider = class _PixelLabProvider {
2460
2706
  return new _PixelLabProvider(new PixelLabClient("download-only"));
2461
2707
  }
2462
2708
  supports(generator) {
2463
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character";
2709
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro";
2464
2710
  }
2465
2711
  /**
2466
2712
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -2496,12 +2742,15 @@ var PixelLabProvider = class _PixelLabProvider {
2496
2742
  const height = spec.revision.sourceHeight ?? spec.height;
2497
2743
  return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
2498
2744
  }
2499
- if (spec.generator === "tiles" || spec.generator === "terrain") {
2745
+ if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
2500
2746
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
2501
2747
  }
2502
2748
  if (spec.generator === "character") {
2503
2749
  return { unit: "generations", amount: characterCost(spec), candidates: 1 };
2504
2750
  }
2751
+ if (spec.generator === "objectPro") {
2752
+ return { unit: "generations", amount: objectProCost(spec), candidates: 1 };
2753
+ }
2505
2754
  return {
2506
2755
  unit: "generations",
2507
2756
  amount: generationCost(spec.width, spec.height, spec.generator),
@@ -2527,19 +2776,19 @@ var PixelLabProvider = class _PixelLabProvider {
2527
2776
  );
2528
2777
  }
2529
2778
  if (spec.generator === "map") {
2530
- requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
2531
- requirePixelLabOption("outline", spec.outline, [
2779
+ requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
2780
+ requirePixelLabOption("map", "outline", spec.outline, [
2532
2781
  "single color outline",
2533
2782
  "selective outline",
2534
2783
  "lineless"
2535
2784
  ]);
2536
- requirePixelLabOption("shading", spec.shading, [
2785
+ requirePixelLabOption("map", "shading", spec.shading, [
2537
2786
  "flat shading",
2538
2787
  "basic shading",
2539
2788
  "medium shading",
2540
2789
  "detailed shading"
2541
2790
  ]);
2542
- requirePixelLabOption("detail", spec.detail, [
2791
+ requirePixelLabOption("map", "detail", spec.detail, [
2543
2792
  "low detail",
2544
2793
  "medium detail",
2545
2794
  "high detail"
@@ -2547,7 +2796,7 @@ var PixelLabProvider = class _PixelLabProvider {
2547
2796
  }
2548
2797
  if (spec.generator === "terrain") {
2549
2798
  if (spec.outline) {
2550
- requirePixelLabOption("outline", spec.outline, [
2799
+ requirePixelLabOption("terrain", "outline", spec.outline, [
2551
2800
  "single color black outline",
2552
2801
  "single color outline",
2553
2802
  "selective outline",
@@ -2555,7 +2804,7 @@ var PixelLabProvider = class _PixelLabProvider {
2555
2804
  ]);
2556
2805
  }
2557
2806
  if (spec.shading) {
2558
- requirePixelLabOption("shading", spec.shading, [
2807
+ requirePixelLabOption("terrain", "shading", spec.shading, [
2559
2808
  "flat shading",
2560
2809
  "basic shading",
2561
2810
  "medium shading",
@@ -2564,7 +2813,7 @@ var PixelLabProvider = class _PixelLabProvider {
2564
2813
  ]);
2565
2814
  }
2566
2815
  if (spec.detail) {
2567
- requirePixelLabOption("detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
2816
+ requirePixelLabOption("terrain", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
2568
2817
  }
2569
2818
  if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
2570
2819
  throw new Error(
@@ -2585,7 +2834,32 @@ var PixelLabProvider = class _PixelLabProvider {
2585
2834
  );
2586
2835
  }
2587
2836
  }
2837
+ if (spec.generator === "isometricTile") {
2838
+ if (spec.outline) {
2839
+ requirePixelLabOption("isometricTile", "outline", spec.outline, [
2840
+ "single color outline",
2841
+ "selective outline",
2842
+ "lineless"
2843
+ ]);
2844
+ }
2845
+ if (spec.shading) {
2846
+ requirePixelLabOption("isometricTile", "shading", spec.shading, [
2847
+ "flat shading",
2848
+ "basic shading",
2849
+ "medium shading",
2850
+ "detailed shading",
2851
+ "highly detailed shading"
2852
+ ]);
2853
+ }
2854
+ if (spec.detail) {
2855
+ requirePixelLabOption("isometricTile", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
2856
+ }
2857
+ if (spec.width < 16 || spec.width > 64) {
2858
+ throw new Error(`PixelLab isometricTile: size must be 16 to 64 pixels, got ${spec.width}`);
2859
+ }
2860
+ }
2588
2861
  if (spec.generator === "character") this.validateCharacter(spec, styleImages);
2862
+ if (spec.generator === "objectPro") this.validateObjectPro(spec, styleImages);
2589
2863
  if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
2590
2864
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
2591
2865
  }
@@ -2599,6 +2873,11 @@ var PixelLabProvider = class _PixelLabProvider {
2599
2873
  "PixelLab imagePro does not support style images yet; /generate-image-v2's own reference_images and style_image are not modeled here"
2600
2874
  );
2601
2875
  }
2876
+ if (spec.generator === "isometricTile" && styleImages.length) {
2877
+ throw new Error(
2878
+ "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)"
2879
+ );
2880
+ }
2602
2881
  for (const image of styleImages) {
2603
2882
  if (image.width > 256 || image.height > 256) {
2604
2883
  throw new Error(
@@ -2741,6 +3020,63 @@ var PixelLabProvider = class _PixelLabProvider {
2741
3020
  }
2742
3021
  }
2743
3022
  }
3023
+ validateObjectPro(spec, styleImages = []) {
3024
+ const object = spec.objectPro;
3025
+ if (!object) throw new Error(`${spec.styleId}/${spec.assetId} has no objectPro shape`);
3026
+ const label = `${spec.styleId}/${spec.assetId}`;
3027
+ if (object.kind === "base" && object.reference && styleImages.length) {
3028
+ throw new Error(`${label}: a base rotating its own reference sprite takes no style image`);
3029
+ }
3030
+ if (object.reference && (object.reference.width > 256 || object.reference.height > 256)) {
3031
+ throw new Error(`${label}: reference is ${object.reference.width}x${object.reference.height}; PixelLab objectPro takes up to 256px`);
3032
+ }
3033
+ if (styleImages.length) {
3034
+ if (styleImages.length > 1) throw new Error(`PixelLab objectPro takes one style image`);
3035
+ const image = styleImages[0];
3036
+ if (image.width > 256 || image.height > 256) {
3037
+ throw new Error(`PixelLab objectPro style image is ${image.width}x${image.height}; the limit is 256px`);
3038
+ }
3039
+ if (image.width > spec.width || image.height > spec.height) {
3040
+ throw new Error(
3041
+ `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`
3042
+ );
3043
+ }
3044
+ }
3045
+ if (object.kind === "base" && (spec.width < 16 || spec.height < 16 || spec.width > 256 || spec.height > 256)) {
3046
+ throw new Error(`PixelLab objectPro bases must be between 16 and 256 pixels`);
3047
+ }
3048
+ if (object.kind === "base" && (spec.width % 4 || spec.height % 4)) {
3049
+ throw new Error(`PixelLab objectPro bases are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
3050
+ }
3051
+ if (object.kind !== "base" && !object.parentAssetId) {
3052
+ throw new Error(`${spec.styleId}/${spec.assetId} is a ${object.kind} with no parent`);
3053
+ }
3054
+ if (object.animation) {
3055
+ const allowed = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
3056
+ if (!allowed.includes(object.animation.direction)) {
3057
+ throw new Error(
3058
+ `${label} animates "${object.animation.direction}", but its object has ${object.directions} direction${object.directions === 1 ? "" : "s"}: ${allowed.join(", ")}`
3059
+ );
3060
+ }
3061
+ if (!spec.prompt.trim()) {
3062
+ throw new Error(`${label} needs a prompt describing the motion`);
3063
+ }
3064
+ const { startFrame, endFrame } = object.animation;
3065
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
3066
+ if (image && (image.width > 256 || image.height > 256)) {
3067
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
3068
+ }
3069
+ }
3070
+ if (endFrame) {
3071
+ const start = startFrame ?? (object.parentFile && existsSync(object.parentFile) ? imageMetadata(readFileSync(object.parentFile)) : null);
3072
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
3073
+ throw new Error(
3074
+ `${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`
3075
+ );
3076
+ }
3077
+ }
3078
+ }
3079
+ }
2744
3080
  /**
2745
3081
  * A base is one request that answers with the character id; a state is
2746
3082
  * the same with the parent's id; an animation names itself after the
@@ -2834,6 +3170,78 @@ var PixelLabProvider = class _PixelLabProvider {
2834
3170
  metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
2835
3171
  };
2836
3172
  }
3173
+ /**
3174
+ * Same shape as `submitCharacter`, minus the mode branching (objectPro has
3175
+ * exactly one base-creation path) and minus the client-side "find and
3176
+ * delete the prior take" dance for animation: `/objects/{id}/animations`
3177
+ * has its own `replace_existing` flag, so this always passes it rather
3178
+ * than fetching the object first to check.
3179
+ */
3180
+ async submitObjectPro(spec, styleImages, context) {
3181
+ const object = spec.objectPro;
3182
+ if (object.kind === "base") {
3183
+ const styleImage = styleImages[0];
3184
+ const res2 = await this.client.createObjectProFlash({
3185
+ description: spec.prompt,
3186
+ directions: object.directions,
3187
+ size: spec.width,
3188
+ view: spec.view,
3189
+ seed: spec.seed,
3190
+ reference: object.reference ? readPose(spec, "reference sprite", object.reference) : void 0,
3191
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
3192
+ styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
3193
+ styleTraits: object.styleTraits
3194
+ });
3195
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "base", objectId: res2.object_id, directions: object.directions, backgroundJobId: res2.background_job_id } } };
3196
+ }
3197
+ const parentId = context?.parentObjectId;
3198
+ if (!parentId) {
3199
+ throw new Error(
3200
+ `${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`
3201
+ );
3202
+ }
3203
+ if (object.kind === "state") {
3204
+ const res2 = await this.client.createObjectProState({
3205
+ objectId: parentId,
3206
+ editDescription: spec.prompt,
3207
+ stateName: spec.assetId.slice(0, 50),
3208
+ seed: spec.seed
3209
+ });
3210
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "state", objectId: res2.object_id, parentObjectId: parentId, directions: object.directions, backgroundJobId: res2.background_job_id } } };
3211
+ }
3212
+ const animation = object.animation;
3213
+ const name = objectAnimationName(spec);
3214
+ const res = await this.client.animateObject({
3215
+ objectId: parentId,
3216
+ directions: object.directions,
3217
+ direction: animation.direction,
3218
+ animationDescription: spec.prompt.trim() || void 0,
3219
+ displayName: name,
3220
+ mode: animation.mode === "pro" ? "pro" : "v3",
3221
+ frameCount: animation.frames,
3222
+ keepFirstFrame: animation.keepFirstFrame,
3223
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
3224
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
3225
+ enhancePrompt: animation.enhancePrompt,
3226
+ replaceExisting: true
3227
+ });
3228
+ const submission = res.submissions.find((s) => s.direction === animation.direction) ?? res.submissions[0];
3229
+ const job = { objectId: parentId, name, direction: animation.direction, jobId: submission?.background_job_id ?? null };
3230
+ return {
3231
+ jobId: encodeObjectAnimationJob(job),
3232
+ metadata: {
3233
+ objectPro: {
3234
+ kind: "animation",
3235
+ objectId: parentId,
3236
+ animationName: name,
3237
+ direction: animation.direction,
3238
+ mode: animation.mode,
3239
+ animationGroupId: res.animation_group_id,
3240
+ animationId: submission?.animation_id ?? null
3241
+ }
3242
+ }
3243
+ };
3244
+ }
2837
3245
  async pollCharacter(jobId, context) {
2838
3246
  const animation = decodeAnimationJob(jobId);
2839
3247
  if (animation) return this.pollCharacterAnimation(animation, context);
@@ -2917,10 +3325,93 @@ var PixelLabProvider = class _PixelLabProvider {
2917
3325
  if (found) return review(found.frames, found.animationId, found.groupId, null);
2918
3326
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
2919
3327
  }
3328
+ async pollObjectPro(jobId, context) {
3329
+ const animation = decodeObjectAnimationJob(jobId);
3330
+ if (animation) return this.pollObjectProAnimation(animation, context);
3331
+ const object = await this.client.getObject(jobId);
3332
+ if (object.status === "failed") return { status: "failed", error: "object generation failed upstream" };
3333
+ if (object.status !== "completed" || !object.rotation_urls) return { status: "processing" };
3334
+ const sources = objectRotationSources(object);
3335
+ if (!sources.length) return { status: "failed", error: "object completed with no rotation images" };
3336
+ const backgroundJobId = context?.metadata?.objectPro?.backgroundJobId;
3337
+ return {
3338
+ status: "ready",
3339
+ objectId: object.id,
3340
+ sourceUrl: sources[0].url,
3341
+ sources,
3342
+ metadata: {
3343
+ objectPro: {
3344
+ objectId: object.id,
3345
+ groupId: object.group_id ?? null,
3346
+ name: object.name,
3347
+ stateName: object.state_name ?? null,
3348
+ directions: object.directions,
3349
+ size: object.size
3350
+ }
3351
+ },
3352
+ billed: await this.billedForJob(backgroundJobId)
3353
+ };
3354
+ }
3355
+ /**
3356
+ * Same resilience shape as `pollCharacterAnimation`: the background job is
3357
+ * the authoritative record while it lasts, the object's own `animations`
3358
+ * list (matched by the `pixelkiln:` display name this asset submitted, or
3359
+ * a recorded animation id) is the fallback once PixelLab cleans the job
3360
+ * up. Never declares failure from absence alone -- a `rate_limited`
3361
+ * `DirectionSubmission` has no background job id at all, so this keeps
3362
+ * reporting "processing" rather than guessing a job that never existed
3363
+ * has failed.
3364
+ */
3365
+ async pollObjectProAnimation(job, context) {
3366
+ const fps = context?.spec?.objectPro?.animation?.fps ?? 8;
3367
+ const review = (frames, animationId, groupId, billed) => ({
3368
+ status: "review-set",
3369
+ objectId: `${job.objectId}#${groupId ?? animationId ?? job.name}`,
3370
+ frameUrls: frames,
3371
+ sources: frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` })),
3372
+ fps,
3373
+ metadata: {
3374
+ frameSet: { fps, count: frames.length },
3375
+ objectPro: {
3376
+ kind: "animation",
3377
+ objectId: job.objectId,
3378
+ animationId,
3379
+ animationGroupId: groupId,
3380
+ animationName: job.name,
3381
+ direction: job.direction
3382
+ }
3383
+ },
3384
+ billed
3385
+ });
3386
+ if (job.jobId) {
3387
+ const status = await this.client.getBackgroundJob(job.jobId).catch((err) => {
3388
+ if (err instanceof PixelLabError && err.status === 404) return null;
3389
+ throw err;
3390
+ });
3391
+ if (status) {
3392
+ if (status.status === "failed") return { status: "failed", error: "animation generation failed upstream" };
3393
+ if (status.status !== "completed") return { status: "processing" };
3394
+ const done = status.last_response ?? {};
3395
+ const frames = done.storage_urls?.frames;
3396
+ const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
3397
+ if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
3398
+ const object2 = await this.client.getObject(job.objectId);
3399
+ const group = findObjectAnimation(object2, job.name, job.direction, animationId);
3400
+ return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
3401
+ }
3402
+ }
3403
+ }
3404
+ const recorded = context?.metadata?.objectPro?.animationId ?? null;
3405
+ const object = await this.client.getObject(job.objectId);
3406
+ const found = findObjectAnimation(object, job.name, job.direction, recorded);
3407
+ if (found) return review(found.frames, found.animationId, found.groupId, null);
3408
+ return { status: "processing" };
3409
+ }
2920
3410
  async submit(spec, styleImages, context) {
2921
3411
  this.validate(spec, styleImages);
2922
3412
  if (spec.revision) return this.submitRevision(spec);
2923
3413
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
3414
+ if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
2924
3415
  if (spec.generator === "pixflux") {
2925
3416
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
2926
3417
  const { png } = await this.client.createImagePixflux({
@@ -2981,6 +3472,20 @@ var PixelLabProvider = class _PixelLabProvider {
2981
3472
  });
2982
3473
  return { jobId: res2.tileset_id, metadata: { backgroundJobId: res2.background_job_id } };
2983
3474
  }
3475
+ if (spec.generator === "isometricTile") {
3476
+ const res2 = await this.client.createIsometricTile({
3477
+ description: spec.prompt,
3478
+ imageWidth: spec.width,
3479
+ imageHeight: spec.height,
3480
+ tileSize: spec.isometricTileSize,
3481
+ tileShape: spec.isometricTileShape,
3482
+ outline: spec.outline,
3483
+ shading: spec.shading,
3484
+ detail: spec.detail,
3485
+ seed: spec.seed
3486
+ });
3487
+ return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
3488
+ }
2984
3489
  if (spec.generator === "imagePro") {
2985
3490
  const res2 = await this.client.createImagePro({
2986
3491
  description: spec.prompt,
@@ -3097,8 +3602,10 @@ var PixelLabProvider = class _PixelLabProvider {
3097
3602
  if (generator === "map") return this.pollMap(jobId, context);
3098
3603
  if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
3099
3604
  if (generator === "terrain") return this.pollTerrain(jobId, context);
3605
+ if (generator === "isometricTile") return this.pollIsometricTile(jobId, context);
3100
3606
  if (generator === "imagePro") return this.pollImagePro(jobId);
3101
3607
  if (generator === "character") return this.pollCharacter(jobId, context);
3608
+ if (generator === "objectPro") return this.pollObjectPro(jobId, context);
3102
3609
  const backgroundJobId = context?.metadata?.backgroundJobId;
3103
3610
  const obj = await this.client.getObject(jobId);
3104
3611
  if (obj.status === "review") {
@@ -3307,6 +3814,31 @@ var PixelLabProvider = class _PixelLabProvider {
3307
3814
  throw err;
3308
3815
  }
3309
3816
  }
3817
+ /**
3818
+ * `/create-isometric-tile` reports progress the same way `/create-tileset`
3819
+ * does (423 while drawing, 200 once finished, no `status` field to poll),
3820
+ * and its image also comes back embedded as base64 rather than a
3821
+ * `storage_urls` link. One call is one tile with nothing to review, so
3822
+ * this goes straight to "ready" like a connectable `tiles`/`terrain` set.
3823
+ */
3824
+ async pollIsometricTile(tileId, context) {
3825
+ try {
3826
+ const { image, usage } = await this.client.getIsometricTile(tileId);
3827
+ const backgroundJobId = context?.metadata?.backgroundJobId;
3828
+ const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
3829
+ const file = path.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
3830
+ writeFileSync(file, Buffer.from(image.base64, "base64"));
3831
+ return {
3832
+ status: "ready",
3833
+ objectId: tileId,
3834
+ sourceUrl: `file://${file}`,
3835
+ billed
3836
+ };
3837
+ } catch (err) {
3838
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
3839
+ throw err;
3840
+ }
3841
+ }
3310
3842
  async selectCandidate(jobId, index, commonTag, generator) {
3311
3843
  if (generator === "tiles") {
3312
3844
  const set = await this.client.getTilesPro(jobId);
@@ -3423,9 +3955,9 @@ function remoteCharacter(character) {
3423
3955
  animationCount: character.animation_count
3424
3956
  };
3425
3957
  }
3426
- function requirePixelLabOption(name, value, allowed) {
3958
+ function requirePixelLabOption(generator, name, value, allowed) {
3427
3959
  if (value != null && !allowed.includes(value)) {
3428
- throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
3960
+ throw new Error(`PixelLab ${generator} ${name} must be one of: ${allowed.join(", ")}`);
3429
3961
  }
3430
3962
  }
3431
3963
  function tilesInIndexOrder(urls) {
@@ -3491,6 +4023,28 @@ function findAnimation(character, name, direction, animationId) {
3491
4023
  }
3492
4024
  return null;
3493
4025
  }
4026
+ function objectRotationSources(object) {
4027
+ const order = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
4028
+ const sources = [];
4029
+ for (const direction of order) {
4030
+ const url = object.rotation_urls?.[direction];
4031
+ if (url) sources.push({ url, role: direction });
4032
+ }
4033
+ return sources;
4034
+ }
4035
+ function findObjectAnimation(object, name, direction, animationId) {
4036
+ for (const group of object.animations) {
4037
+ const match = group.directions.find((d) => d.direction === direction);
4038
+ const frames = match ? match.storage_urls.frames : void 0;
4039
+ if (!match || !Array.isArray(frames) || !frames.length || !frames.every((f) => typeof f === "string")) continue;
4040
+ const byName = group.display_name === name;
4041
+ const byId = animationId !== null && frames.some((url) => url.includes(`/animations/${animationId}/`));
4042
+ if (!byName && !byId) continue;
4043
+ const fromUrl = /\/animations\/([^/]+)\//.exec(frames[0])?.[1] ?? null;
4044
+ return { frames, groupId: group.animation_group_id, animationId: animationId ?? fromUrl };
4045
+ }
4046
+ return null;
4047
+ }
3494
4048
 
3495
4049
  // src/providers/retrodiffusion.ts
3496
4050
  var DEFAULT_BASE_URL = "https://api.retrodiffusion.ai/v1";
@@ -6044,14 +6598,22 @@ async function resolveSpecs(loaded, filter) {
6044
6598
  size = style.terrainTileSize ?? 16;
6045
6599
  width = size;
6046
6600
  height = size;
6601
+ } else if (generator === "isometricTile") {
6602
+ size = asset.size ?? style.size ?? 32;
6603
+ width = size;
6604
+ height = size;
6605
+ } else if (generator === "objectPro") {
6606
+ size = asset.size ?? style.size ?? 64;
6607
+ width = size;
6608
+ height = size;
6047
6609
  } else {
6048
6610
  width = asset.width ?? style.size ?? 64;
6049
6611
  height = asset.height ?? style.size ?? 64;
6050
6612
  size = Math.max(width, height);
6051
6613
  }
6052
- if ((asset.state || asset.animation) && generator !== "character") {
6614
+ if ((asset.state || asset.animation) && generator !== "character" && generator !== "objectPro") {
6053
6615
  throw new Error(
6054
- `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character style; "${styleId}" generates ${generator}`
6616
+ `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
6055
6617
  );
6056
6618
  }
6057
6619
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
@@ -6072,7 +6634,7 @@ async function resolveSpecs(loaded, filter) {
6072
6634
  terrainUpperDescription = wrap(parsed.upper);
6073
6635
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
6074
6636
  prompt = subject.trim();
6075
- } else if (generator === "character" && characterKind !== "base") {
6637
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
6076
6638
  prompt = subject.trim();
6077
6639
  } else {
6078
6640
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
@@ -6137,8 +6699,11 @@ async function resolveSpecs(loaded, filter) {
6137
6699
  terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
6138
6700
  terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
6139
6701
  terrainView: generator === "terrain" ? style.terrainView : void 0,
6702
+ isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
6703
+ isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
6140
6704
  ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
6141
- cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generationCost(width, height, generator),
6705
+ ...generator === "objectPro" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
6706
+ cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
6142
6707
  costUnit: "generations",
6143
6708
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
6144
6709
  };
@@ -6203,6 +6768,18 @@ async function resolveSpecs(loaded, filter) {
6203
6768
  } else if (sourceSpec.character) {
6204
6769
  resolved.character = { ...sourceSpec.character };
6205
6770
  }
6771
+ if (sourceSpec.objectPro?.kind === "animation") {
6772
+ const facing = sourceSpec.objectPro.animation.direction;
6773
+ const mirrored = MIRRORED_DIRECTION[facing];
6774
+ if (mirrored === facing) {
6775
+ throw new Error(
6776
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
6777
+ );
6778
+ }
6779
+ resolved.objectPro = { ...sourceSpec.objectPro, animation: { ...sourceSpec.objectPro.animation, direction: mirrored } };
6780
+ } else if (sourceSpec.objectPro) {
6781
+ resolved.objectPro = { ...sourceSpec.objectPro };
6782
+ }
6206
6783
  resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
6207
6784
  resolved.generator = sourceSpec.generator;
6208
6785
  resolved.width = sourceSpec.width;
@@ -6244,6 +6821,23 @@ async function resolveSpecs(loaded, filter) {
6244
6821
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
6245
6822
  };
6246
6823
  }
6824
+ if (resolved.objectPro && characterParent) {
6825
+ const parentSpec = await finalize(characterParent);
6826
+ if (!parentSpec.objectPro) {
6827
+ throw new Error(`assets.${assetId}: ${characterParent} is not an objectPro asset`);
6828
+ }
6829
+ const parentDirections = parentSpec.objectPro.directions;
6830
+ const parentFile = memberPath(parentSpec.outFile, "south", 0, parentDirections, MediaType.PNG);
6831
+ resolved.objectPro = {
6832
+ ...resolved.objectPro,
6833
+ parentAssetId: characterParent,
6834
+ parentSpec,
6835
+ parentFile,
6836
+ parentSha256: existsSync4(parentFile) ? await sha256File(parentFile) : null,
6837
+ // A state and its animations keep the base's own rotation count.
6838
+ directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
6839
+ };
6840
+ }
6247
6841
  if (asset.revision) {
6248
6842
  if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
6249
6843
  throw new Error(
@@ -6429,6 +7023,60 @@ async function resolveCharacterShape(asset, style, kind, files) {
6429
7023
  }
6430
7024
  return shape;
6431
7025
  }
7026
+ async function resolveObjectProShape(assetId, asset, style, kind, files) {
7027
+ const directions = style.objectDirections ?? 8;
7028
+ const shape = {
7029
+ kind,
7030
+ directions,
7031
+ ...style.styleTraits ? { styleTraits: style.styleTraits } : {}
7032
+ };
7033
+ if (asset.reference) {
7034
+ const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
7035
+ if (south) {
7036
+ const image = await files.load(south, "Reference sprite");
7037
+ shape.reference = { path: path5.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
7038
+ }
7039
+ }
7040
+ if (asset.state) {
7041
+ shape.state = {
7042
+ paletteFromReference: asset.state.paletteFromReference,
7043
+ ...asset.state.canvas ? { canvas: asset.state.canvas } : {}
7044
+ };
7045
+ }
7046
+ if (asset.animation) {
7047
+ for (const [field, value] of Object.entries({
7048
+ template: asset.animation.template,
7049
+ subject: asset.animation.subject,
7050
+ outline: asset.animation.outline,
7051
+ shading: asset.animation.shading,
7052
+ detail: asset.animation.detail
7053
+ })) {
7054
+ if (value !== void 0) {
7055
+ throw new Error(
7056
+ `assets.${assetId}: objectPro has no skeleton/template concept, so animation.${field} does not apply; describe the motion in the asset's own prompt instead`
7057
+ );
7058
+ }
7059
+ }
7060
+ if (asset.animation.mode === "template") {
7061
+ throw new Error(`assets.${assetId}: objectPro animations have no "template" mode; use "v3" or "pro"`);
7062
+ }
7063
+ const pose = async (rel, what) => {
7064
+ const image = await files.load(rel, what);
7065
+ return { path: path5.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
7066
+ };
7067
+ shape.animation = {
7068
+ mode: asset.animation.mode ?? "v3",
7069
+ direction: asset.animation.direction,
7070
+ frames: asset.animation.frames ?? 8,
7071
+ fps: asset.animation.fps,
7072
+ keepFirstFrame: asset.animation.keepFirstFrame,
7073
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
7074
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
7075
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
7076
+ };
7077
+ }
7078
+ return shape;
7079
+ }
6432
7080
  function characterDirections(directions) {
6433
7081
  return directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
6434
7082
  }
@@ -7905,6 +8553,7 @@ async function inspectRevisionReadiness(spec, lock) {
7905
8553
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
7906
8554
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
7907
8555
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
8556
+ if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
7908
8557
  if (!spec.revision) return null;
7909
8558
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
7910
8559
  }
@@ -7946,6 +8595,24 @@ async function inspectCharacterParent(spec, lock, seen) {
7946
8595
  }
7947
8596
  return { ready: true, reason: `${label} is current` };
7948
8597
  }
8598
+ async function inspectObjectProParent(spec, lock, seen) {
8599
+ const object = spec.objectPro;
8600
+ const parentSpec = object.parentSpec;
8601
+ const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
8602
+ const dependency = await inspectParent(parentSpec, lock, seen);
8603
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
8604
+ if (!object.parentSha256 || !object.parentFile || !existsSync7(object.parentFile)) {
8605
+ return { ready: false, reason: `${label} has no generated south-facing file yet` };
8606
+ }
8607
+ if (await sha256File(object.parentFile) !== object.parentSha256) {
8608
+ return { ready: false, reason: `${label} changed after the manifest was resolved` };
8609
+ }
8610
+ const parentEntry = lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)];
8611
+ if (!parentEntry?.objectId) {
8612
+ return { ready: false, reason: `${label} has no PixelLab object id recorded` };
8613
+ }
8614
+ return { ready: true, reason: `${label} is current` };
8615
+ }
7949
8616
  async function inspectRevision(spec, lock, seen) {
7950
8617
  const revision = spec.revision;
7951
8618
  if (!revision) return { ready: true, reason: "asset has no revision inputs" };
@@ -7997,6 +8664,10 @@ async function inspectParent(spec, lock, seen) {
7997
8664
  const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
7998
8665
  if (!inputs.ready) return inputs;
7999
8666
  }
8667
+ if (spec.objectPro?.parentSpec) {
8668
+ const inputs = await inspectObjectProParent(spec, lock, nextSeen);
8669
+ if (!inputs.ready) return inputs;
8670
+ }
8000
8671
  if (spec.quality) {
8001
8672
  const quality = await inspectQualityProfile(spec, lock);
8002
8673
  return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
@@ -8041,7 +8712,8 @@ function dependencies(spec) {
8041
8712
  spec.mirror?.sourceSpec,
8042
8713
  spec.revision?.sourceSpec,
8043
8714
  spec.character?.parentSpec,
8044
- spec.character?.styleAnchor?.spec
8715
+ spec.character?.styleAnchor?.spec,
8716
+ spec.objectPro?.parentSpec
8045
8717
  ].filter((parent) => Boolean(parent));
8046
8718
  }
8047
8719
  function resumeCommandForStatus(status) {
@@ -10702,7 +11374,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
10702
11374
  lastSubmitAt = Date.now();
10703
11375
  try {
10704
11376
  const refs = styleImages.get(spec.styleId) ?? [];
10705
- const parentEntry = spec.character?.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
11377
+ const parentSpec = spec.character?.parentSpec ?? spec.objectPro?.parentSpec;
11378
+ const parentEntry = parentSpec ? lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)] : void 0;
10706
11379
  const anchor = spec.character?.styleAnchor;
10707
11380
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
10708
11381
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
@@ -14936,6 +15609,7 @@ export {
14936
15609
  isFrameSetEntry,
14937
15610
  isMemberSetEntry,
14938
15611
  isSensitiveSourceUrl,
15612
+ isometricTileCost,
14939
15613
  listBundledRecipes,
14940
15614
  loadClaims,
14941
15615
  loadLock,