pixelkiln 0.62.0 → 0.63.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
@@ -956,6 +956,14 @@ var CharacterAnimationSchema = z.object({
956
956
  template: z.string().min(1).optional(),
957
957
  /** Direction to animate. */
958
958
  direction: CharacterDirectionSchema.default("south"),
959
+ /**
960
+ * Shorthand for one loop in several directions, instead of `direction`.
961
+ * Expanded when the manifest loads into one loop per named direction
962
+ * (`<id>.<direction>`) plus a mirror for each direction whose flip is
963
+ * not named, exactly as if each had been written out. See
964
+ * `src/loop-directions.ts`.
965
+ */
966
+ directions: z.array(CharacterDirectionSchema).min(1).optional(),
959
967
  /** Frames to generate without a template (v3). Even, 4 to 16. */
960
968
  frames: z.number().int().min(4).max(16).optional(),
961
969
  /** Playback rate recorded with the frames; PixelLab does not store one. */
@@ -984,6 +992,13 @@ var CharacterAnimationSchema = z.object({
984
992
  /** v3 text loops only: let PixelLab expand the action into a fuller motion description first. */
985
993
  enhancePrompt: z.boolean().optional()
986
994
  }).strict().superRefine((animation, context) => {
995
+ if (animation.directions !== void 0) {
996
+ context.addIssue({
997
+ code: z.ZodIssueCode.custom,
998
+ message: "directions is expanded when the manifest loads; validate the loaded manifest, not the raw file",
999
+ path: ["directions"]
1000
+ });
1001
+ }
987
1002
  if (animation.frames !== void 0 && animation.frames % 2 !== 0) {
988
1003
  context.addIssue({ code: z.ZodIssueCode.custom, message: "frames must be even", path: ["frames"] });
989
1004
  }
@@ -9549,10 +9564,102 @@ registerProvider({
9549
9564
  import { readFile as readFile6 } from "fs/promises";
9550
9565
  import { existsSync as existsSync8 } from "fs";
9551
9566
  import path13 from "path";
9567
+
9568
+ // src/loop-directions.ts
9569
+ var SINGLE_OUTPUT_FIELDS = ["file", "cell", "remoteId", "source", "sourceByStyle", "outputRole"];
9570
+ var MIRROR_FIELDS = ["category", "tags", "styles"];
9571
+ function loopDirectionId(shorthandId, direction) {
9572
+ return `${shorthandId}.${direction}`;
9573
+ }
9574
+ function expandLoopDirections(raw) {
9575
+ const families = {};
9576
+ const issues = [];
9577
+ if (!isObject2(raw) || !isObject2(raw.assets)) return { raw, families, issues };
9578
+ const assets = raw.assets;
9579
+ if (!Object.values(assets).some((asset) => isObject2(asset) && isObject2(asset.animation) && "directions" in asset.animation)) {
9580
+ return { raw, families, issues };
9581
+ }
9582
+ const expanded = {};
9583
+ const claimed = new Set(Object.keys(assets));
9584
+ for (const [id, asset] of Object.entries(assets)) {
9585
+ if (!isObject2(asset) || !isObject2(asset.animation) || !("directions" in asset.animation)) {
9586
+ expanded[id] = asset;
9587
+ continue;
9588
+ }
9589
+ const where = `assets.${id}.animation.directions`;
9590
+ const { directions: rawDirections, ...animation } = asset.animation;
9591
+ const parsed = CharacterDirectionSchema.array().min(1).safeParse(rawDirections);
9592
+ if (!parsed.success) {
9593
+ issues.push(`${where}: expected a non-empty list of directions (${CharacterDirectionSchema.options.join(", ")})`);
9594
+ continue;
9595
+ }
9596
+ const directions = parsed.data;
9597
+ const duplicate = directions.find((direction, index) => directions.indexOf(direction) !== index);
9598
+ if (duplicate) {
9599
+ issues.push(`${where}: "${duplicate}" is listed twice`);
9600
+ continue;
9601
+ }
9602
+ if ("direction" in animation) {
9603
+ issues.push(`assets.${id}.animation: direction and directions are mutually exclusive; directions names every loop to make`);
9604
+ continue;
9605
+ }
9606
+ const single = SINGLE_OUTPUT_FIELDS.filter((field) => field in asset);
9607
+ if (single.length) {
9608
+ issues.push(
9609
+ `assets.${id}: ${single.join(", ")} name${single.length === 1 ? "s" : ""} one output, but directions makes ${directions.length} loops; declare the loops explicitly to set ${single.length === 1 ? "it" : "them"} per direction`
9610
+ );
9611
+ continue;
9612
+ }
9613
+ const loops = directions.map((direction) => ({ id: loopDirectionId(id, direction), direction }));
9614
+ const named = new Set(directions);
9615
+ const mirrors = [];
9616
+ for (const { id: loopId, direction } of loops) {
9617
+ const flipped = MIRRORED_DIRECTION[direction];
9618
+ if (flipped === direction || named.has(flipped)) continue;
9619
+ named.add(flipped);
9620
+ mirrors.push({ id: loopDirectionId(id, flipped), of: loopId });
9621
+ }
9622
+ const ids = [...loops.map((loop) => loop.id), ...mirrors.map((mirror) => mirror.id)];
9623
+ const collision = ids.find((expandedId) => claimed.has(expandedId));
9624
+ if (collision) {
9625
+ issues.push(`${where}: expands to "${collision}", which is already declared`);
9626
+ continue;
9627
+ }
9628
+ for (const expandedId of ids) claimed.add(expandedId);
9629
+ for (const loop of loops) {
9630
+ expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
9631
+ }
9632
+ for (const mirror of mirrors) {
9633
+ const inherited = {};
9634
+ for (const field of MIRROR_FIELDS) if (field in asset) inherited[field] = asset[field];
9635
+ expanded[mirror.id] = { mirror: mirror.of, ...inherited };
9636
+ }
9637
+ families[id] = ids;
9638
+ }
9639
+ return { raw: { ...raw, assets: expanded }, families, issues };
9640
+ }
9641
+ function expandAssetFilter(ids, families) {
9642
+ if (!families) return [...ids];
9643
+ return [...new Set(ids.flatMap((id) => families[id] ?? [id]))];
9644
+ }
9645
+ function loopShorthandOf(assetId, families) {
9646
+ if (!families) return void 0;
9647
+ return Object.keys(families).find((id) => families[id].includes(assetId));
9648
+ }
9649
+ function isObject2(value) {
9650
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9651
+ }
9652
+
9653
+ // src/manifest.ts
9552
9654
  async function loadManifest(manifestPath) {
9553
9655
  const abs = path13.resolve(manifestPath);
9554
9656
  if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
9555
- const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
9657
+ const expansion = expandLoopDirections(JSON.parse(await readFile6(abs, "utf8")));
9658
+ if (expansion.issues.length) {
9659
+ throw new ProjectError(`Manifest at ${abs} is invalid:
9660
+ ${expansion.issues.map((i) => ` ${i}`).join("\n")}`);
9661
+ }
9662
+ const input = ManifestInputSchema.safeParse(expansion.raw);
9556
9663
  if (!input.success) {
9557
9664
  const issues = formatManifestIssues(input.error.issues);
9558
9665
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -9637,7 +9744,8 @@ ${issues}`);
9637
9744
  throw new ProjectError(`Manifest at ${abs} is invalid:
9638
9745
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
9639
9746
  }
9640
- return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
9747
+ const loopFamilies = Object.keys(expansion.families).length ? { loopFamilies: expansion.families } : {};
9748
+ return { manifest: parsed.data, root: path13.dirname(abs), path: abs, ...loopFamilies };
9641
9749
  }
9642
9750
  function parentAssetId(asset) {
9643
9751
  return asset?.revision?.from ?? asset?.state?.of ?? asset?.animation?.of;
@@ -9721,12 +9829,13 @@ async function resolveSpecs(loaded, filter) {
9721
9829
  );
9722
9830
  }
9723
9831
  }
9724
- for (const unknownAsset of filter?.assets ?? []) {
9832
+ const assetFilter = expandAssetFilter(filter?.assets ?? [], loaded.loopFamilies);
9833
+ for (const unknownAsset of assetFilter) {
9725
9834
  if (!manifest.assets[unknownAsset]) {
9726
9835
  throw new UsageError(`Unknown asset "${unknownAsset}".`);
9727
9836
  }
9728
9837
  }
9729
- const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
9838
+ const requestedAssetIds = new Set(assetFilter.length ? assetFilter : Object.keys(manifest.assets));
9730
9839
  const resolutionAssetIds = new Set(requestedAssetIds);
9731
9840
  for (const assetId of [...requestedAssetIds]) {
9732
9841
  let current = parentAssetId(manifest.assets[assetId]);
@@ -12733,11 +12842,21 @@ function assetStyles(raw, asset) {
12733
12842
  const own = Array.isArray(asset.styles) ? asset.styles : [];
12734
12843
  return own.length ? own : Object.keys(raw.styles ?? {});
12735
12844
  }
12845
+ function declaredAssets(raw) {
12846
+ const expanded = expandLoopDirections(raw).raw;
12847
+ return new Set(Object.keys(expanded.assets ?? {}));
12848
+ }
12849
+ function undeclaredAsset(raw, assetId) {
12850
+ const shorthand = loopShorthandOf(assetId, expandLoopDirections(raw).families);
12851
+ return new ManifestEditError(
12852
+ shorthand ? `asset "${assetId}" is expanded from "${shorthand}"'s animation.directions; edit "${shorthand}" (it applies to every direction), or write this direction out as its own asset to change it alone` : `asset "${assetId}" is not declared by the manifest`
12853
+ );
12854
+ }
12736
12855
  function applyEdit(raw, edit) {
12737
12856
  raw.assets ??= {};
12738
12857
  if (edit.action === "set-source" || edit.action === "clear-source") {
12739
12858
  const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
12740
- if (!asset2) throw new ManifestEditError(`asset "${edit.assetId}" is not declared by the manifest`);
12859
+ if (!asset2) throw undeclaredAsset(raw, edit.assetId);
12741
12860
  if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
12742
12861
  throw new ManifestEditError(`unknown style "${edit.styleId}"`);
12743
12862
  }
@@ -12789,7 +12908,7 @@ function applyEdit(raw, edit) {
12789
12908
  return;
12790
12909
  }
12791
12910
  if (edit.action === "add-asset") {
12792
- if (Object.hasOwn(raw.assets, edit.assetId)) {
12911
+ if (declaredAssets(raw).has(edit.assetId)) {
12793
12912
  throw new ManifestEditError(`asset "${edit.assetId}" already exists`);
12794
12913
  }
12795
12914
  const { styles, tags, ...rest } = edit.asset;
@@ -12803,20 +12922,20 @@ function applyEdit(raw, edit) {
12803
12922
  }
12804
12923
  asset2.styles = styles;
12805
12924
  }
12806
- if (edit.asset.revision && !Object.hasOwn(raw.assets, edit.asset.revision.from)) {
12925
+ if (edit.asset.revision && !declaredAssets(raw).has(edit.asset.revision.from)) {
12807
12926
  throw new ManifestEditError(`revision parent "${edit.asset.revision.from}" is not declared by the manifest`);
12808
12927
  }
12809
- if (edit.asset.state && !Object.hasOwn(raw.assets, edit.asset.state.of)) {
12928
+ if (edit.asset.state && !declaredAssets(raw).has(edit.asset.state.of)) {
12810
12929
  throw new ManifestEditError(`state parent "${edit.asset.state.of}" is not declared by the manifest`);
12811
12930
  }
12812
- if (edit.asset.animation && !Object.hasOwn(raw.assets, edit.asset.animation.of)) {
12931
+ if (edit.asset.animation && !declaredAssets(raw).has(edit.asset.animation.of)) {
12813
12932
  throw new ManifestEditError(`animation parent "${edit.asset.animation.of}" is not declared by the manifest`);
12814
12933
  }
12815
12934
  raw.assets[edit.assetId] = asset2;
12816
12935
  return;
12817
12936
  }
12818
12937
  const asset = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
12819
- if (!asset) throw new ManifestEditError(`asset "${edit.assetId}" is not declared by the manifest`);
12938
+ if (!asset) throw undeclaredAsset(raw, edit.assetId);
12820
12939
  const { patch } = edit;
12821
12940
  if (patch.prompt !== void 0) asset.prompt = patch.prompt;
12822
12941
  if (patch.promptForStyle) {
@@ -13604,6 +13723,7 @@ async function describeQuality(media, root, inspection) {
13604
13723
  }
13605
13724
  async function buildGallerySnapshot(opts) {
13606
13725
  const { loaded, specs, lock } = opts;
13726
+ const filter = opts.filter?.assets?.length ? { ...opts.filter, assets: expandAssetFilter(opts.filter.assets, loaded.loopFamilies) } : opts.filter;
13607
13727
  const root = loaded.root;
13608
13728
  const media = /* @__PURE__ */ new Map();
13609
13729
  const plan = await buildPlan(specs, lock);
@@ -13748,7 +13868,7 @@ async function buildGallerySnapshot(opts) {
13748
13868
  for (const [key, entry] of Object.entries(lock.entries)) {
13749
13869
  if (shown.has(key)) continue;
13750
13870
  if (isDeclared(loaded, entry.styleId, entry.assetId)) continue;
13751
- if (!matchesFilter(opts.filter, entry.styleId, entry.assetId)) continue;
13871
+ if (!matchesFilter(filter, entry.styleId, entry.assetId)) continue;
13752
13872
  const outputs = await Promise.all(
13753
13873
  entry.outputs.map(
13754
13874
  (output) => describeOutput(media, root, resolveOutputPath(output.path, root), output)
@@ -13914,7 +14034,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
13914
14034
  try {
13915
14035
  const loaded = await loadManifest(manifestPath);
13916
14036
  const styleFilter = filter.styles.filter((id) => loaded.manifest.styles[id]);
13917
- const assetFilter = filter.assets.filter((id) => loaded.manifest.assets[id]);
14037
+ const assetFilter = expandAssetFilter(filter.assets, loaded.loopFamilies).filter((id) => loaded.manifest.assets[id]);
13918
14038
  const excluded = filter.styles.length > 0 && styleFilter.length === 0 || filter.assets.length > 0 && assetFilter.length === 0;
13919
14039
  const lock = await loadLock(lockPath);
13920
14040
  let projectItems = [];