pixelkiln 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -707,7 +707,7 @@ import path2 from "path";
707
707
  // src/types.ts
708
708
  import { z } from "zod";
709
709
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
710
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character"]);
710
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain"]);
711
711
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
712
712
  var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
713
713
  function tileVariationCount(descriptions) {
@@ -740,6 +740,14 @@ function tilesCost(tileSize, variations) {
740
740
  if (px <= 2048) return 25;
741
741
  return 40;
742
742
  }
743
+ function terrainTileCount(transitionSize) {
744
+ return transitionSize === 1 ? 25 : 16;
745
+ }
746
+ function parseTerrainDescriptions(prompt) {
747
+ const parts = prompt.split(/\d+\s*\)\s*\./).map((part) => part.trim()).filter(Boolean);
748
+ if (parts.length < 2) return null;
749
+ return { lower: parts[0], upper: parts[1], transition: parts[2] };
750
+ }
743
751
  var StyleImageSchema = z.object({
744
752
  /** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
745
753
  path: z.string()
@@ -938,17 +946,37 @@ var StyleObjectSchema = z.object({
938
946
  shading: z.string().optional(),
939
947
  detail: z.string().optional(),
940
948
  /**
941
- * `tiles` generator only. Edge length of one tile, 16-256.
949
+ * `tiles` generator only. Edge length of one tile, 16-128 (the API's own
950
+ * range; connectable sets narrow it further per shape, e.g. square
951
+ * top-down roads are fixed at 32).
942
952
  *
943
953
  * Ignored when `styleImages` is set: style mode takes the tile's shape
944
954
  * and dimensions from the reference image, which is the whole reason to
945
955
  * use it against an existing sheet.
946
956
  */
947
- tileSize: z.number().int().min(16).max(256).optional(),
957
+ tileSize: z.number().int().min(16).max(128).optional(),
958
+ /** `tiles` generator only. Height in pixels (16-256) for a non-square
959
+ * tile (e.g. a tall building wall); omit to compute it from `tileType`
960
+ * geometry and the view angle. */
961
+ tileHeight: z.number().int().min(16).max(256).optional(),
948
962
  /** `tiles` generator only. Defaults to the API's `isometric`. */
949
963
  tileType: z.enum(["hex", "hex_pointy", "isometric", "oblique", "octagon", "square_topdown"]).optional(),
950
964
  /** `tiles` generator only. Defaults to the API's `low top-down`. */
951
965
  tileView: z.enum(["top-down", "high top-down", "low top-down", "side"]).optional(),
966
+ /** `tiles` generator only. Continuous view angle in degrees (0 = side,
967
+ * 90 = top-down), overriding `tileView` when set. */
968
+ tileViewAngle: z.number().min(0).max(90).optional(),
969
+ /** `tiles` generator only. Tile depth/thickness as a ratio (0-1) of the
970
+ * tile's height, overriding the default the API computes from the
971
+ * view. This is the tutorials' "thickness" control. */
972
+ tileDepthRatio: z.number().min(0).max(1).optional(),
973
+ /** `tiles` generator only, `tileType: "isometric"`. Top/bottom cap
974
+ * width in pixels: 2 for the classic look, 4 for a more modern one. */
975
+ tileFlatTopPx: z.number().int().min(2).max(8).optional(),
976
+ /** `tiles` generator only, `tileType: "oblique"` (ground tiles or
977
+ * building walls). Horizontal shear per pixel of height (0-1); 0.5 is
978
+ * a classic cabinet projection (~27°), 1.0 a full 45° diagonal. */
979
+ obliqueLean: z.number().min(0).max(1).optional(),
952
980
  /**
953
981
  * `tiles` generator only. Asks for a connectable set instead of
954
982
  * independent variations:
@@ -963,6 +991,28 @@ var StyleObjectSchema = z.object({
963
991
  * is load-bearing; do not sort a connectable set by anything else.
964
992
  */
965
993
  tileFeature: z.enum(["roads", "tileset", "building"]).optional(),
994
+ /** `tileFeature: "building"` only. Wall height in tiles (1-3); the API
995
+ * defaults to 2. */
996
+ buildingWallTiles: z.number().int().min(1).max(3).optional(),
997
+ /** `tileFeature: "building"` only. `"grid"` paints each shaped piece
998
+ * individually (richer, the isometric default); `"materials"` paints
999
+ * flat swatches and renders pieces from them (more consistent for
1000
+ * square top-down building kits). */
1001
+ buildingLayout: z.enum(["grid", "materials"]).optional(),
1002
+ /** `tileFeature: "building"` only. Wall material, e.g. "stone brick
1003
+ * walls" — more reliable than relying on the main `prompt` being split
1004
+ * into wall/floor parts. */
1005
+ buildingWallDescription: z.string().min(1).max(500).optional(),
1006
+ /** `tileFeature: "building"` only. Floor material, e.g. "wooden plank
1007
+ * floor". */
1008
+ buildingFloorDescription: z.string().min(1).max(500).optional(),
1009
+ /** `tileFeature: "building"` only. Upper-storey or roof surface;
1010
+ * defaults to the wall material when omitted. */
1011
+ buildingFloor2Description: z.string().min(1).max(500).optional(),
1012
+ /** `tileFeature: "building"` with `tileType: "square_topdown"` only.
1013
+ * Wall storey height as its own camera angle in degrees (5-90),
1014
+ * decoupled from the ground's pitch. */
1015
+ buildingWallAngle: z.number().min(5).max(90).optional(),
966
1016
  /**
967
1017
  * `tiles` generator only. How tile edges are drawn.
968
1018
  *
@@ -977,6 +1027,48 @@ var StyleObjectSchema = z.object({
977
1027
  * rather than inheriting.
978
1028
  */
979
1029
  outlineMode: z.enum(["outline", "segmentation"]).optional(),
1030
+ /**
1031
+ * `terrain` generator only. Edge length of one tile in pixels. 16 or 32
1032
+ * work in both modes; 64 needs `terrainMode: "pro"`. The API default is
1033
+ * 16. See `parseTerrainDescriptions` for how an asset's `prompt` becomes
1034
+ * the lower/upper/transition terrain descriptions this endpoint wants.
1035
+ */
1036
+ terrainTileSize: z.union([z.literal(16), z.literal(32), z.literal(64)]).optional(),
1037
+ /**
1038
+ * `terrain` generator only. `standard` is the classic Wang tileset
1039
+ * pipeline; `pro` is a newer corner-pair pipeline with its own shape
1040
+ * controls (`terrainSpreadX`, `terrainSlopeSize`, `terrainRaggedness`)
1041
+ * in place of `terrainShapeStyle`. The API default is `standard`.
1042
+ */
1043
+ terrainMode: z.enum(["standard", "pro"]).optional(),
1044
+ /**
1045
+ * `terrain` generator only, `terrainMode: "standard"`. Procedural
1046
+ * boundary geometry: `square` or `round`, 16px or 32px tiles only.
1047
+ * Rejected together with `terrainMode: "pro"`, whose own shape controls
1048
+ * are `terrainSpreadX`/`terrainSlopeSize`/`terrainRaggedness`.
1049
+ */
1050
+ terrainShapeStyle: z.enum(["square", "round"]).optional(),
1051
+ /** `terrain` generator only, `terrainMode: "pro"`. Boundary spread
1052
+ * between terrains (0 = steep, 1 = gradual). API default 0.5. */
1053
+ terrainSpreadX: z.number().min(0).max(1).optional(),
1054
+ /** `terrain` generator only, `terrainMode: "pro"`. Slope on the N/W/E
1055
+ * sides as a fraction of wall height. API default 0. */
1056
+ terrainSlopeSize: z.number().min(0).max(1).optional(),
1057
+ /** `terrain` generator only, `terrainMode: "pro"`. Terrain boundary
1058
+ * noise (0 = smooth, 1 = rough). API default 0. */
1059
+ terrainRaggedness: z.number().min(0).max(1).optional(),
1060
+ /**
1061
+ * `terrain` generator only. Visual height of the step between lower and
1062
+ * upper terrain. Without `terrainShapeStyle`, only 0, 0.25, 0.5, or 1 are
1063
+ * accepted (1 switches to the 25-tile cliff layout, where corners take a
1064
+ * third "transition" value); with it, any value from 0 to 1 works, but
1065
+ * above 0.5 switches to an extended 32-tile layout this adapter does not
1066
+ * model. API default 0.
1067
+ */
1068
+ terrainTransitionSize: z.number().min(0).max(1).optional(),
1069
+ /** `terrain` generator only. Camera angle; the API default is `high
1070
+ * top-down`. */
1071
+ terrainView: z.enum(["low top-down", "high top-down"]).optional(),
980
1072
  /**
981
1073
  * `pixflux` only. Whether to strip the generated background.
982
1074
  *
@@ -1090,7 +1182,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
1090
1182
  message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
1091
1183
  path: ["tileFeature"]
1092
1184
  }).superRefine((style, ctx) => {
1093
- if (style.quality && (style.generator === "tiles" || style.generator === "animation")) {
1185
+ if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation")) {
1094
1186
  ctx.addIssue({
1095
1187
  code: z.ZodIssueCode.custom,
1096
1188
  message: "quality profiles currently support single-image generators only",
@@ -5115,6 +5207,11 @@ var MapSubmitSchema = z2.object({
5115
5207
  object_id: z2.string().min(1),
5116
5208
  status: z2.string().default("processing")
5117
5209
  }).passthrough();
5210
+ var UsageSchema = z2.object({
5211
+ type: z2.string().optional(),
5212
+ usd: z2.number().nullable().optional(),
5213
+ generations: z2.number().nullable().optional()
5214
+ }).passthrough().nullable().optional();
5118
5215
  var TilesSubmitSchema = z2.object({
5119
5216
  tile_id: z2.string().min(1),
5120
5217
  background_job_id: z2.string().min(1),
@@ -5125,6 +5222,32 @@ var TilesProSchema = z2.object({
5125
5222
  kind: z2.string().nullable().default(null),
5126
5223
  tile_rules: z2.record(z2.unknown()).nullable().optional()
5127
5224
  }).passthrough();
5225
+ var TilesetSubmitSchema = z2.object({
5226
+ tileset_id: z2.string().min(1),
5227
+ background_job_id: z2.string().min(1),
5228
+ status: z2.literal("processing").default("processing")
5229
+ }).passthrough();
5230
+ var TilesetTileSchema = z2.object({
5231
+ id: z2.string().min(1),
5232
+ name: z2.string(),
5233
+ image: z2.object({ base64: z2.string().min(1), format: z2.string().default("png") }).passthrough(),
5234
+ corners: z2.object({ NW: z2.string(), NE: z2.string(), SW: z2.string(), SE: z2.string() }).passthrough(),
5235
+ pattern_4x4: z2.object({
5236
+ row_0: z2.array(z2.number()),
5237
+ row_1: z2.array(z2.number()),
5238
+ row_2: z2.array(z2.number()),
5239
+ row_3: z2.array(z2.number())
5240
+ }).passthrough()
5241
+ }).passthrough();
5242
+ var TilesetGetSchema = z2.object({
5243
+ tileset: z2.object({
5244
+ total_tiles: z2.number().int().min(1),
5245
+ tile_size: z2.object({ width: z2.number().int().positive(), height: z2.number().int().positive() }).passthrough(),
5246
+ terrain_types: z2.array(z2.string()),
5247
+ tiles: z2.array(TilesetTileSchema).min(1)
5248
+ }).passthrough(),
5249
+ usage: UsageSchema
5250
+ }).passthrough();
5128
5251
  var RevisionJobSubmitSchema = z2.object({
5129
5252
  background_job_id: z2.string().min(1),
5130
5253
  status: z2.string().default("processing")
@@ -5156,11 +5279,6 @@ var MapObjectSchema = z2.object({
5156
5279
  var ObjectListSchema = z2.object({ objects: z2.array(PixelLabObjectSchema), total: z2.number().int().min(0) }).passthrough();
5157
5280
  var PixfluxResponseSchema = z2.object({ image: z2.object({ base64: z2.string().min(1) }).passthrough(), usage: z2.unknown().optional() }).passthrough();
5158
5281
  var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
5159
- var UsageSchema = z2.object({
5160
- type: z2.string().optional(),
5161
- usd: z2.number().nullable().optional(),
5162
- generations: z2.number().nullable().optional()
5163
- }).passthrough().nullable().optional();
5164
5282
  var CharacterSubmitSchema = z2.object({
5165
5283
  background_job_id: z2.string().min(1),
5166
5284
  character_id: z2.string().min(1),
@@ -5345,9 +5463,20 @@ var PixelLabClient = class {
5345
5463
  async createTilesPro(args) {
5346
5464
  const body = { description: args.description };
5347
5465
  if (args.tileSize != null) body.tile_size = args.tileSize;
5466
+ if (args.tileHeight != null) body.tile_height = args.tileHeight;
5348
5467
  if (args.tileType) body.tile_type = args.tileType;
5349
5468
  if (args.tileView) body.tile_view = args.tileView;
5469
+ if (args.tileViewAngle != null) body.tile_view_angle = args.tileViewAngle;
5470
+ if (args.tileDepthRatio != null) body.tile_depth_ratio = args.tileDepthRatio;
5471
+ if (args.tileFlatTopPx != null) body.tile_flat_top_px = args.tileFlatTopPx;
5472
+ if (args.obliqueLean != null) body.oblique_lean = args.obliqueLean;
5350
5473
  if (args.tileFeature) body.tile_feature = args.tileFeature;
5474
+ if (args.buildingWallTiles != null) body.building_wall_tiles = args.buildingWallTiles;
5475
+ if (args.buildingLayout) body.building_layout = args.buildingLayout;
5476
+ if (args.buildingWallDescription) body.building_wall_description = args.buildingWallDescription;
5477
+ if (args.buildingFloorDescription) body.building_floor_description = args.buildingFloorDescription;
5478
+ if (args.buildingFloor2Description) body.building_floor2_description = args.buildingFloor2Description;
5479
+ if (args.buildingWallAngle != null) body.building_wall_angle = args.buildingWallAngle;
5351
5480
  if (args.outlineMode) body.outline_mode = args.outlineMode;
5352
5481
  if (args.seed != null) body.seed = args.seed;
5353
5482
  if (args.styleImages?.length) body.style_images = args.styleImages;
@@ -5365,6 +5494,45 @@ var PixelLabClient = class {
5365
5494
  "get tiles"
5366
5495
  );
5367
5496
  }
5497
+ /**
5498
+ * `/create-tileset`: two named terrain levels (`lower`/`upper`) and the
5499
+ * transition between them, laid out as a Wang corner set. Unlike
5500
+ * `/create-tiles-pro`, the descriptions are separate fields the API itself
5501
+ * places on the terrain vertex grid, not one prompt it splits by number.
5502
+ */
5503
+ async createTileset(args) {
5504
+ const body = {
5505
+ lower_description: args.lowerDescription,
5506
+ upper_description: args.upperDescription
5507
+ };
5508
+ if (args.transitionDescription) body.transition_description = args.transitionDescription;
5509
+ if (args.tileSize != null) body.tile_size = { width: args.tileSize, height: args.tileSize };
5510
+ if (args.mode) body.mode = args.mode;
5511
+ if (args.shapeStyle) body.shape_style = args.shapeStyle;
5512
+ if (args.spreadX != null) body.spread_x = args.spreadX;
5513
+ if (args.slopeSize != null) body.slope_size = args.slopeSize;
5514
+ if (args.raggedness != null) body.raggedness = args.raggedness;
5515
+ if (args.transitionSize != null) body.transition_size = args.transitionSize;
5516
+ if (args.view) body.view = args.view;
5517
+ if (args.outline) body.outline = args.outline;
5518
+ if (args.shading) body.shading = args.shading;
5519
+ if (args.detail) body.detail = args.detail;
5520
+ if (args.seed != null) body.seed = args.seed;
5521
+ return validateResponse(
5522
+ TilesetSubmitSchema,
5523
+ await this.request("/create-tileset", { method: "POST", body: JSON.stringify(body) }),
5524
+ "create tileset"
5525
+ );
5526
+ }
5527
+ /** Throws PixelLabError(423) while the set is still drawing; see Tileset. */
5528
+ async getTileset(tilesetId) {
5529
+ const res = await validateResponse(
5530
+ TilesetGetSchema,
5531
+ await this.request(`/tilesets/${tilesetId}`),
5532
+ "get tileset"
5533
+ );
5534
+ return { tileset: res.tileset, usage: res.usage ?? null };
5535
+ }
5368
5536
  /**
5369
5537
  * Synchronous single-image generation. Returns the PNG inline rather than a
5370
5538
  * job id, and is the only endpoint that honours a forced palette;
@@ -5603,13 +5771,21 @@ var PixelLabClient = class {
5603
5771
  /**
5604
5772
  * Masked inpaint, PixelLab's `/inpaint-v3` (the endpoint its own docs list
5605
5773
  * first in the Inpaint section, its convention for "reach for this by
5606
- * default"). Unlike the rest of this client, this method's shape is taken
5607
- * from the OpenAPI spec, not exercised against a live account: the request
5608
- * side is exact (`InpaintV3Request`), but a completed job's `last_response`
5609
- * has no documented example for this endpoint (the spec's only worked
5610
- * example is a character job's shape). `pollRevision` in pixellab.ts reads
5611
- * it defensively and fails loudly on an unrecognized shape rather than
5612
- * guessing.
5774
+ * default"). Exercised live at nine sizes, all returning `last_response.
5775
+ * image` as a single `{type, base64, width, height}` (the first shape
5776
+ * `pollRevision` in pixellab.ts checks, so no change was ever needed):
5777
+ * 32x32 (1024px², billed 20, this adapter's tiering floor) through
5778
+ * 256x256 (65536px²) all billed 20 (the tiering wrongly predicts 25 from
5779
+ * 1024px² up, see estimate() in pixellab.ts); 288x288 (82944px²) and
5780
+ * 320x320 (102400px²) billed 25 — the middle tier is real, just starting
5781
+ * far higher than this tiering assumes; 352x352 (123904px²), 384x384
5782
+ * (147456px²), and 512x512 (262144px², the tiering ceiling) all billed
5783
+ * 40. Both breakpoints are now tightly bracketed: 20->25 in
5784
+ * (65536px², 82944px²], 25->40 in (102400px², 123904px²].
5785
+ * `editImagesV2` below returns the same fields under `images`, plural
5786
+ * and array-wrapped, not `image` — do not assume the two endpoints share
5787
+ * one response shape. See docs/ENDPOINTS.md and docs/REVISIONS.md for
5788
+ * the full shape and cost picture, including what is still unconfirmed.
5613
5789
  *
5614
5790
  * `crop_to_mask` defaults true upstream (confirmed in the schema): PixelLab
5615
5791
  * otherwise blends generated pixels outside the mask edge to "fit
@@ -5632,11 +5808,15 @@ var PixelLabClient = class {
5632
5808
  );
5633
5809
  }
5634
5810
  /**
5635
- * Whole-image edit with no mask, PixelLab's `/edit-images-v2`. Same
5636
- * not-yet-live-verified caveat as `inpaintV3` above. `edit_images` takes an
5637
- * array (the endpoint supports editing several images with one
5811
+ * Whole-image edit with no mask, PixelLab's `/edit-images-v2`. `edit_images`
5812
+ * takes an array (the endpoint supports editing several images with one
5638
5813
  * instruction); pixelkiln's `revision` model is one parent per child, so
5639
- * this always sends exactly one.
5814
+ * this always sends exactly one. Exercised live at its floor size (32x32):
5815
+ * billed exactly the 20-generation estimate this adapter borrows from
5816
+ * `1dir`, matching `inpaintV3`'s floor exactly. The completed response is
5817
+ * `last_response.images`, an *array* of `{type, base64, width, height}` —
5818
+ * plural and array-wrapped, unlike `inpaintV3`'s singular `image` above,
5819
+ * even though exactly one image is ever sent or expected here.
5640
5820
  */
5641
5821
  async editImagesV2(args) {
5642
5822
  const body = {
@@ -5791,7 +5971,7 @@ var PixelLabProvider = class _PixelLabProvider {
5791
5971
  return new _PixelLabProvider(new PixelLabClient("download-only"));
5792
5972
  }
5793
5973
  supports(generator) {
5794
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
5974
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "character";
5795
5975
  }
5796
5976
  /**
5797
5977
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -5827,7 +6007,7 @@ var PixelLabProvider = class _PixelLabProvider {
5827
6007
  const height = spec.revision.sourceHeight ?? spec.height;
5828
6008
  return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
5829
6009
  }
5830
- if (spec.generator === "tiles") {
6010
+ if (spec.generator === "tiles" || spec.generator === "terrain") {
5831
6011
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
5832
6012
  }
5833
6013
  if (spec.generator === "character") {
@@ -5871,10 +6051,55 @@ var PixelLabProvider = class _PixelLabProvider {
5871
6051
  "high detail"
5872
6052
  ]);
5873
6053
  }
6054
+ if (spec.generator === "terrain") {
6055
+ if (spec.outline) {
6056
+ requirePixelLabOption("outline", spec.outline, [
6057
+ "single color black outline",
6058
+ "single color outline",
6059
+ "selective outline",
6060
+ "lineless"
6061
+ ]);
6062
+ }
6063
+ if (spec.shading) {
6064
+ requirePixelLabOption("shading", spec.shading, [
6065
+ "flat shading",
6066
+ "basic shading",
6067
+ "medium shading",
6068
+ "detailed shading",
6069
+ "highly detailed shading"
6070
+ ]);
6071
+ }
6072
+ if (spec.detail) {
6073
+ requirePixelLabOption("detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
6074
+ }
6075
+ if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
6076
+ throw new Error(
6077
+ "PixelLab terrain: terrainShapeStyle is standard-mode only; pro's own shape controls are terrainSpreadX/terrainSlopeSize/terrainRaggedness"
6078
+ );
6079
+ }
6080
+ if (spec.terrainTileSize === 64 && spec.terrainMode !== "pro") {
6081
+ throw new Error('PixelLab terrain: a 64px tile needs terrainMode: "pro"');
6082
+ }
6083
+ if (spec.terrainShapeStyle && spec.terrainTransitionSize != null && spec.terrainTransitionSize > 0.5) {
6084
+ throw new Error(
6085
+ "PixelLab terrain: terrainShapeStyle with terrainTransitionSize above 0.5 uses an extended 32-tile layout pixelkiln does not model; use 0, 0.25, or 0.5"
6086
+ );
6087
+ }
6088
+ if (!spec.terrainShapeStyle && spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
6089
+ throw new Error(
6090
+ "PixelLab terrain: terrainTransitionSize must be 0, 0.25, 0.5, or 1 unless terrainShapeStyle is set"
6091
+ );
6092
+ }
6093
+ }
5874
6094
  if (spec.generator === "character") this.validateCharacter(spec, styleImages);
5875
6095
  if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
5876
6096
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
5877
6097
  }
6098
+ if (spec.generator === "terrain" && styleImages.length) {
6099
+ throw new Error(
6100
+ "PixelLab terrain does not support style images yet; use color_image/reference images directly against /create-tileset if this becomes a real need"
6101
+ );
6102
+ }
5878
6103
  for (const image of styleImages) {
5879
6104
  if (image.width > 256 || image.height > 256) {
5880
6105
  throw new Error(
@@ -6215,9 +6440,20 @@ var PixelLabProvider = class _PixelLabProvider {
6215
6440
  const res2 = await this.client.createTilesPro({
6216
6441
  description: spec.prompt,
6217
6442
  tileSize: spec.tileSize,
6443
+ tileHeight: spec.tileHeight,
6218
6444
  tileType: spec.tileType,
6219
6445
  tileView: spec.tileView,
6446
+ tileViewAngle: spec.tileViewAngle,
6447
+ tileDepthRatio: spec.tileDepthRatio,
6448
+ tileFlatTopPx: spec.tileFlatTopPx,
6449
+ obliqueLean: spec.obliqueLean,
6220
6450
  tileFeature: spec.tileFeature,
6451
+ buildingWallTiles: spec.buildingWallTiles,
6452
+ buildingLayout: spec.buildingLayout,
6453
+ buildingWallDescription: spec.buildingWallDescription,
6454
+ buildingFloorDescription: spec.buildingFloorDescription,
6455
+ buildingFloor2Description: spec.buildingFloor2Description,
6456
+ buildingWallAngle: spec.buildingWallAngle,
6221
6457
  outlineMode: spec.outlineMode,
6222
6458
  seed: spec.seed,
6223
6459
  // TilesProStyleImage is flat and wants the reference's real dimensions,
@@ -6226,6 +6462,26 @@ var PixelLabProvider = class _PixelLabProvider {
6226
6462
  });
6227
6463
  return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
6228
6464
  }
6465
+ if (spec.generator === "terrain") {
6466
+ const res2 = await this.client.createTileset({
6467
+ lowerDescription: spec.terrainLowerDescription,
6468
+ upperDescription: spec.terrainUpperDescription,
6469
+ transitionDescription: spec.terrainTransitionDescription,
6470
+ tileSize: spec.terrainTileSize,
6471
+ mode: spec.terrainMode,
6472
+ shapeStyle: spec.terrainShapeStyle,
6473
+ spreadX: spec.terrainSpreadX,
6474
+ slopeSize: spec.terrainSlopeSize,
6475
+ raggedness: spec.terrainRaggedness,
6476
+ transitionSize: spec.terrainTransitionSize,
6477
+ view: spec.terrainView,
6478
+ outline: spec.outline,
6479
+ shading: spec.shading,
6480
+ detail: spec.detail,
6481
+ seed: spec.seed
6482
+ });
6483
+ return { jobId: res2.tileset_id, metadata: { backgroundJobId: res2.background_job_id } };
6484
+ }
6229
6485
  if (spec.generator === "1dir") {
6230
6486
  const res2 = await this.client.create1Direction({
6231
6487
  description: spec.prompt,
@@ -6331,6 +6587,7 @@ var PixelLabProvider = class _PixelLabProvider {
6331
6587
  }
6332
6588
  if (generator === "map") return this.pollMap(jobId, context);
6333
6589
  if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
6590
+ if (generator === "terrain") return this.pollTerrain(jobId, context);
6334
6591
  if (generator === "character") return this.pollCharacter(jobId, context);
6335
6592
  const backgroundJobId = context?.metadata?.backgroundJobId;
6336
6593
  const obj = await this.client.getObject(jobId);
@@ -6351,16 +6608,17 @@ var PixelLabProvider = class _PixelLabProvider {
6351
6608
  /**
6352
6609
  * `/inpaint-v3` and `/edit-images-v2` both hand back a plain background
6353
6610
  * job with no resource of its own, polled generically at
6354
- * `GET /background-jobs/{id}`. What a *completed* job's `last_response`
6355
- * actually contains is not documented for either endpoint: the OpenAPI
6356
- * spec's only worked example of that field is a character job's shape
6357
- * (`character_id`, `uploaded_directions`, ...), not an inpaint or edit
6358
- * job's. This reads every image shape seen elsewhere in this client
6359
- * (a nested `{image: {base64, format}}`, a bare `{base64, format}`, or a
6360
- * hosted URL under a handful of plausible keys) and fails loudly, naming
6361
- * the keys it actually got, rather than guess wrong silently. Fixing a
6362
- * real completed response into this list is a one-line change once one is
6363
- * seen live.
6611
+ * `GET /background-jobs/{id}`. Confirmed live for both, and the two do not
6612
+ * match: a completed `inpaint-v3` job's `last_response.image` is a single
6613
+ * `{type: "base64", base64, width, height}`, matched by the `imageKeys`
6614
+ * branch below; a completed `edit-images-v2` job's is `last_response.images`,
6615
+ * an *array* of that same shape, matched by the `done.images` branch below
6616
+ * instead exactly why both branches exist rather than just the first one
6617
+ * (docs/ENDPOINTS.md, docs/REVISIONS.md). Beyond these two confirmed
6618
+ * shapes, this also checks a nested `{image: {base64, format}}` and a
6619
+ * hosted URL under a handful of plausible keys, and fails loudly, naming
6620
+ * the keys it actually got, rather than guess wrong silently, for whatever
6621
+ * shape still isn't covered.
6364
6622
  */
6365
6623
  async pollRevision(jobId) {
6366
6624
  const job = await this.client.getBackgroundJob(jobId);
@@ -6468,6 +6726,43 @@ var PixelLabProvider = class _PixelLabProvider {
6468
6726
  throw err;
6469
6727
  }
6470
6728
  }
6729
+ /**
6730
+ * `/create-tileset` reports progress the same way `tiles` does (423 while
6731
+ * drawing, 200 once finished), but each tile's image comes back embedded
6732
+ * as base64 rather than a `storage_urls` link, so it is decoded straight
6733
+ * to a cache file the same way a revision result is (`pollRevision`). A
6734
+ * terrain set is always a connectable Wang tileset, never independent
6735
+ * candidates to review, so this goes straight to "ready" like a connectable
6736
+ * `tiles` set does.
6737
+ */
6738
+ async pollTerrain(tilesetId, context) {
6739
+ try {
6740
+ const { tileset, usage } = await this.client.getTileset(tilesetId);
6741
+ const backgroundJobId = context?.metadata?.backgroundJobId;
6742
+ const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
6743
+ const sources = [];
6744
+ const terrainTiles = [];
6745
+ for (const [index, tile] of tileset.tiles.entries()) {
6746
+ const slug = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
6747
+ const role = `tile-${String(index).padStart(2, "0")}-${slug}`;
6748
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
6749
+ writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
6750
+ sources.push({ url: `file://${file}`, role });
6751
+ terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
6752
+ }
6753
+ return {
6754
+ status: "ready",
6755
+ objectId: tilesetId,
6756
+ sourceUrl: sources[0]?.url ?? null,
6757
+ sources,
6758
+ metadata: { terrainTypes: tileset.terrain_types, terrainTiles },
6759
+ billed
6760
+ };
6761
+ } catch (err) {
6762
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
6763
+ throw err;
6764
+ }
6765
+ }
6471
6766
  async selectCandidate(jobId, index, commonTag, generator) {
6472
6767
  if (generator === "tiles") {
6473
6768
  const set = await this.client.getTilesPro(jobId);
@@ -7818,6 +8113,10 @@ async function resolveSpecs(loaded, filter) {
7818
8113
  size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
7819
8114
  width = asset.state?.canvas?.width ?? size;
7820
8115
  height = asset.state?.canvas?.height ?? size;
8116
+ } else if (generator === "terrain") {
8117
+ size = style.terrainTileSize ?? 16;
8118
+ width = size;
8119
+ height = size;
7821
8120
  } else {
7822
8121
  width = asset.width ?? style.size ?? 64;
7823
8122
  height = asset.height ?? style.size ?? 64;
@@ -7830,12 +8129,33 @@ async function resolveSpecs(loaded, filter) {
7830
8129
  }
7831
8130
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
7832
8131
  const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
7833
- const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8132
+ let terrainLowerDescription;
8133
+ let terrainUpperDescription;
8134
+ let terrainTransitionDescription;
8135
+ let prompt;
8136
+ if (generator === "terrain") {
8137
+ const parsed = parseTerrainDescriptions(subject);
8138
+ if (!parsed) {
8139
+ throw new Error(
8140
+ `assets.${assetId}: terrain needs "1). <lower terrain> 2). <upper terrain>" in its prompt, optionally followed by "3). <transition>"`
8141
+ );
8142
+ }
8143
+ const wrap = (text) => [style.promptPrefix, text, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8144
+ terrainLowerDescription = wrap(parsed.lower);
8145
+ terrainUpperDescription = wrap(parsed.upper);
8146
+ terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
8147
+ prompt = subject.trim();
8148
+ } else if (generator === "character" && characterKind !== "base") {
8149
+ prompt = subject.trim();
8150
+ } else {
8151
+ prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8152
+ }
7834
8153
  const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
7835
8154
  const outFile = path13.resolve(root, style.outDir, relFile);
7836
8155
  const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
7837
8156
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
7838
8157
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8158
+ const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
7839
8159
  const base = {
7840
8160
  styleId,
7841
8161
  assetId,
@@ -7857,14 +8177,36 @@ async function resolveSpecs(loaded, filter) {
7857
8177
  enforcePalette: style.enforcePalette,
7858
8178
  noBackground: style.noBackground,
7859
8179
  tileSize: generator === "tiles" ? tileSize : void 0,
8180
+ tileHeight: generator === "tiles" ? style.tileHeight : void 0,
7860
8181
  tileType: generator === "tiles" ? style.tileType : void 0,
7861
8182
  tileView: generator === "tiles" ? style.tileView : void 0,
8183
+ tileViewAngle: generator === "tiles" ? style.tileViewAngle : void 0,
8184
+ tileDepthRatio: generator === "tiles" ? style.tileDepthRatio : void 0,
8185
+ tileFlatTopPx: generator === "tiles" ? style.tileFlatTopPx : void 0,
8186
+ obliqueLean: generator === "tiles" ? style.obliqueLean : void 0,
7862
8187
  tileFeature: generator === "tiles" ? style.tileFeature : void 0,
8188
+ buildingWallTiles: generator === "tiles" ? style.buildingWallTiles : void 0,
8189
+ buildingLayout: generator === "tiles" ? style.buildingLayout : void 0,
8190
+ buildingWallDescription: generator === "tiles" ? style.buildingWallDescription : void 0,
8191
+ buildingFloorDescription: generator === "tiles" ? style.buildingFloorDescription : void 0,
8192
+ buildingFloor2Description: generator === "tiles" ? style.buildingFloor2Description : void 0,
8193
+ buildingWallAngle: generator === "tiles" ? style.buildingWallAngle : void 0,
7863
8194
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
8195
+ terrainLowerDescription: generator === "terrain" ? terrainLowerDescription : void 0,
8196
+ terrainUpperDescription: generator === "terrain" ? terrainUpperDescription : void 0,
8197
+ terrainTransitionDescription: generator === "terrain" ? terrainTransitionDescription : void 0,
8198
+ terrainTileSize: generator === "terrain" ? size : void 0,
8199
+ terrainMode: generator === "terrain" ? style.terrainMode : void 0,
8200
+ terrainShapeStyle: generator === "terrain" ? style.terrainShapeStyle : void 0,
8201
+ terrainSpreadX: generator === "terrain" ? style.terrainSpreadX : void 0,
8202
+ terrainSlopeSize: generator === "terrain" ? style.terrainSlopeSize : void 0,
8203
+ terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
8204
+ terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
8205
+ terrainView: generator === "terrain" ? style.terrainView : void 0,
7864
8206
  ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7865
- cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
8207
+ cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generationCost(width, height, generator),
7866
8208
  costUnit: "generations",
7867
- candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
8209
+ candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" ? candidateCount(size) : 1
7868
8210
  };
7869
8211
  const tags = [
7870
8212
  .../* @__PURE__ */ new Set([
@@ -7912,7 +8254,7 @@ async function resolveSpecs(loaded, filter) {
7912
8254
  if (asset.mirror) {
7913
8255
  if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
7914
8256
  const sourceSpec = await finalize(asset.mirror);
7915
- if (sourceSpec.generator === "tiles") {
8257
+ if (sourceSpec.generator === "tiles" || sourceSpec.generator === "terrain") {
7916
8258
  throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
7917
8259
  }
7918
8260
  if (sourceSpec.character?.kind === "animation") {