pixelkiln 0.90.0 → 0.91.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
@@ -284,6 +284,7 @@ __export(index_exports, {
284
284
  readVfxConversation: () => readVfxConversation,
285
285
  readVfxRecipe: () => readVfxRecipe,
286
286
  recipeDigest: () => recipeDigest,
287
+ recordedAnimationGroup: () => recordedAnimationGroup,
287
288
  referencedHashes: () => referencedHashes,
288
289
  refineAsset: () => refineAsset,
289
290
  refineFrameSet: () => refineFrameSet,
@@ -692,6 +693,15 @@ var CharacterAnimationSchema = import_zod.z.object({
692
693
  * `src/loop-directions.ts`.
693
694
  */
694
695
  directions: import_zod.z.array(CharacterDirectionSchema).min(1).optional(),
696
+ /**
697
+ * A sentence loop (`mode: "skeleton-v3"`, no `template`) of another
698
+ * direction whose motion this one shares: the asset id of the loop that
699
+ * leads. This loop is then submitted after it, into its animation group,
700
+ * so PixelLab draws it from the same 3D motion. Written for you when
701
+ * `directions` expands (the first direction leads); set by hand only when
702
+ * declaring the loops explicitly.
703
+ */
704
+ motionFrom: import_zod.z.string().min(1).optional(),
695
705
  /**
696
706
  * Frames to generate without a template. A v3 loop takes an even 4 to 16;
697
707
  * a sentence loop (`mode: "skeleton-v3"` with no `template`) takes 3 to 15.
@@ -779,6 +789,9 @@ var CharacterAnimationSchema = import_zod.z.object({
779
789
  if (!animation.template && animation.mode === "template") {
780
790
  context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
781
791
  }
792
+ if (animation.motionFrom !== void 0 && !sentence) {
793
+ context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "motionFrom applies to a skeleton-v3 loop from a sentence (no template)", path: ["motionFrom"] });
794
+ }
782
795
  if (sentence && animation.subject !== void 0) {
783
796
  context.addIssue({ code: import_zod.z.ZodIssueCode.custom, message: "subject does not apply to a skeleton-v3 loop from a sentence; describe the motion in the prompt", path: ["subject"] });
784
797
  }
@@ -1711,6 +1724,7 @@ var LockTerrainConnectSchema = import_zod.z.record(
1711
1724
  TerrainSideSchema,
1712
1725
  import_zod.z.object({ sourceAssetId: import_zod.z.string().min(1), sourceObjectId: import_zod.z.string().min(1) }).strict()
1713
1726
  );
1727
+ var LockMotionShareSchema = import_zod.z.object({ sourceAssetId: import_zod.z.string().min(1), animationGroupId: import_zod.z.string().min(1).nullable() }).strict();
1714
1728
  var LockTerrainSettingsSchema = import_zod.z.object({
1715
1729
  terrainMode: import_zod.z.string().optional(),
1716
1730
  terrainShapeStyle: import_zod.z.string().optional(),
@@ -1747,6 +1761,8 @@ var LockHistoryEntrySchema = import_zod.z.object({
1747
1761
  postprocess: PostprocessSchema.optional(),
1748
1762
  /** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
1749
1763
  terrainConnect: LockTerrainConnectSchema.optional(),
1764
+ /** A sentence loop that shares another direction's motion; see LockEntrySchema.motionShare. */
1765
+ motionShare: LockMotionShareSchema.optional(),
1750
1766
  /** A terrain set's settings outside specHash; see LockEntrySchema.terrainSettings. */
1751
1767
  terrainSettings: LockTerrainSettingsSchema.optional(),
1752
1768
  /** When the replacement was submitted. */
@@ -1823,6 +1839,16 @@ var LockEntrySchema = import_zod.z.object({
1823
1839
  * Absent (not null) otherwise, so other entries do not grow the key.
1824
1840
  */
1825
1841
  terrainConnect: LockTerrainConnectSchema.optional(),
1842
+ /**
1843
+ * For a sentence loop of a later direction: the loop whose motion it
1844
+ * shares and the animation group of that loop's generation this was
1845
+ * drawn into when submitted. Tracked the way `terrainConnect` is, not
1846
+ * through `specHash`: the group lives in the lockfile. A regenerated leader
1847
+ * has a new group, so the plan marks this stale. `animationGroupId` is
1848
+ * null when the leader had no group recorded and this was drawn on its own.
1849
+ * Absent otherwise.
1850
+ */
1851
+ motionShare: LockMotionShareSchema.optional(),
1826
1852
  /**
1827
1853
  * For a terrain set: the settings it was submitted with that `specHash`
1828
1854
  * leaves out (see LockTerrainSettingsSchema). The plan marks the entry
@@ -2288,6 +2314,8 @@ var CharacterSubmitSchema = import_zod3.z.object({
2288
2314
  var AnimateSubmitSchema = import_zod3.z.object({
2289
2315
  background_job_ids: import_zod3.z.array(import_zod3.z.string()),
2290
2316
  directions: import_zod3.z.array(import_zod3.z.string()),
2317
+ /** skeleton-v3 only: the group the directions were drawn into. */
2318
+ animation_group_id: import_zod3.z.string().nullable().optional(),
2291
2319
  status: import_zod3.z.string().default("processing"),
2292
2320
  usage: UsageSchema
2293
2321
  }).passthrough();
@@ -3011,7 +3039,10 @@ var PixelLabClient = class {
3011
3039
  * the same job list (plus an `animation_group_id`). With no `template` it
3012
3040
  * animates `actionDescription` as a sentence, 3 to 15 `frameCount` frames
3013
3041
  * written into one 3D motion; measured on a v3 character (8 frames, 64px:
3014
- * 3 generations, frames back at 96x96).
3042
+ * 3 generations, frames back at 96x96). `animationGroupId` (skeleton-v3
3043
+ * only) adds the directions to an existing animation of the character,
3044
+ * drawn from its motion; the group's own name must be sent again as
3045
+ * `animationName`, or the group appears renamed.
3015
3046
  */
3016
3047
  async animateCharacter(args) {
3017
3048
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
@@ -3023,6 +3054,7 @@ var PixelLabClient = class {
3023
3054
  animation_name: args.animationName,
3024
3055
  mode: args.mode,
3025
3056
  directions: args.directions,
3057
+ ...args.mode === "skeleton-v3" && args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
3026
3058
  ...args.template ? { template_animation_id: args.template } : {},
3027
3059
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
3028
3060
  ...args.description ? { description: args.description } : {},
@@ -4395,6 +4427,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
4395
4427
  template: spec.character.template,
4396
4428
  parent: spec.character.parentAssetId,
4397
4429
  parentSha256: spec.character.parentSha256 ?? null,
4430
+ // Which loop leads, not its group: the group is in the lockfile,
4431
+ // and the plan compares it there. Absent for every loop that
4432
+ // shares nothing, so no existing hash moves.
4433
+ motionFrom: spec.character.motionLeader?.assetId,
4398
4434
  state: spec.character.state,
4399
4435
  // Pose images enter as their hashes, never their paths.
4400
4436
  animation: spec.character.animation ? {
@@ -5439,7 +5475,13 @@ function decodeAnimationJob(jobId) {
5439
5475
  try {
5440
5476
  const value = JSON.parse(Buffer.from(jobId.slice("character-animation:".length), "base64url").toString("utf8"));
5441
5477
  if (typeof value?.characterId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
5442
- return { characterId: value.characterId, name: value.name, direction: value.direction, jobIds: Array.isArray(value.jobIds) ? value.jobIds : [] };
5478
+ return {
5479
+ characterId: value.characterId,
5480
+ name: value.name,
5481
+ direction: value.direction,
5482
+ jobIds: Array.isArray(value.jobIds) ? value.jobIds : [],
5483
+ ...typeof value.groupId === "string" && value.groupId ? { groupId: value.groupId } : {}
5484
+ };
5443
5485
  }
5444
5486
  } catch {
5445
5487
  }
@@ -6277,11 +6319,18 @@ var PixelLabProvider = class _PixelLabProvider {
6277
6319
  };
6278
6320
  }
6279
6321
  const animation = character.animation;
6280
- const name = characterAnimationName(spec);
6322
+ const ownName = characterAnimationName(spec);
6323
+ const shared = context?.motionGroup;
6324
+ const name = shared?.animationName ?? ownName;
6281
6325
  const replaced = context?.replacedMetadata?.character ?? {};
6282
6326
  const existing = await this.client.getCharacter(parentId);
6327
+ if (shared && !existing.animations.some((group) => group.animation_group_id === shared.animationGroupId && group.directions.some((d) => d.direction !== animation.direction))) {
6328
+ throw new Error(
6329
+ `${spec.styleId}/${spec.assetId} shares the motion of an animation group (${shared.animationGroupId}) that is no longer on the character; regenerate the leading direction first`
6330
+ );
6331
+ }
6283
6332
  for (const group of existing.animations) {
6284
- const ours = group.animation_group_id && (group.animation_group_id === replaced.animationGroupId || group.display_name === name || replaced.animationId && group.directions.some((d) => d.frames.some((url) => url.includes(`/animations/${replaced.animationId}/`))));
6333
+ const ours = group.animation_group_id && (group.animation_group_id === replaced.animationGroupId || group.display_name === name || group.display_name === ownName || replaced.animationId && group.directions.some((d) => d.frames.some((url) => url.includes(`/animations/${replaced.animationId}/`))));
6285
6334
  if (!ours || !group.animation_group_id) continue;
6286
6335
  if (group.directions.some((d) => d.direction === animation.direction)) {
6287
6336
  await this.client.deleteCharacterAnimations(parentId, { animationGroupId: group.animation_group_id }, animation.direction);
@@ -6296,6 +6345,7 @@ var PixelLabProvider = class _PixelLabProvider {
6296
6345
  frameCount: animation.frames,
6297
6346
  keepFirstFrame: animation.keepFirstFrame,
6298
6347
  directions: [animation.direction],
6348
+ ...shared ? { animationGroupId: shared.animationGroupId } : {},
6299
6349
  seed: spec.seed,
6300
6350
  textGuidanceScale: character.textGuidanceScale,
6301
6351
  isometric: character.isometric,
@@ -6308,10 +6358,11 @@ var PixelLabProvider = class _PixelLabProvider {
6308
6358
  enhancePrompt: animation.enhancePrompt,
6309
6359
  paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
6310
6360
  });
6311
- const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
6361
+ const groupId = res.animation_group_id ?? shared?.animationGroupId;
6362
+ const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids, ...groupId ? { groupId } : {} };
6312
6363
  return {
6313
6364
  jobId: encodeAnimationJob(job),
6314
- metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
6365
+ metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids, ...groupId ? { animationGroupId: groupId } : {} } }
6315
6366
  };
6316
6367
  }
6317
6368
  /**
@@ -6526,13 +6577,13 @@ var PixelLabProvider = class _PixelLabProvider {
6526
6577
  const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
6527
6578
  if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
6528
6579
  const character2 = await this.client.getCharacter(job.characterId);
6529
- const group = findAnimation(character2, job.name, job.direction, animationId);
6530
- return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
6580
+ const group = findAnimation(character2, job.name, job.direction, animationId, job.groupId);
6581
+ return review(frames, animationId, group?.groupId ?? job.groupId ?? null, billedFromUsage(status.usage));
6531
6582
  }
6532
6583
  }
6533
6584
  const recorded = context?.metadata?.character?.animationId ?? null;
6534
6585
  const character = await this.client.getCharacter(job.characterId);
6535
- const found = findAnimation(character, job.name, job.direction, recorded);
6586
+ const found = findAnimation(character, job.name, job.direction, recorded, job.groupId);
6536
6587
  if (found) return review(found.frames, found.animationId, found.groupId, null);
6537
6588
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
6538
6589
  }
@@ -7966,10 +8017,11 @@ function rotationSources(character) {
7966
8017
  }
7967
8018
  return sources;
7968
8019
  }
7969
- function findAnimation(character, name, direction, animationId) {
8020
+ function findAnimation(character, name, direction, animationId, groupId) {
7970
8021
  for (const group of character.animations) {
7971
8022
  const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
7972
8023
  if (!match) continue;
8024
+ if (groupId && group.animation_group_id !== groupId) continue;
7973
8025
  const byName = group.display_name === name || group.animation_type === name;
7974
8026
  const byId = animationId !== null && match.frames.some((url) => url.includes(`/animations/${animationId}/`));
7975
8027
  if (!byName && !byId) continue;
@@ -10203,8 +10255,16 @@ function expandLoopDirections(raw) {
10203
10255
  continue;
10204
10256
  }
10205
10257
  for (const expandedId of ids) claimed.add(expandedId);
10206
- for (const loop of loops) {
10207
- expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
10258
+ const sentence = animation.mode === "skeleton-v3" && !animation.template;
10259
+ for (const [index, loop] of loops.entries()) {
10260
+ expanded[loop.id] = {
10261
+ ...asset,
10262
+ animation: {
10263
+ ...animation,
10264
+ direction: loop.direction,
10265
+ ...sentence && index > 0 ? { motionFrom: loops[0].id } : {}
10266
+ }
10267
+ };
10208
10268
  }
10209
10269
  for (const mirror of mirrors) {
10210
10270
  const inherited = {};
@@ -11139,7 +11199,8 @@ async function resolveSpecs(loaded, filter) {
11139
11199
  `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
11140
11200
  );
11141
11201
  }
11142
- resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
11202
+ const { motionLeader: _motionLeader, ...flipped } = sourceSpec.character;
11203
+ resolved.character = { ...flipped, animation: { ...sourceSpec.character.animation, direction: mirrored } };
11143
11204
  } else if (sourceSpec.character) {
11144
11205
  resolved.character = { ...sourceSpec.character };
11145
11206
  }
@@ -11196,6 +11257,26 @@ async function resolveSpecs(loaded, filter) {
11196
11257
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
11197
11258
  };
11198
11259
  }
11260
+ if (resolved.character?.animation && asset.animation?.motionFrom) {
11261
+ const leaderId = asset.animation.motionFrom;
11262
+ const where = `assets.${assetId}.animation.motionFrom`;
11263
+ if (leaderId === assetId) throw new Error(`${where}: an asset cannot share its own motion`);
11264
+ const leader = await finalize(leaderId);
11265
+ const led = leader.character?.animation;
11266
+ if (leader.character?.kind !== "animation" || leader.mirror || !led || led.mode !== "skeleton-v3" || led.template) {
11267
+ throw new Error(`${where}: ${leaderId} is not a skeleton-v3 loop from a sentence`);
11268
+ }
11269
+ if (leader.character.parentAssetId !== resolved.character.parentAssetId) {
11270
+ throw new Error(`${where}: ${leaderId} animates ${leader.character.parentAssetId}, not ${resolved.character.parentAssetId}`);
11271
+ }
11272
+ if (leader.character.motionLeader) {
11273
+ throw new Error(`${where}: ${leaderId} itself shares ${leader.character.motionLeader.assetId}'s motion; point at the loop that leads`);
11274
+ }
11275
+ if (led.direction === resolved.character.animation.direction) {
11276
+ throw new Error(`${where}: ${leaderId} is drawn in the same direction; sharing a motion needs another direction`);
11277
+ }
11278
+ resolved.character = { ...resolved.character, motionLeader: { assetId: leaderId, spec: leader } };
11279
+ }
11199
11280
  if (resolved.objectPro && characterParent) {
11200
11281
  const parentSpec = await finalize(characterParent);
11201
11282
  if (!parentSpec.objectPro) {
@@ -13190,7 +13271,10 @@ var import_node_path14 = __toESM(require("path"), 1);
13190
13271
  async function inspectRevisionReadiness(spec, lock) {
13191
13272
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
13192
13273
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
13193
- if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
13274
+ if (spec.character?.parentSpec) {
13275
+ const parent = await inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
13276
+ return parent.ready && spec.character.motionLeader ? inspectMotionLeader(spec, lock, /* @__PURE__ */ new Set()) : parent;
13277
+ }
13194
13278
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
13195
13279
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
13196
13280
  if (spec.scene) return inspectScene(spec);
@@ -13225,6 +13309,17 @@ async function inspectTerrainConnect(spec, lock, seen) {
13225
13309
  }
13226
13310
  return { ready: true, reason: "connected sets are current" };
13227
13311
  }
13312
+ function recordedAnimationGroup(entry) {
13313
+ const character = entry?.providerMetadata?.[entry.provider]?.character;
13314
+ const text = (value) => typeof value === "string" && value ? value : null;
13315
+ return { animationGroupId: text(character?.animationGroupId), animationName: text(character?.animationName) };
13316
+ }
13317
+ async function inspectMotionLeader(spec, lock, seen) {
13318
+ const leader = spec.character.motionLeader;
13319
+ const label = `motion leader ${spec.styleId}/${leader.assetId}`;
13320
+ const dependency = await inspectParent(leader.spec, lock, seen);
13321
+ return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
13322
+ }
13228
13323
  async function inspectScene(spec) {
13229
13324
  const scene = spec.scene;
13230
13325
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
@@ -13383,6 +13478,10 @@ async function inspectParent(spec, lock, seen) {
13383
13478
  const inputs = await inspectCharacterParent(spec, lock, nextSeen);
13384
13479
  if (!inputs.ready) return inputs;
13385
13480
  }
13481
+ if (spec.character?.motionLeader) {
13482
+ const inputs = await inspectMotionLeader(spec, lock, nextSeen);
13483
+ if (!inputs.ready) return inputs;
13484
+ }
13386
13485
  if (spec.character?.styleAnchor) {
13387
13486
  const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
13388
13487
  if (!inputs.ready) return inputs;
@@ -13439,6 +13538,7 @@ function dependencies(spec) {
13439
13538
  spec.mirror?.sourceSpec,
13440
13539
  spec.revision?.sourceSpec,
13441
13540
  spec.character?.parentSpec,
13541
+ spec.character?.motionLeader?.spec,
13442
13542
  spec.character?.styleAnchor?.spec,
13443
13543
  spec.objectPro?.parentSpec,
13444
13544
  spec.terrainConnect?.lower?.spec,
@@ -13455,6 +13555,12 @@ function changedTerrainConnection(spec, entry, lock) {
13455
13555
  }
13456
13556
  return null;
13457
13557
  }
13558
+ function changedMotionLeader(spec, entry, lock) {
13559
+ const leader = spec.character?.motionLeader;
13560
+ if (!leader) return null;
13561
+ const current = recordedAnimationGroup(lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)]).animationGroupId;
13562
+ return (entry.motionShare?.animationGroupId ?? null) === current ? null : leader.assetId;
13563
+ }
13458
13564
  function changedTerrainSettings(spec, entry) {
13459
13565
  const recorded = entry.terrainSettings;
13460
13566
  const current = terrainSettings(spec);
@@ -13496,6 +13602,7 @@ async function buildPlan(specs, lock, opts = {}) {
13496
13602
  let state;
13497
13603
  let reason;
13498
13604
  const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
13605
+ const changedMotion = entry ? changedMotionLeader(spec, entry, lock) : null;
13499
13606
  const changedSettings = entry ? changedTerrainSettings(spec, entry) : null;
13500
13607
  const revision = await inspectRevisionReadiness(spec, lock);
13501
13608
  if (revision && !revision.ready) {
@@ -13535,6 +13642,9 @@ async function buildPlan(specs, lock, opts = {}) {
13535
13642
  } else if (changedConnection) {
13536
13643
  state = "stale";
13537
13644
  reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
13645
+ } else if (changedMotion) {
13646
+ state = "stale";
13647
+ reason = `shares motion with ${changedMotion}, which was regenerated; draw this direction into its new motion`;
13538
13648
  } else if (changedSettings) {
13539
13649
  state = "stale";
13540
13650
  reason = changedSettings;
@@ -13598,6 +13708,14 @@ async function buildPlan(specs, lock, opts = {}) {
13598
13708
  }
13599
13709
  }
13600
13710
  }
13711
+ for (const item of items) {
13712
+ const leader = item.spec.character?.motionLeader;
13713
+ if (!leader || !isActionable(item)) continue;
13714
+ const leaderEntry = lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)];
13715
+ if (leaderEntry?.status === "downloaded" && !recordedAnimationGroup(leaderEntry).animationGroupId) {
13716
+ item.reason += `; ${leader.assetId} has no animation group recorded, so this is drawn on its own and its motion will differ`;
13717
+ }
13718
+ }
13601
13719
  const actionable = items.filter(isActionable);
13602
13720
  const groupsByProvider = /* @__PURE__ */ new Map();
13603
13721
  for (const item of actionable) {
@@ -15007,6 +15125,7 @@ function retireGeneration(entry, retiredAt) {
15007
15125
  provider: entry.provider,
15008
15126
  postprocess: entry.postprocess,
15009
15127
  ...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
15128
+ ...entry.motionShare ? { motionShare: entry.motionShare } : {},
15010
15129
  ...entry.terrainSettings ? { terrainSettings: entry.terrainSettings } : {},
15011
15130
  retiredAt
15012
15131
  };
@@ -15102,6 +15221,8 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
15102
15221
  // A connected set comes back with the connection it was drawn from, so
15103
15222
  // the plan compares the restored generation, not the one it replaced.
15104
15223
  terrainConnect: generation.terrainConnect,
15224
+ // Likewise a loop that shared another direction's motion.
15225
+ motionShare: generation.motionShare,
15105
15226
  // Likewise its settings; a generation from before they were recorded
15106
15227
  // comes back with none and is taken as current.
15107
15228
  terrainSettings: generation.terrainSettings,
@@ -16141,6 +16262,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
16141
16262
  };
16142
16263
  terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
16143
16264
  }
16265
+ const motionLeader = spec.character?.motionLeader;
16266
+ const leaderGroup = motionLeader ? recordedAnimationGroup(lock.entries[lockKey(motionLeader.spec.styleId, motionLeader.spec.assetId)]) : void 0;
16267
+ const motionGroup = leaderGroup?.animationGroupId && leaderGroup.animationName ? { animationGroupId: leaderGroup.animationGroupId, animationName: leaderGroup.animationName } : void 0;
16268
+ if (motionLeader && !motionGroup) {
16269
+ log(` ${key}: ${motionLeader.assetId} has no animation group recorded; drawing this direction on its own`);
16270
+ }
16144
16271
  upsert(lock, key, {
16145
16272
  styleId: spec.styleId,
16146
16273
  assetId: spec.assetId,
@@ -16182,6 +16309,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
16182
16309
  referenceSha256: spec.character.outfit.reference.sha256
16183
16310
  } : null,
16184
16311
  terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
16312
+ motionShare: motionLeader ? { sourceAssetId: motionLeader.assetId, animationGroupId: motionGroup?.animationGroupId ?? null } : void 0,
16185
16313
  // The terrain knobs specHash leaves out, so the plan can tell a
16186
16314
  // changed one; undefined (no key) for every other generator.
16187
16315
  terrainSettings: terrainSettings(spec),
@@ -16232,6 +16360,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
16232
16360
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
16233
16361
  ...parentOutputs ? { parentOutputs } : {},
16234
16362
  ...spec.terrainConnect ? { terrainLinks } : {},
16363
+ ...motionGroup ? { motionGroup } : {},
16235
16364
  ...replacedMetadata ? { replacedMetadata } : {},
16236
16365
  ...lastGenerationMetadata ? { lastGenerationMetadata } : {},
16237
16366
  ...previousMetadata ? { previousMetadata } : {},
@@ -21424,6 +21553,7 @@ async function workspaceStatus(ws, dir) {
21424
21553
  readVfxConversation,
21425
21554
  readVfxRecipe,
21426
21555
  recipeDigest,
21556
+ recordedAnimationGroup,
21427
21557
  referencedHashes,
21428
21558
  refineAsset,
21429
21559
  refineFrameSet,