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.cjs CHANGED
@@ -169,6 +169,7 @@ __export(index_exports, {
169
169
  isFrameSetEntry: () => isFrameSetEntry,
170
170
  isMemberSetEntry: () => isMemberSetEntry,
171
171
  isSensitiveSourceUrl: () => isSensitiveSourceUrl,
172
+ isometricTileCost: () => isometricTileCost,
172
173
  listBundledRecipes: () => listBundledRecipes,
173
174
  loadClaims: () => loadClaims,
174
175
  loadLock: () => loadLock,
@@ -303,7 +304,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
303
304
  // src/types.ts
304
305
  var import_zod = require("zod");
305
306
  var MediaTypeSchema = import_zod.z.enum(["image/png", "image/gif"]);
306
- var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro"]);
307
+ var GeneratorSchema = import_zod.z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain", "imagePro", "isometricTile", "objectPro"]);
307
308
  var GridConfidenceSchema = import_zod.z.enum(["low", "medium", "high"]);
308
309
  var RevisionModeSchema = import_zod.z.enum(["image-to-image", "inpaint", "outpaint"]);
309
310
  function tileVariationCount(descriptions) {
@@ -337,6 +338,9 @@ function tilesCost(tileSize, variations) {
337
338
  if (px <= 2048) return 25;
338
339
  return 40;
339
340
  }
341
+ function isometricTileCost() {
342
+ return 1;
343
+ }
340
344
  function terrainTileCount(transitionSize) {
341
345
  return transitionSize === 1 ? 25 : 16;
342
346
  }
@@ -666,6 +670,28 @@ var StyleObjectSchema = import_zod.z.object({
666
670
  /** `terrain` generator only. Camera angle; the API default is `high
667
671
  * top-down`. */
668
672
  terrainView: import_zod.z.enum(["low top-down", "high top-down"]).optional(),
673
+ /**
674
+ * `isometricTile` generator only. `/create-isometric-tile`'s own tile
675
+ * grid size, distinct from `size` (the generation canvas, 16-64px — the
676
+ * API's own guidance is that sizes above 24px "often produce better
677
+ * quality results"). The API default is 16.
678
+ */
679
+ isometricTileSize: import_zod.z.union([import_zod.z.literal(16), import_zod.z.literal(32)]).optional(),
680
+ /**
681
+ * `isometricTile` generator only. Vertical thickness, i.e. how much
682
+ * height/elevation variation the tile can represent on a map: `"thin
683
+ * tile"` (~15% of canvas height), `"thick tile"` (~25%), or `"block"`
684
+ * (~50%, the API default).
685
+ */
686
+ isometricTileShape: import_zod.z.enum(["thin tile", "thick tile", "block"]).optional(),
687
+ /**
688
+ * `objectPro` generator only. Rotations captured for a base: 1 (the API
689
+ * still draws and stores a single facing) or the full 8. The API default
690
+ * is 8. Unlike `character`, `objectPro` has no skeleton/template modes
691
+ * and so no `mode`/`template` fields of its own — `/create-object-pro-flash`
692
+ * is the only base-creation path.
693
+ */
694
+ objectDirections: import_zod.z.union([import_zod.z.literal(1), import_zod.z.literal(8)]).optional(),
669
695
  /**
670
696
  * `pixflux` only. Whether to strip the generated background.
671
697
  *
@@ -870,25 +896,41 @@ var AssetSchema = import_zod.z.object({
870
896
  revision: RevisionSchema.optional(),
871
897
  /**
872
898
  * The provider's own id for art that already exists on the account, so
873
- * `adopt` can map it without matching bytes: an object id, a character
874
- * id, or `<character id>#<animation group id>` for a character animation.
875
- * Not part of the spec's identity.
899
+ * `adopt` can map it without matching bytes: an object id (an
900
+ * `objectPro` base and state included, since they share PixelLab's
901
+ * `/objects` identity space), a character id, or `<id>#<animation group
902
+ * id>` for a character or `objectPro` animation. Not part of the spec's
903
+ * identity.
876
904
  */
877
905
  remoteId: import_zod.z.string().min(1).optional(),
878
- /** `character` styles: this asset is a pose or outfit of another character asset. */
906
+ /**
907
+ * `character` or `objectPro` styles: this asset is a pose or edit of
908
+ * another asset in the same family. `objectPro` ignores
909
+ * `paletteFromReference`/`canvas` — the API has no equivalent, and
910
+ * always inherits the parent's exact canvas.
911
+ */
879
912
  state: CharacterStateSchema.optional(),
880
- /** `character` styles: this asset is a loop of another character asset in one direction. */
913
+ /**
914
+ * `character` or `objectPro` styles: this asset is a loop of another
915
+ * asset in the same family, in one direction. `objectPro` has no
916
+ * skeleton/template concept, so `template`/`subject`/`outline`/
917
+ * `shading`/`detail` are rejected on an `objectPro` animation rather
918
+ * than silently ignored — describe the motion in the asset's own
919
+ * `prompt` instead.
920
+ */
881
921
  animation: CharacterAnimationSchema.optional(),
882
922
  /** `character` styles, `standard` humanoid bases: this character's proportions, over the style's. */
883
923
  proportions: CharacterProportionsSchema.optional(),
884
924
  /**
885
- * `character` styles, bases only: the character's own sprite, which
886
- * PixelLab rotates into the other directions instead of drawing from
887
- * the prompt. A manifest-relative PNG or JPEG of the south-facing
888
- * sprite, or an object keyed by direction (`{ "south": ..., "east":
889
- * ... }`); quadrupeds in `standard` mode need south and east, and only
890
- * `standard` takes more than south. `standard` wants each image at the
891
- * style's size; v3 accepts up to 256px, pro up to 168px. The prompt
925
+ * `character` or `objectPro` styles, bases only: the subject's own
926
+ * sprite, which PixelLab rotates into the other directions instead of
927
+ * drawing from the prompt. A manifest-relative PNG or JPEG of the
928
+ * south-facing sprite, or an object keyed by direction (`{ "south":
929
+ * ..., "east": ... }`); quadrupeds in `standard` character mode need
930
+ * south and east, and only `standard` takes more than south. `objectPro`
931
+ * only ever reads the south image, since `/create-object-pro-flash`
932
+ * rotates from one frame. `standard` wants each image at the style's
933
+ * size; v3 accepts up to 256px, pro up to 168px. The prompt
892
934
  * still guides the result.
893
935
  */
894
936
  reference: import_zod.z.union([import_zod.z.string().min(1), import_zod.z.record(CharacterDirectionSchema, import_zod.z.string().min(1))]).optional(),
@@ -1370,13 +1412,36 @@ var TilesetGetSchema = import_zod2.z.object({
1370
1412
  }).passthrough(),
1371
1413
  usage: UsageSchema
1372
1414
  }).passthrough();
1415
+ var IsometricTileSubmitSchema = import_zod2.z.object({
1416
+ tile_id: import_zod2.z.string().min(1),
1417
+ background_job_id: import_zod2.z.string().min(1),
1418
+ status: import_zod2.z.literal("processing").default("processing")
1419
+ }).passthrough();
1420
+ var IsometricTileGetSchema = import_zod2.z.object({
1421
+ image: import_zod2.z.object({ base64: import_zod2.z.string().min(1), format: import_zod2.z.string().default("png") }).passthrough(),
1422
+ usage: UsageSchema
1423
+ }).passthrough();
1373
1424
  var RevisionJobSubmitSchema = import_zod2.z.object({
1374
1425
  background_job_id: import_zod2.z.string().min(1),
1375
1426
  status: import_zod2.z.string().default("processing")
1376
1427
  }).passthrough();
1428
+ var ObjectAnimationGroupSchema = import_zod2.z.object({
1429
+ animation_group_id: import_zod2.z.string().min(1),
1430
+ display_name: import_zod2.z.string().nullable().default(null),
1431
+ description: import_zod2.z.string().default(""),
1432
+ frame_count: import_zod2.z.number().int().default(0),
1433
+ directions: import_zod2.z.array(
1434
+ import_zod2.z.object({
1435
+ direction: import_zod2.z.string(),
1436
+ storage_urls: import_zod2.z.record(import_zod2.z.unknown()).default({}),
1437
+ created_at: import_zod2.z.string()
1438
+ }).passthrough()
1439
+ ).default([])
1440
+ }).passthrough();
1377
1441
  var PixelLabObjectSchema = import_zod2.z.object({
1378
1442
  id: import_zod2.z.string().min(1),
1379
1443
  name: import_zod2.z.string().nullable().default(null),
1444
+ state_name: import_zod2.z.string().nullable().optional(),
1380
1445
  prompt: import_zod2.z.string().default(""),
1381
1446
  size: import_zod2.z.object({ width: import_zod2.z.number().int().positive(), height: import_zod2.z.number().int().positive() }),
1382
1447
  directions: import_zod2.z.number().default(0),
@@ -1388,7 +1453,9 @@ var PixelLabObjectSchema = import_zod2.z.object({
1388
1453
  tags: import_zod2.z.array(import_zod2.z.string()).default([]),
1389
1454
  status: import_zod2.z.string().nullable().default(null),
1390
1455
  progress_percent: import_zod2.z.number().nullable().optional(),
1391
- eta_seconds: import_zod2.z.number().nullable().optional()
1456
+ eta_seconds: import_zod2.z.number().nullable().optional(),
1457
+ group_id: import_zod2.z.string().nullable().optional(),
1458
+ animations: import_zod2.z.array(ObjectAnimationGroupSchema).default([])
1392
1459
  }).passthrough();
1393
1460
  var MapObjectSchema = import_zod2.z.object({
1394
1461
  object_id: import_zod2.z.string().min(1),
@@ -1413,6 +1480,28 @@ var AnimateSubmitSchema = import_zod2.z.object({
1413
1480
  status: import_zod2.z.string().default("processing"),
1414
1481
  usage: UsageSchema
1415
1482
  }).passthrough();
1483
+ var ObjectProSubmitSchema = import_zod2.z.object({
1484
+ object_id: import_zod2.z.string().min(1),
1485
+ background_job_id: import_zod2.z.string().min(1),
1486
+ status: import_zod2.z.string().default("processing"),
1487
+ usage: UsageSchema
1488
+ }).passthrough();
1489
+ var DirectionSubmissionSchema = import_zod2.z.object({
1490
+ direction: import_zod2.z.string(),
1491
+ status: import_zod2.z.string(),
1492
+ background_job_id: import_zod2.z.string().nullable().default(null),
1493
+ animation_id: import_zod2.z.string().nullable().default(null)
1494
+ }).passthrough();
1495
+ var AnimateObjectSubmitSchema = import_zod2.z.object({
1496
+ animation_group_id: import_zod2.z.string().min(1),
1497
+ object_id: import_zod2.z.string().min(1),
1498
+ mode: import_zod2.z.string(),
1499
+ frame_count: import_zod2.z.number().int(),
1500
+ display_name: import_zod2.z.string().nullable().default(null),
1501
+ description: import_zod2.z.string().default(""),
1502
+ submissions: import_zod2.z.array(DirectionSubmissionSchema),
1503
+ usage: UsageSchema
1504
+ }).passthrough();
1416
1505
  var CharacterAnimationDirectionSchema = import_zod2.z.object({
1417
1506
  direction: import_zod2.z.string(),
1418
1507
  frame_count: import_zod2.z.number().int().min(0),
@@ -1655,6 +1744,33 @@ var PixelLabClient = class {
1655
1744
  );
1656
1745
  return { tileset: res.tileset, usage: res.usage ?? null };
1657
1746
  }
1747
+ /** A single standalone isometric tile — no connectable set, no candidates. */
1748
+ async createIsometricTile(args) {
1749
+ const body = { description: args.description };
1750
+ if (args.imageWidth != null && args.imageHeight != null) {
1751
+ body.image_size = { width: args.imageWidth, height: args.imageHeight };
1752
+ }
1753
+ if (args.tileSize != null) body.isometric_tile_size = args.tileSize;
1754
+ if (args.tileShape) body.isometric_tile_shape = args.tileShape;
1755
+ if (args.outline) body.outline = args.outline;
1756
+ if (args.shading) body.shading = args.shading;
1757
+ if (args.detail) body.detail = args.detail;
1758
+ if (args.seed != null) body.seed = args.seed;
1759
+ return validateResponse(
1760
+ IsometricTileSubmitSchema,
1761
+ await this.request("/create-isometric-tile", { method: "POST", body: JSON.stringify(body) }),
1762
+ "create isometric tile"
1763
+ );
1764
+ }
1765
+ /** Throws PixelLabError(423) while the tile is still processing. */
1766
+ async getIsometricTile(tileId) {
1767
+ const res = await validateResponse(
1768
+ IsometricTileGetSchema,
1769
+ await this.request(`/isometric-tiles/${tileId}`),
1770
+ "get isometric tile"
1771
+ );
1772
+ return { image: res.image, usage: res.usage ?? null };
1773
+ }
1658
1774
  /**
1659
1775
  * Synchronous single-image generation. Returns the PNG inline rather than a
1660
1776
  * job id, and is the only endpoint that honours a forced palette;
@@ -1688,6 +1804,81 @@ var PixelLabClient = class {
1688
1804
  "get object"
1689
1805
  );
1690
1806
  }
1807
+ /**
1808
+ * A standalone object with no skeleton, one call draws the south sprite
1809
+ * (or takes the author's) and rotates it with v3 — the character
1810
+ * pro-flash pattern, minus `template_id` (objects have no skeleton) and
1811
+ * with a real `n_directions` choice (1 or 8, character pro-flash is
1812
+ * always 8). Async only: `/create-object-pro-flash` has no sync response.
1813
+ */
1814
+ async createObjectProFlash(args) {
1815
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1816
+ const traits = args.styleTraits;
1817
+ const body = {
1818
+ description: args.description,
1819
+ n_directions: args.directions,
1820
+ ...args.reference ? { first_frame: encode(args.reference) } : { image_size: { width: args.size, height: args.size } },
1821
+ ...args.view ? { view: args.view } : {},
1822
+ ...args.seed != null ? { seed: args.seed } : {},
1823
+ ...args.styleReference && args.styleReferenceSize ? {
1824
+ style_image: { image: encode(args.styleReference), size: args.styleReferenceSize },
1825
+ ...traits ? {
1826
+ style_options: {
1827
+ ...traits.palette !== void 0 ? { color_palette: traits.palette } : {},
1828
+ ...traits.outline !== void 0 ? { outline: traits.outline } : {},
1829
+ ...traits.detail !== void 0 ? { detail: traits.detail } : {},
1830
+ ...traits.shading !== void 0 ? { shading: traits.shading } : {}
1831
+ }
1832
+ } : {}
1833
+ } : {}
1834
+ };
1835
+ return validateResponse(
1836
+ ObjectProSubmitSchema,
1837
+ await this.request("/create-object-pro-flash", { method: "POST", body: JSON.stringify(body) }),
1838
+ "create-object-pro-flash"
1839
+ );
1840
+ }
1841
+ /** 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. */
1842
+ async createObjectProState(args) {
1843
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/states`, {
1844
+ method: "POST",
1845
+ body: JSON.stringify({
1846
+ edit_description: args.editDescription,
1847
+ ...args.stateName ? { state_name: args.stateName } : {},
1848
+ ...args.seed != null ? { seed: args.seed } : {}
1849
+ })
1850
+ });
1851
+ return validateResponse(ObjectProSubmitSchema, raw, "create object state");
1852
+ }
1853
+ /**
1854
+ * One direction of one object's animation. Unlike character animation,
1855
+ * this endpoint's own `replace_existing` flag regenerates an
1856
+ * already-animated direction directly (character has no such flag and
1857
+ * pixelkiln instead deletes the prior take itself before resubmitting;
1858
+ * see `submitCharacter`). A 1-direction object must not receive
1859
+ * `directions` at all -- the API 400s if it does.
1860
+ */
1861
+ async animateObject(args) {
1862
+ const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
1863
+ const body = {
1864
+ mode: args.mode,
1865
+ ...args.directions === 8 ? { directions: [args.direction] } : {},
1866
+ ...args.animationDescription ? { animation_description: args.animationDescription } : {},
1867
+ ...args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
1868
+ ...args.displayName ? { display_name: args.displayName } : {},
1869
+ ...args.frameCount != null ? { frame_count: args.frameCount } : {},
1870
+ ...args.keepFirstFrame === false ? { keep_first_frame: false } : {},
1871
+ ...args.startFrame ? { custom_start_frame: encode(args.startFrame) } : {},
1872
+ ...args.endFrame ? { end_frame: encode(args.endFrame) } : {},
1873
+ ...args.enhancePrompt ? { enhance_prompt: true } : {},
1874
+ ...args.replaceExisting ? { replace_existing: true } : {}
1875
+ };
1876
+ const raw = await this.request(`/objects/${encodeURIComponent(args.objectId)}/animations`, {
1877
+ method: "POST",
1878
+ body: JSON.stringify(body)
1879
+ });
1880
+ return validateResponse(AnimateObjectSubmitSchema, raw, "animate object");
1881
+ }
1691
1882
  async getMapObject(objectId) {
1692
1883
  return validateResponse(
1693
1884
  MapObjectSchema,
@@ -2633,6 +2824,8 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
2633
2824
  tileView: spec.tileView,
2634
2825
  tileFeature: spec.tileFeature,
2635
2826
  outlineMode: spec.outlineMode,
2827
+ isometricTileSize: spec.isometricTileSize,
2828
+ isometricTileShape: spec.isometricTileShape,
2636
2829
  styleImages: styleImageHashes,
2637
2830
  // A character family: the engine and rotations of a base, the parent's
2638
2831
  // generated bytes for a state or animation (so a regenerated parent
@@ -2661,6 +2854,23 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
2661
2854
  concept: spec.character.concept?.sha256,
2662
2855
  styleAnchor: spec.character.styleAnchor ? { of: spec.character.styleAnchor.assetId, sha256: spec.character.styleAnchor.sha256 } : void 0
2663
2856
  } : void 0,
2857
+ // An objectPro family: same shape as character above, minus the
2858
+ // fields it has none of (mode/template/proportions/isometric/concept/
2859
+ // styleAnchor) — see ResolvedObjectPro.
2860
+ objectPro: spec.objectPro ? {
2861
+ kind: spec.objectPro.kind,
2862
+ directions: spec.objectPro.directions,
2863
+ parent: spec.objectPro.parentAssetId,
2864
+ parentSha256: spec.objectPro.parentSha256 ?? null,
2865
+ state: spec.objectPro.state,
2866
+ animation: spec.objectPro.animation ? {
2867
+ ...spec.objectPro.animation,
2868
+ startFrame: spec.objectPro.animation.startFrame?.sha256,
2869
+ endFrame: spec.objectPro.animation.endFrame?.sha256
2870
+ } : void 0,
2871
+ styleTraits: spec.objectPro.styleTraits,
2872
+ reference: spec.objectPro.reference?.sha256
2873
+ } : void 0,
2664
2874
  // A mirror's bytes come from its source's recorded outputs; the plan
2665
2875
  // compares those directly, so only the choice of source is identity.
2666
2876
  mirror: spec.mirror ? { of: spec.mirror.sourceAssetId } : void 0,
@@ -2728,6 +2938,29 @@ function proFlashCharacterCost(width, height, fromReference) {
2728
2938
  function characterAnimationName(spec) {
2729
2939
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
2730
2940
  }
2941
+ function objectAnimationName(spec) {
2942
+ return `pixelkiln:${spec.styleId}/${spec.assetId}`;
2943
+ }
2944
+ function proFlashObjectCost(width, height, directions, fromReference) {
2945
+ const side = Math.max(32, Math.ceil(Math.max(width, height) / 4) * 4);
2946
+ const rotations = Math.max(1, Math.ceil(side * side * directions / 65536));
2947
+ if (fromReference) return rotations;
2948
+ const image = side <= 96 ? 5 : side <= 208 ? 6 : 9;
2949
+ return image + rotations;
2950
+ }
2951
+ function objectProCost(spec) {
2952
+ const object = spec.objectPro;
2953
+ if (!object) return generationCost(spec.width, spec.height, "1dir");
2954
+ const px = spec.width * spec.height;
2955
+ if (object.kind === "animation") {
2956
+ const animation = object.animation;
2957
+ if (animation.mode === "pro") return generationCost(spec.width, spec.height, "1dir");
2958
+ return Math.max(1, Math.ceil(px * animation.frames / 65536));
2959
+ }
2960
+ if (object.kind === "state") return generationCost(spec.width, spec.height, "1dir");
2961
+ const south = object.reference;
2962
+ return south ? proFlashObjectCost(south.width, south.height, object.directions, true) : proFlashObjectCost(spec.width, spec.height, object.directions, false);
2963
+ }
2731
2964
  var CHARACTER_TEMPLATES = ["mannequin", "bear", "cat", "dog", "horse", "lion"];
2732
2965
  var PRO_FLASH_TEMPLATES = [...CHARACTER_TEMPLATES, "custom"];
2733
2966
  function encodeAnimationJob(job) {
@@ -2744,6 +2977,20 @@ function decodeAnimationJob(jobId) {
2744
2977
  }
2745
2978
  return null;
2746
2979
  }
2980
+ function encodeObjectAnimationJob(job) {
2981
+ return `object-animation:${Buffer.from(JSON.stringify(job)).toString("base64url")}`;
2982
+ }
2983
+ function decodeObjectAnimationJob(jobId) {
2984
+ if (!jobId.startsWith("object-animation:")) return null;
2985
+ try {
2986
+ const value = JSON.parse(Buffer.from(jobId.slice("object-animation:".length), "base64url").toString("utf8"));
2987
+ if (typeof value?.objectId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
2988
+ return { objectId: value.objectId, name: value.name, direction: value.direction, jobId: typeof value.jobId === "string" ? value.jobId : null };
2989
+ }
2990
+ } catch {
2991
+ }
2992
+ return null;
2993
+ }
2747
2994
  var PixelLabProvider = class _PixelLabProvider {
2748
2995
  constructor(client2) {
2749
2996
  this.client = client2;
@@ -2762,7 +3009,7 @@ var PixelLabProvider = class _PixelLabProvider {
2762
3009
  return new _PixelLabProvider(new PixelLabClient("download-only"));
2763
3010
  }
2764
3011
  supports(generator) {
2765
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character";
3012
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "imagePro" || generator === "character" || generator === "isometricTile" || generator === "objectPro";
2766
3013
  }
2767
3014
  /**
2768
3015
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -2798,12 +3045,15 @@ var PixelLabProvider = class _PixelLabProvider {
2798
3045
  const height = spec.revision.sourceHeight ?? spec.height;
2799
3046
  return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
2800
3047
  }
2801
- if (spec.generator === "tiles" || spec.generator === "terrain") {
3048
+ if (spec.generator === "tiles" || spec.generator === "terrain" || spec.generator === "isometricTile") {
2802
3049
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
2803
3050
  }
2804
3051
  if (spec.generator === "character") {
2805
3052
  return { unit: "generations", amount: characterCost(spec), candidates: 1 };
2806
3053
  }
3054
+ if (spec.generator === "objectPro") {
3055
+ return { unit: "generations", amount: objectProCost(spec), candidates: 1 };
3056
+ }
2807
3057
  return {
2808
3058
  unit: "generations",
2809
3059
  amount: generationCost(spec.width, spec.height, spec.generator),
@@ -2829,19 +3079,19 @@ var PixelLabProvider = class _PixelLabProvider {
2829
3079
  );
2830
3080
  }
2831
3081
  if (spec.generator === "map") {
2832
- requirePixelLabOption("view", spec.view, ["low top-down", "high top-down", "side"]);
2833
- requirePixelLabOption("outline", spec.outline, [
3082
+ requirePixelLabOption("map", "view", spec.view, ["low top-down", "high top-down", "side"]);
3083
+ requirePixelLabOption("map", "outline", spec.outline, [
2834
3084
  "single color outline",
2835
3085
  "selective outline",
2836
3086
  "lineless"
2837
3087
  ]);
2838
- requirePixelLabOption("shading", spec.shading, [
3088
+ requirePixelLabOption("map", "shading", spec.shading, [
2839
3089
  "flat shading",
2840
3090
  "basic shading",
2841
3091
  "medium shading",
2842
3092
  "detailed shading"
2843
3093
  ]);
2844
- requirePixelLabOption("detail", spec.detail, [
3094
+ requirePixelLabOption("map", "detail", spec.detail, [
2845
3095
  "low detail",
2846
3096
  "medium detail",
2847
3097
  "high detail"
@@ -2849,7 +3099,7 @@ var PixelLabProvider = class _PixelLabProvider {
2849
3099
  }
2850
3100
  if (spec.generator === "terrain") {
2851
3101
  if (spec.outline) {
2852
- requirePixelLabOption("outline", spec.outline, [
3102
+ requirePixelLabOption("terrain", "outline", spec.outline, [
2853
3103
  "single color black outline",
2854
3104
  "single color outline",
2855
3105
  "selective outline",
@@ -2857,7 +3107,7 @@ var PixelLabProvider = class _PixelLabProvider {
2857
3107
  ]);
2858
3108
  }
2859
3109
  if (spec.shading) {
2860
- requirePixelLabOption("shading", spec.shading, [
3110
+ requirePixelLabOption("terrain", "shading", spec.shading, [
2861
3111
  "flat shading",
2862
3112
  "basic shading",
2863
3113
  "medium shading",
@@ -2866,7 +3116,7 @@ var PixelLabProvider = class _PixelLabProvider {
2866
3116
  ]);
2867
3117
  }
2868
3118
  if (spec.detail) {
2869
- requirePixelLabOption("detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
3119
+ requirePixelLabOption("terrain", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
2870
3120
  }
2871
3121
  if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
2872
3122
  throw new Error(
@@ -2887,7 +3137,32 @@ var PixelLabProvider = class _PixelLabProvider {
2887
3137
  );
2888
3138
  }
2889
3139
  }
3140
+ if (spec.generator === "isometricTile") {
3141
+ if (spec.outline) {
3142
+ requirePixelLabOption("isometricTile", "outline", spec.outline, [
3143
+ "single color outline",
3144
+ "selective outline",
3145
+ "lineless"
3146
+ ]);
3147
+ }
3148
+ if (spec.shading) {
3149
+ requirePixelLabOption("isometricTile", "shading", spec.shading, [
3150
+ "flat shading",
3151
+ "basic shading",
3152
+ "medium shading",
3153
+ "detailed shading",
3154
+ "highly detailed shading"
3155
+ ]);
3156
+ }
3157
+ if (spec.detail) {
3158
+ requirePixelLabOption("isometricTile", "detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
3159
+ }
3160
+ if (spec.width < 16 || spec.width > 64) {
3161
+ throw new Error(`PixelLab isometricTile: size must be 16 to 64 pixels, got ${spec.width}`);
3162
+ }
3163
+ }
2890
3164
  if (spec.generator === "character") this.validateCharacter(spec, styleImages);
3165
+ if (spec.generator === "objectPro") this.validateObjectPro(spec, styleImages);
2891
3166
  if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
2892
3167
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
2893
3168
  }
@@ -2901,6 +3176,11 @@ var PixelLabProvider = class _PixelLabProvider {
2901
3176
  "PixelLab imagePro does not support style images yet; /generate-image-v2's own reference_images and style_image are not modeled here"
2902
3177
  );
2903
3178
  }
3179
+ if (spec.generator === "isometricTile" && styleImages.length) {
3180
+ throw new Error(
3181
+ "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)"
3182
+ );
3183
+ }
2904
3184
  for (const image of styleImages) {
2905
3185
  if (image.width > 256 || image.height > 256) {
2906
3186
  throw new Error(
@@ -3043,6 +3323,63 @@ var PixelLabProvider = class _PixelLabProvider {
3043
3323
  }
3044
3324
  }
3045
3325
  }
3326
+ validateObjectPro(spec, styleImages = []) {
3327
+ const object = spec.objectPro;
3328
+ if (!object) throw new Error(`${spec.styleId}/${spec.assetId} has no objectPro shape`);
3329
+ const label = `${spec.styleId}/${spec.assetId}`;
3330
+ if (object.kind === "base" && object.reference && styleImages.length) {
3331
+ throw new Error(`${label}: a base rotating its own reference sprite takes no style image`);
3332
+ }
3333
+ if (object.reference && (object.reference.width > 256 || object.reference.height > 256)) {
3334
+ throw new Error(`${label}: reference is ${object.reference.width}x${object.reference.height}; PixelLab objectPro takes up to 256px`);
3335
+ }
3336
+ if (styleImages.length) {
3337
+ if (styleImages.length > 1) throw new Error(`PixelLab objectPro takes one style image`);
3338
+ const image = styleImages[0];
3339
+ if (image.width > 256 || image.height > 256) {
3340
+ throw new Error(`PixelLab objectPro style image is ${image.width}x${image.height}; the limit is 256px`);
3341
+ }
3342
+ if (image.width > spec.width || image.height > spec.height) {
3343
+ throw new Error(
3344
+ `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`
3345
+ );
3346
+ }
3347
+ }
3348
+ if (object.kind === "base" && (spec.width < 16 || spec.height < 16 || spec.width > 256 || spec.height > 256)) {
3349
+ throw new Error(`PixelLab objectPro bases must be between 16 and 256 pixels`);
3350
+ }
3351
+ if (object.kind === "base" && (spec.width % 4 || spec.height % 4)) {
3352
+ throw new Error(`PixelLab objectPro bases are sized in multiples of 4 pixels; ${spec.width}x${spec.height} is not`);
3353
+ }
3354
+ if (object.kind !== "base" && !object.parentAssetId) {
3355
+ throw new Error(`${spec.styleId}/${spec.assetId} is a ${object.kind} with no parent`);
3356
+ }
3357
+ if (object.animation) {
3358
+ const allowed = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
3359
+ if (!allowed.includes(object.animation.direction)) {
3360
+ throw new Error(
3361
+ `${label} animates "${object.animation.direction}", but its object has ${object.directions} direction${object.directions === 1 ? "" : "s"}: ${allowed.join(", ")}`
3362
+ );
3363
+ }
3364
+ if (!spec.prompt.trim()) {
3365
+ throw new Error(`${label} needs a prompt describing the motion`);
3366
+ }
3367
+ const { startFrame, endFrame } = object.animation;
3368
+ for (const [name, image] of [["start frame", startFrame], ["end frame", endFrame]]) {
3369
+ if (image && (image.width > 256 || image.height > 256)) {
3370
+ throw new Error(`${label}: ${name} is ${image.width}x${image.height}; PixelLab v3 takes up to 256px`);
3371
+ }
3372
+ }
3373
+ if (endFrame) {
3374
+ const start = startFrame ?? (object.parentFile && (0, import_node_fs2.existsSync)(object.parentFile) ? imageMetadata((0, import_node_fs2.readFileSync)(object.parentFile)) : null);
3375
+ if (start && (start.width !== endFrame.width || start.height !== endFrame.height)) {
3376
+ throw new Error(
3377
+ `${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`
3378
+ );
3379
+ }
3380
+ }
3381
+ }
3382
+ }
3046
3383
  /**
3047
3384
  * A base is one request that answers with the character id; a state is
3048
3385
  * the same with the parent's id; an animation names itself after the
@@ -3136,6 +3473,78 @@ var PixelLabProvider = class _PixelLabProvider {
3136
3473
  metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
3137
3474
  };
3138
3475
  }
3476
+ /**
3477
+ * Same shape as `submitCharacter`, minus the mode branching (objectPro has
3478
+ * exactly one base-creation path) and minus the client-side "find and
3479
+ * delete the prior take" dance for animation: `/objects/{id}/animations`
3480
+ * has its own `replace_existing` flag, so this always passes it rather
3481
+ * than fetching the object first to check.
3482
+ */
3483
+ async submitObjectPro(spec, styleImages, context) {
3484
+ const object = spec.objectPro;
3485
+ if (object.kind === "base") {
3486
+ const styleImage = styleImages[0];
3487
+ const res2 = await this.client.createObjectProFlash({
3488
+ description: spec.prompt,
3489
+ directions: object.directions,
3490
+ size: spec.width,
3491
+ view: spec.view,
3492
+ seed: spec.seed,
3493
+ reference: object.reference ? readPose(spec, "reference sprite", object.reference) : void 0,
3494
+ styleReference: styleImage ? { base64: styleImage.base64, format: styleImage.format } : void 0,
3495
+ styleReferenceSize: styleImage ? { width: styleImage.width, height: styleImage.height } : void 0,
3496
+ styleTraits: object.styleTraits
3497
+ });
3498
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "base", objectId: res2.object_id, directions: object.directions, backgroundJobId: res2.background_job_id } } };
3499
+ }
3500
+ const parentId = context?.parentObjectId;
3501
+ if (!parentId) {
3502
+ throw new Error(
3503
+ `${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`
3504
+ );
3505
+ }
3506
+ if (object.kind === "state") {
3507
+ const res2 = await this.client.createObjectProState({
3508
+ objectId: parentId,
3509
+ editDescription: spec.prompt,
3510
+ stateName: spec.assetId.slice(0, 50),
3511
+ seed: spec.seed
3512
+ });
3513
+ return { jobId: res2.object_id, metadata: { objectPro: { kind: "state", objectId: res2.object_id, parentObjectId: parentId, directions: object.directions, backgroundJobId: res2.background_job_id } } };
3514
+ }
3515
+ const animation = object.animation;
3516
+ const name = objectAnimationName(spec);
3517
+ const res = await this.client.animateObject({
3518
+ objectId: parentId,
3519
+ directions: object.directions,
3520
+ direction: animation.direction,
3521
+ animationDescription: spec.prompt.trim() || void 0,
3522
+ displayName: name,
3523
+ mode: animation.mode === "pro" ? "pro" : "v3",
3524
+ frameCount: animation.frames,
3525
+ keepFirstFrame: animation.keepFirstFrame,
3526
+ startFrame: animation.startFrame ? readPose(spec, "start frame", animation.startFrame) : void 0,
3527
+ endFrame: animation.endFrame ? readPose(spec, "end frame", animation.endFrame) : void 0,
3528
+ enhancePrompt: animation.enhancePrompt,
3529
+ replaceExisting: true
3530
+ });
3531
+ const submission = res.submissions.find((s) => s.direction === animation.direction) ?? res.submissions[0];
3532
+ const job = { objectId: parentId, name, direction: animation.direction, jobId: submission?.background_job_id ?? null };
3533
+ return {
3534
+ jobId: encodeObjectAnimationJob(job),
3535
+ metadata: {
3536
+ objectPro: {
3537
+ kind: "animation",
3538
+ objectId: parentId,
3539
+ animationName: name,
3540
+ direction: animation.direction,
3541
+ mode: animation.mode,
3542
+ animationGroupId: res.animation_group_id,
3543
+ animationId: submission?.animation_id ?? null
3544
+ }
3545
+ }
3546
+ };
3547
+ }
3139
3548
  async pollCharacter(jobId, context) {
3140
3549
  const animation = decodeAnimationJob(jobId);
3141
3550
  if (animation) return this.pollCharacterAnimation(animation, context);
@@ -3219,10 +3628,93 @@ var PixelLabProvider = class _PixelLabProvider {
3219
3628
  if (found) return review(found.frames, found.animationId, found.groupId, null);
3220
3629
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
3221
3630
  }
3631
+ async pollObjectPro(jobId, context) {
3632
+ const animation = decodeObjectAnimationJob(jobId);
3633
+ if (animation) return this.pollObjectProAnimation(animation, context);
3634
+ const object = await this.client.getObject(jobId);
3635
+ if (object.status === "failed") return { status: "failed", error: "object generation failed upstream" };
3636
+ if (object.status !== "completed" || !object.rotation_urls) return { status: "processing" };
3637
+ const sources = objectRotationSources(object);
3638
+ if (!sources.length) return { status: "failed", error: "object completed with no rotation images" };
3639
+ const backgroundJobId = context?.metadata?.objectPro?.backgroundJobId;
3640
+ return {
3641
+ status: "ready",
3642
+ objectId: object.id,
3643
+ sourceUrl: sources[0].url,
3644
+ sources,
3645
+ metadata: {
3646
+ objectPro: {
3647
+ objectId: object.id,
3648
+ groupId: object.group_id ?? null,
3649
+ name: object.name,
3650
+ stateName: object.state_name ?? null,
3651
+ directions: object.directions,
3652
+ size: object.size
3653
+ }
3654
+ },
3655
+ billed: await this.billedForJob(backgroundJobId)
3656
+ };
3657
+ }
3658
+ /**
3659
+ * Same resilience shape as `pollCharacterAnimation`: the background job is
3660
+ * the authoritative record while it lasts, the object's own `animations`
3661
+ * list (matched by the `pixelkiln:` display name this asset submitted, or
3662
+ * a recorded animation id) is the fallback once PixelLab cleans the job
3663
+ * up. Never declares failure from absence alone -- a `rate_limited`
3664
+ * `DirectionSubmission` has no background job id at all, so this keeps
3665
+ * reporting "processing" rather than guessing a job that never existed
3666
+ * has failed.
3667
+ */
3668
+ async pollObjectProAnimation(job, context) {
3669
+ const fps = context?.spec?.objectPro?.animation?.fps ?? 8;
3670
+ const review = (frames, animationId, groupId, billed) => ({
3671
+ status: "review-set",
3672
+ objectId: `${job.objectId}#${groupId ?? animationId ?? job.name}`,
3673
+ frameUrls: frames,
3674
+ sources: frames.map((url, index) => ({ url, role: `frame-${String(index).padStart(2, "0")}` })),
3675
+ fps,
3676
+ metadata: {
3677
+ frameSet: { fps, count: frames.length },
3678
+ objectPro: {
3679
+ kind: "animation",
3680
+ objectId: job.objectId,
3681
+ animationId,
3682
+ animationGroupId: groupId,
3683
+ animationName: job.name,
3684
+ direction: job.direction
3685
+ }
3686
+ },
3687
+ billed
3688
+ });
3689
+ if (job.jobId) {
3690
+ const status = await this.client.getBackgroundJob(job.jobId).catch((err) => {
3691
+ if (err instanceof PixelLabError && err.status === 404) return null;
3692
+ throw err;
3693
+ });
3694
+ if (status) {
3695
+ if (status.status === "failed") return { status: "failed", error: "animation generation failed upstream" };
3696
+ if (status.status !== "completed") return { status: "processing" };
3697
+ const done = status.last_response ?? {};
3698
+ const frames = done.storage_urls?.frames;
3699
+ const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
3700
+ if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
3701
+ const object2 = await this.client.getObject(job.objectId);
3702
+ const group = findObjectAnimation(object2, job.name, job.direction, animationId);
3703
+ return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
3704
+ }
3705
+ }
3706
+ }
3707
+ const recorded = context?.metadata?.objectPro?.animationId ?? null;
3708
+ const object = await this.client.getObject(job.objectId);
3709
+ const found = findObjectAnimation(object, job.name, job.direction, recorded);
3710
+ if (found) return review(found.frames, found.animationId, found.groupId, null);
3711
+ return { status: "processing" };
3712
+ }
3222
3713
  async submit(spec, styleImages, context) {
3223
3714
  this.validate(spec, styleImages);
3224
3715
  if (spec.revision) return this.submitRevision(spec);
3225
3716
  if (spec.generator === "character") return this.submitCharacter(spec, styleImages, context);
3717
+ if (spec.generator === "objectPro") return this.submitObjectPro(spec, styleImages, context);
3226
3718
  if (spec.generator === "pixflux") {
3227
3719
  const swatch = spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0;
3228
3720
  const { png } = await this.client.createImagePixflux({
@@ -3283,6 +3775,20 @@ var PixelLabProvider = class _PixelLabProvider {
3283
3775
  });
3284
3776
  return { jobId: res2.tileset_id, metadata: { backgroundJobId: res2.background_job_id } };
3285
3777
  }
3778
+ if (spec.generator === "isometricTile") {
3779
+ const res2 = await this.client.createIsometricTile({
3780
+ description: spec.prompt,
3781
+ imageWidth: spec.width,
3782
+ imageHeight: spec.height,
3783
+ tileSize: spec.isometricTileSize,
3784
+ tileShape: spec.isometricTileShape,
3785
+ outline: spec.outline,
3786
+ shading: spec.shading,
3787
+ detail: spec.detail,
3788
+ seed: spec.seed
3789
+ });
3790
+ return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
3791
+ }
3286
3792
  if (spec.generator === "imagePro") {
3287
3793
  const res2 = await this.client.createImagePro({
3288
3794
  description: spec.prompt,
@@ -3399,8 +3905,10 @@ var PixelLabProvider = class _PixelLabProvider {
3399
3905
  if (generator === "map") return this.pollMap(jobId, context);
3400
3906
  if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
3401
3907
  if (generator === "terrain") return this.pollTerrain(jobId, context);
3908
+ if (generator === "isometricTile") return this.pollIsometricTile(jobId, context);
3402
3909
  if (generator === "imagePro") return this.pollImagePro(jobId);
3403
3910
  if (generator === "character") return this.pollCharacter(jobId, context);
3911
+ if (generator === "objectPro") return this.pollObjectPro(jobId, context);
3404
3912
  const backgroundJobId = context?.metadata?.backgroundJobId;
3405
3913
  const obj = await this.client.getObject(jobId);
3406
3914
  if (obj.status === "review") {
@@ -3609,6 +4117,31 @@ var PixelLabProvider = class _PixelLabProvider {
3609
4117
  throw err;
3610
4118
  }
3611
4119
  }
4120
+ /**
4121
+ * `/create-isometric-tile` reports progress the same way `/create-tileset`
4122
+ * does (423 while drawing, 200 once finished, no `status` field to poll),
4123
+ * and its image also comes back embedded as base64 rather than a
4124
+ * `storage_urls` link. One call is one tile with nothing to review, so
4125
+ * this goes straight to "ready" like a connectable `tiles`/`terrain` set.
4126
+ */
4127
+ async pollIsometricTile(tileId, context) {
4128
+ try {
4129
+ const { image, usage } = await this.client.getIsometricTile(tileId);
4130
+ const backgroundJobId = context?.metadata?.backgroundJobId;
4131
+ const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
4132
+ const file = import_node_path.default.join(_PixelLabProvider.cacheDir(), `${tileId}.png`);
4133
+ (0, import_node_fs2.writeFileSync)(file, Buffer.from(image.base64, "base64"));
4134
+ return {
4135
+ status: "ready",
4136
+ objectId: tileId,
4137
+ sourceUrl: `file://${file}`,
4138
+ billed
4139
+ };
4140
+ } catch (err) {
4141
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
4142
+ throw err;
4143
+ }
4144
+ }
3612
4145
  async selectCandidate(jobId, index, commonTag, generator) {
3613
4146
  if (generator === "tiles") {
3614
4147
  const set = await this.client.getTilesPro(jobId);
@@ -3725,9 +4258,9 @@ function remoteCharacter(character) {
3725
4258
  animationCount: character.animation_count
3726
4259
  };
3727
4260
  }
3728
- function requirePixelLabOption(name, value, allowed) {
4261
+ function requirePixelLabOption(generator, name, value, allowed) {
3729
4262
  if (value != null && !allowed.includes(value)) {
3730
- throw new Error(`PixelLab map ${name} must be one of: ${allowed.join(", ")}`);
4263
+ throw new Error(`PixelLab ${generator} ${name} must be one of: ${allowed.join(", ")}`);
3731
4264
  }
3732
4265
  }
3733
4266
  function tilesInIndexOrder(urls) {
@@ -3793,6 +4326,28 @@ function findAnimation(character, name, direction, animationId) {
3793
4326
  }
3794
4327
  return null;
3795
4328
  }
4329
+ function objectRotationSources(object) {
4330
+ const order = object.directions === 1 ? ["south"] : CHARACTER_DIRECTIONS_8;
4331
+ const sources = [];
4332
+ for (const direction of order) {
4333
+ const url = object.rotation_urls?.[direction];
4334
+ if (url) sources.push({ url, role: direction });
4335
+ }
4336
+ return sources;
4337
+ }
4338
+ function findObjectAnimation(object, name, direction, animationId) {
4339
+ for (const group of object.animations) {
4340
+ const match = group.directions.find((d) => d.direction === direction);
4341
+ const frames = match ? match.storage_urls.frames : void 0;
4342
+ if (!match || !Array.isArray(frames) || !frames.length || !frames.every((f) => typeof f === "string")) continue;
4343
+ const byName = group.display_name === name;
4344
+ const byId = animationId !== null && frames.some((url) => url.includes(`/animations/${animationId}/`));
4345
+ if (!byName && !byId) continue;
4346
+ const fromUrl = /\/animations\/([^/]+)\//.exec(frames[0])?.[1] ?? null;
4347
+ return { frames, groupId: group.animation_group_id, animationId: animationId ?? fromUrl };
4348
+ }
4349
+ return null;
4350
+ }
3796
4351
 
3797
4352
  // src/providers/retrodiffusion.ts
3798
4353
  var DEFAULT_BASE_URL = "https://api.retrodiffusion.ai/v1";
@@ -6346,14 +6901,22 @@ async function resolveSpecs(loaded, filter) {
6346
6901
  size = style.terrainTileSize ?? 16;
6347
6902
  width = size;
6348
6903
  height = size;
6904
+ } else if (generator === "isometricTile") {
6905
+ size = asset.size ?? style.size ?? 32;
6906
+ width = size;
6907
+ height = size;
6908
+ } else if (generator === "objectPro") {
6909
+ size = asset.size ?? style.size ?? 64;
6910
+ width = size;
6911
+ height = size;
6349
6912
  } else {
6350
6913
  width = asset.width ?? style.size ?? 64;
6351
6914
  height = asset.height ?? style.size ?? 64;
6352
6915
  size = Math.max(width, height);
6353
6916
  }
6354
- if ((asset.state || asset.animation) && generator !== "character") {
6917
+ if ((asset.state || asset.animation) && generator !== "character" && generator !== "objectPro") {
6355
6918
  throw new Error(
6356
- `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character style; "${styleId}" generates ${generator}`
6919
+ `assets.${assetId}: ${asset.state ? "state" : "animation"} needs a character or objectPro style; "${styleId}" generates ${generator}`
6357
6920
  );
6358
6921
  }
6359
6922
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
@@ -6374,7 +6937,7 @@ async function resolveSpecs(loaded, filter) {
6374
6937
  terrainUpperDescription = wrap(parsed.upper);
6375
6938
  terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
6376
6939
  prompt = subject.trim();
6377
- } else if (generator === "character" && characterKind !== "base") {
6940
+ } else if ((generator === "character" || generator === "objectPro") && characterKind !== "base") {
6378
6941
  prompt = subject.trim();
6379
6942
  } else {
6380
6943
  prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
@@ -6439,8 +7002,11 @@ async function resolveSpecs(loaded, filter) {
6439
7002
  terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
6440
7003
  terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
6441
7004
  terrainView: generator === "terrain" ? style.terrainView : void 0,
7005
+ isometricTileSize: generator === "isometricTile" ? style.isometricTileSize : void 0,
7006
+ isometricTileShape: generator === "isometricTile" ? style.isometricTileShape : void 0,
6442
7007
  ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
6443
- cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generationCost(width, height, generator),
7008
+ ...generator === "objectPro" ? { objectPro: await resolveObjectProShape(assetId, asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7009
+ cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generator === "isometricTile" ? isometricTileCost() : generationCost(width, height, generator),
6444
7010
  costUnit: "generations",
6445
7011
  candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" || generator === "imagePro" ? candidateCount(size) : 1
6446
7012
  };
@@ -6505,6 +7071,18 @@ async function resolveSpecs(loaded, filter) {
6505
7071
  } else if (sourceSpec.character) {
6506
7072
  resolved.character = { ...sourceSpec.character };
6507
7073
  }
7074
+ if (sourceSpec.objectPro?.kind === "animation") {
7075
+ const facing = sourceSpec.objectPro.animation.direction;
7076
+ const mirrored = MIRRORED_DIRECTION[facing];
7077
+ if (mirrored === facing) {
7078
+ throw new Error(
7079
+ `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
7080
+ );
7081
+ }
7082
+ resolved.objectPro = { ...sourceSpec.objectPro, animation: { ...sourceSpec.objectPro.animation, direction: mirrored } };
7083
+ } else if (sourceSpec.objectPro) {
7084
+ resolved.objectPro = { ...sourceSpec.objectPro };
7085
+ }
6508
7086
  resolved.mirror = { sourceAssetId: asset.mirror, sourceSpec };
6509
7087
  resolved.generator = sourceSpec.generator;
6510
7088
  resolved.width = sourceSpec.width;
@@ -6546,6 +7124,23 @@ async function resolveSpecs(loaded, filter) {
6546
7124
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
6547
7125
  };
6548
7126
  }
7127
+ if (resolved.objectPro && characterParent) {
7128
+ const parentSpec = await finalize(characterParent);
7129
+ if (!parentSpec.objectPro) {
7130
+ throw new Error(`assets.${assetId}: ${characterParent} is not an objectPro asset`);
7131
+ }
7132
+ const parentDirections = parentSpec.objectPro.directions;
7133
+ const parentFile = memberPath(parentSpec.outFile, "south", 0, parentDirections, MediaType.PNG);
7134
+ resolved.objectPro = {
7135
+ ...resolved.objectPro,
7136
+ parentAssetId: characterParent,
7137
+ parentSpec,
7138
+ parentFile,
7139
+ parentSha256: (0, import_node_fs5.existsSync)(parentFile) ? await sha256File(parentFile) : null,
7140
+ // A state and its animations keep the base's own rotation count.
7141
+ directions: resolved.objectPro.kind === "base" ? resolved.objectPro.directions : parentSpec.objectPro.directions
7142
+ };
7143
+ }
6549
7144
  if (asset.revision) {
6550
7145
  if (!activeProvider.supportsRevision?.(asset.revision.mode)) {
6551
7146
  throw new Error(
@@ -6731,6 +7326,60 @@ async function resolveCharacterShape(asset, style, kind, files) {
6731
7326
  }
6732
7327
  return shape;
6733
7328
  }
7329
+ async function resolveObjectProShape(assetId, asset, style, kind, files) {
7330
+ const directions = style.objectDirections ?? 8;
7331
+ const shape = {
7332
+ kind,
7333
+ directions,
7334
+ ...style.styleTraits ? { styleTraits: style.styleTraits } : {}
7335
+ };
7336
+ if (asset.reference) {
7337
+ const south = typeof asset.reference === "string" ? asset.reference : asset.reference.south;
7338
+ if (south) {
7339
+ const image = await files.load(south, "Reference sprite");
7340
+ shape.reference = { path: import_node_path5.default.resolve(files.root, south), sha256: image.hash, width: image.width, height: image.height, format: image.format };
7341
+ }
7342
+ }
7343
+ if (asset.state) {
7344
+ shape.state = {
7345
+ paletteFromReference: asset.state.paletteFromReference,
7346
+ ...asset.state.canvas ? { canvas: asset.state.canvas } : {}
7347
+ };
7348
+ }
7349
+ if (asset.animation) {
7350
+ for (const [field, value] of Object.entries({
7351
+ template: asset.animation.template,
7352
+ subject: asset.animation.subject,
7353
+ outline: asset.animation.outline,
7354
+ shading: asset.animation.shading,
7355
+ detail: asset.animation.detail
7356
+ })) {
7357
+ if (value !== void 0) {
7358
+ throw new Error(
7359
+ `assets.${assetId}: objectPro has no skeleton/template concept, so animation.${field} does not apply; describe the motion in the asset's own prompt instead`
7360
+ );
7361
+ }
7362
+ }
7363
+ if (asset.animation.mode === "template") {
7364
+ throw new Error(`assets.${assetId}: objectPro animations have no "template" mode; use "v3" or "pro"`);
7365
+ }
7366
+ const pose = async (rel, what) => {
7367
+ const image = await files.load(rel, what);
7368
+ return { path: import_node_path5.default.resolve(files.root, rel), sha256: image.hash, width: image.width, height: image.height, format: image.format };
7369
+ };
7370
+ shape.animation = {
7371
+ mode: asset.animation.mode ?? "v3",
7372
+ direction: asset.animation.direction,
7373
+ frames: asset.animation.frames ?? 8,
7374
+ fps: asset.animation.fps,
7375
+ keepFirstFrame: asset.animation.keepFirstFrame,
7376
+ ...asset.animation.startFrame ? { startFrame: await pose(asset.animation.startFrame, "Animation start frame") } : {},
7377
+ ...asset.animation.endFrame ? { endFrame: await pose(asset.animation.endFrame, "Animation end frame") } : {},
7378
+ ...asset.animation.enhancePrompt !== void 0 ? { enhancePrompt: asset.animation.enhancePrompt } : {}
7379
+ };
7380
+ }
7381
+ return shape;
7382
+ }
6734
7383
  function characterDirections(directions) {
6735
7384
  return directions === 4 ? CHARACTER_DIRECTIONS_4 : CHARACTER_DIRECTIONS_8;
6736
7385
  }
@@ -8207,6 +8856,7 @@ async function inspectRevisionReadiness(spec, lock) {
8207
8856
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
8208
8857
  if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
8209
8858
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
8859
+ if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
8210
8860
  if (!spec.revision) return null;
8211
8861
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
8212
8862
  }
@@ -8248,6 +8898,24 @@ async function inspectCharacterParent(spec, lock, seen) {
8248
8898
  }
8249
8899
  return { ready: true, reason: `${label} is current` };
8250
8900
  }
8901
+ async function inspectObjectProParent(spec, lock, seen) {
8902
+ const object = spec.objectPro;
8903
+ const parentSpec = object.parentSpec;
8904
+ const label = `${object.kind} parent ${spec.styleId}/${object.parentAssetId}`;
8905
+ const dependency = await inspectParent(parentSpec, lock, seen);
8906
+ if (!dependency.ready) return { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
8907
+ if (!object.parentSha256 || !object.parentFile || !(0, import_node_fs8.existsSync)(object.parentFile)) {
8908
+ return { ready: false, reason: `${label} has no generated south-facing file yet` };
8909
+ }
8910
+ if (await sha256File(object.parentFile) !== object.parentSha256) {
8911
+ return { ready: false, reason: `${label} changed after the manifest was resolved` };
8912
+ }
8913
+ const parentEntry = lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)];
8914
+ if (!parentEntry?.objectId) {
8915
+ return { ready: false, reason: `${label} has no PixelLab object id recorded` };
8916
+ }
8917
+ return { ready: true, reason: `${label} is current` };
8918
+ }
8251
8919
  async function inspectRevision(spec, lock, seen) {
8252
8920
  const revision = spec.revision;
8253
8921
  if (!revision) return { ready: true, reason: "asset has no revision inputs" };
@@ -8299,6 +8967,10 @@ async function inspectParent(spec, lock, seen) {
8299
8967
  const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
8300
8968
  if (!inputs.ready) return inputs;
8301
8969
  }
8970
+ if (spec.objectPro?.parentSpec) {
8971
+ const inputs = await inspectObjectProParent(spec, lock, nextSeen);
8972
+ if (!inputs.ready) return inputs;
8973
+ }
8302
8974
  if (spec.quality) {
8303
8975
  const quality = await inspectQualityProfile(spec, lock);
8304
8976
  return quality?.state === "approved" ? { ready: true, reason: "approved quality output is current" } : {
@@ -8343,7 +9015,8 @@ function dependencies(spec) {
8343
9015
  spec.mirror?.sourceSpec,
8344
9016
  spec.revision?.sourceSpec,
8345
9017
  spec.character?.parentSpec,
8346
- spec.character?.styleAnchor?.spec
9018
+ spec.character?.styleAnchor?.spec,
9019
+ spec.objectPro?.parentSpec
8347
9020
  ].filter((parent) => Boolean(parent));
8348
9021
  }
8349
9022
  function resumeCommandForStatus(status) {
@@ -11004,7 +11677,8 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
11004
11677
  lastSubmitAt = Date.now();
11005
11678
  try {
11006
11679
  const refs = styleImages.get(spec.styleId) ?? [];
11007
- const parentEntry = spec.character?.parentSpec ? lock.entries[lockKey(spec.character.parentSpec.styleId, spec.character.parentSpec.assetId)] : void 0;
11680
+ const parentSpec = spec.character?.parentSpec ?? spec.objectPro?.parentSpec;
11681
+ const parentEntry = parentSpec ? lock.entries[lockKey(parentSpec.styleId, parentSpec.assetId)] : void 0;
11008
11682
  const anchor = spec.character?.styleAnchor;
11009
11683
  const anchorEntry = anchor ? lock.entries[lockKey(anchor.spec.styleId, anchor.spec.assetId)] : void 0;
11010
11684
  const replacedMetadata = previousEntry?.providerMetadata?.[provider.id];
@@ -15239,6 +15913,7 @@ async function workspaceStatus(ws, dir) {
15239
15913
  isFrameSetEntry,
15240
15914
  isMemberSetEntry,
15241
15915
  isSensitiveSourceUrl,
15916
+ isometricTileCost,
15242
15917
  listBundledRecipes,
15243
15918
  loadClaims,
15244
15919
  loadLock,