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/cli.js CHANGED
@@ -807,6 +807,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
807
807
  template: spec.character.template,
808
808
  parent: spec.character.parentAssetId,
809
809
  parentSha256: spec.character.parentSha256 ?? null,
810
+ // Which loop leads, not its group: the group is in the lockfile,
811
+ // and the plan compares it there. Absent for every loop that
812
+ // shares nothing, so no existing hash moves.
813
+ motionFrom: spec.character.motionLeader?.assetId,
810
814
  state: spec.character.state,
811
815
  // Pose images enter as their hashes, never their paths.
812
816
  animation: spec.character.animation ? {
@@ -1238,6 +1242,15 @@ var CharacterAnimationSchema = z.object({
1238
1242
  * `src/loop-directions.ts`.
1239
1243
  */
1240
1244
  directions: z.array(CharacterDirectionSchema).min(1).optional(),
1245
+ /**
1246
+ * A sentence loop (`mode: "skeleton-v3"`, no `template`) of another
1247
+ * direction whose motion this one shares: the asset id of the loop that
1248
+ * leads. This loop is then submitted after it, into its animation group,
1249
+ * so PixelLab draws it from the same 3D motion. Written for you when
1250
+ * `directions` expands (the first direction leads); set by hand only when
1251
+ * declaring the loops explicitly.
1252
+ */
1253
+ motionFrom: z.string().min(1).optional(),
1241
1254
  /**
1242
1255
  * Frames to generate without a template. A v3 loop takes an even 4 to 16;
1243
1256
  * a sentence loop (`mode: "skeleton-v3"` with no `template`) takes 3 to 15.
@@ -1325,6 +1338,9 @@ var CharacterAnimationSchema = z.object({
1325
1338
  if (!animation.template && animation.mode === "template") {
1326
1339
  context.addIssue({ code: z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
1327
1340
  }
1341
+ if (animation.motionFrom !== void 0 && !sentence) {
1342
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "motionFrom applies to a skeleton-v3 loop from a sentence (no template)", path: ["motionFrom"] });
1343
+ }
1328
1344
  if (sentence && animation.subject !== void 0) {
1329
1345
  context.addIssue({ code: z.ZodIssueCode.custom, message: "subject does not apply to a skeleton-v3 loop from a sentence; describe the motion in the prompt", path: ["subject"] });
1330
1346
  }
@@ -2257,6 +2273,7 @@ var LockTerrainConnectSchema = z.record(
2257
2273
  TerrainSideSchema,
2258
2274
  z.object({ sourceAssetId: z.string().min(1), sourceObjectId: z.string().min(1) }).strict()
2259
2275
  );
2276
+ var LockMotionShareSchema = z.object({ sourceAssetId: z.string().min(1), animationGroupId: z.string().min(1).nullable() }).strict();
2260
2277
  var LockTerrainSettingsSchema = z.object({
2261
2278
  terrainMode: z.string().optional(),
2262
2279
  terrainShapeStyle: z.string().optional(),
@@ -2293,6 +2310,8 @@ var LockHistoryEntrySchema = z.object({
2293
2310
  postprocess: PostprocessSchema.optional(),
2294
2311
  /** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
2295
2312
  terrainConnect: LockTerrainConnectSchema.optional(),
2313
+ /** A sentence loop that shares another direction's motion; see LockEntrySchema.motionShare. */
2314
+ motionShare: LockMotionShareSchema.optional(),
2296
2315
  /** A terrain set's settings outside specHash; see LockEntrySchema.terrainSettings. */
2297
2316
  terrainSettings: LockTerrainSettingsSchema.optional(),
2298
2317
  /** When the replacement was submitted. */
@@ -2369,6 +2388,16 @@ var LockEntrySchema = z.object({
2369
2388
  * Absent (not null) otherwise, so other entries do not grow the key.
2370
2389
  */
2371
2390
  terrainConnect: LockTerrainConnectSchema.optional(),
2391
+ /**
2392
+ * For a sentence loop of a later direction: the loop whose motion it
2393
+ * shares and the animation group of that loop's generation this was
2394
+ * drawn into when submitted. Tracked the way `terrainConnect` is, not
2395
+ * through `specHash`: the group lives in the lockfile. A regenerated leader
2396
+ * has a new group, so the plan marks this stale. `animationGroupId` is
2397
+ * null when the leader had no group recorded and this was drawn on its own.
2398
+ * Absent otherwise.
2399
+ */
2400
+ motionShare: LockMotionShareSchema.optional(),
2372
2401
  /**
2373
2402
  * For a terrain set: the settings it was submitted with that `specHash`
2374
2403
  * leaves out (see LockTerrainSettingsSchema). The plan marks the entry
@@ -4695,7 +4724,10 @@ import path8 from "path";
4695
4724
  async function inspectRevisionReadiness(spec, lock) {
4696
4725
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
4697
4726
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
4698
- if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
4727
+ if (spec.character?.parentSpec) {
4728
+ const parent = await inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
4729
+ return parent.ready && spec.character.motionLeader ? inspectMotionLeader(spec, lock, /* @__PURE__ */ new Set()) : parent;
4730
+ }
4699
4731
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
4700
4732
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
4701
4733
  if (spec.scene) return inspectScene(spec);
@@ -4730,6 +4762,17 @@ async function inspectTerrainConnect(spec, lock, seen) {
4730
4762
  }
4731
4763
  return { ready: true, reason: "connected sets are current" };
4732
4764
  }
4765
+ function recordedAnimationGroup(entry) {
4766
+ const character = entry?.providerMetadata?.[entry.provider]?.character;
4767
+ const text = (value) => typeof value === "string" && value ? value : null;
4768
+ return { animationGroupId: text(character?.animationGroupId), animationName: text(character?.animationName) };
4769
+ }
4770
+ async function inspectMotionLeader(spec, lock, seen) {
4771
+ const leader = spec.character.motionLeader;
4772
+ const label = `motion leader ${spec.styleId}/${leader.assetId}`;
4773
+ const dependency = await inspectParent(leader.spec, lock, seen);
4774
+ return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
4775
+ }
4733
4776
  async function inspectScene(spec) {
4734
4777
  const scene = spec.scene;
4735
4778
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
@@ -4888,6 +4931,10 @@ async function inspectParent(spec, lock, seen) {
4888
4931
  const inputs = await inspectCharacterParent(spec, lock, nextSeen);
4889
4932
  if (!inputs.ready) return inputs;
4890
4933
  }
4934
+ if (spec.character?.motionLeader) {
4935
+ const inputs = await inspectMotionLeader(spec, lock, nextSeen);
4936
+ if (!inputs.ready) return inputs;
4937
+ }
4891
4938
  if (spec.character?.styleAnchor) {
4892
4939
  const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
4893
4940
  if (!inputs.ready) return inputs;
@@ -4944,6 +4991,7 @@ function dependencies(spec) {
4944
4991
  spec.mirror?.sourceSpec,
4945
4992
  spec.revision?.sourceSpec,
4946
4993
  spec.character?.parentSpec,
4994
+ spec.character?.motionLeader?.spec,
4947
4995
  spec.character?.styleAnchor?.spec,
4948
4996
  spec.objectPro?.parentSpec,
4949
4997
  spec.terrainConnect?.lower?.spec,
@@ -4960,6 +5008,12 @@ function changedTerrainConnection(spec, entry, lock) {
4960
5008
  }
4961
5009
  return null;
4962
5010
  }
5011
+ function changedMotionLeader(spec, entry, lock) {
5012
+ const leader = spec.character?.motionLeader;
5013
+ if (!leader) return null;
5014
+ const current = recordedAnimationGroup(lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)]).animationGroupId;
5015
+ return (entry.motionShare?.animationGroupId ?? null) === current ? null : leader.assetId;
5016
+ }
4963
5017
  function changedTerrainSettings(spec, entry) {
4964
5018
  const recorded = entry.terrainSettings;
4965
5019
  const current = terrainSettings(spec);
@@ -5001,6 +5055,7 @@ async function buildPlan(specs, lock, opts = {}) {
5001
5055
  let state;
5002
5056
  let reason;
5003
5057
  const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
5058
+ const changedMotion = entry ? changedMotionLeader(spec, entry, lock) : null;
5004
5059
  const changedSettings = entry ? changedTerrainSettings(spec, entry) : null;
5005
5060
  const revision = await inspectRevisionReadiness(spec, lock);
5006
5061
  if (revision && !revision.ready) {
@@ -5040,6 +5095,9 @@ async function buildPlan(specs, lock, opts = {}) {
5040
5095
  } else if (changedConnection) {
5041
5096
  state = "stale";
5042
5097
  reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
5098
+ } else if (changedMotion) {
5099
+ state = "stale";
5100
+ reason = `shares motion with ${changedMotion}, which was regenerated; draw this direction into its new motion`;
5043
5101
  } else if (changedSettings) {
5044
5102
  state = "stale";
5045
5103
  reason = changedSettings;
@@ -5103,6 +5161,14 @@ async function buildPlan(specs, lock, opts = {}) {
5103
5161
  }
5104
5162
  }
5105
5163
  }
5164
+ for (const item of items) {
5165
+ const leader = item.spec.character?.motionLeader;
5166
+ if (!leader || !isActionable(item)) continue;
5167
+ const leaderEntry = lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)];
5168
+ if (leaderEntry?.status === "downloaded" && !recordedAnimationGroup(leaderEntry).animationGroupId) {
5169
+ item.reason += `; ${leader.assetId} has no animation group recorded, so this is drawn on its own and its motion will differ`;
5170
+ }
5171
+ }
5106
5172
  const actionable = items.filter(isActionable);
5107
5173
  const groupsByProvider = /* @__PURE__ */ new Map();
5108
5174
  for (const item of actionable) {
@@ -6554,6 +6620,8 @@ var CharacterSubmitSchema = z3.object({
6554
6620
  var AnimateSubmitSchema = z3.object({
6555
6621
  background_job_ids: z3.array(z3.string()),
6556
6622
  directions: z3.array(z3.string()),
6623
+ /** skeleton-v3 only: the group the directions were drawn into. */
6624
+ animation_group_id: z3.string().nullable().optional(),
6557
6625
  status: z3.string().default("processing"),
6558
6626
  usage: UsageSchema
6559
6627
  }).passthrough();
@@ -7277,7 +7345,10 @@ var PixelLabClient = class {
7277
7345
  * the same job list (plus an `animation_group_id`). With no `template` it
7278
7346
  * animates `actionDescription` as a sentence, 3 to 15 `frameCount` frames
7279
7347
  * written into one 3D motion; measured on a v3 character (8 frames, 64px:
7280
- * 3 generations, frames back at 96x96).
7348
+ * 3 generations, frames back at 96x96). `animationGroupId` (skeleton-v3
7349
+ * only) adds the directions to an existing animation of the character,
7350
+ * drawn from its motion; the group's own name must be sent again as
7351
+ * `animationName`, or the group appears renamed.
7281
7352
  */
7282
7353
  async animateCharacter(args) {
7283
7354
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
@@ -7289,6 +7360,7 @@ var PixelLabClient = class {
7289
7360
  animation_name: args.animationName,
7290
7361
  mode: args.mode,
7291
7362
  directions: args.directions,
7363
+ ...args.mode === "skeleton-v3" && args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
7292
7364
  ...args.template ? { template_animation_id: args.template } : {},
7293
7365
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
7294
7366
  ...args.description ? { description: args.description } : {},
@@ -8973,7 +9045,13 @@ function decodeAnimationJob(jobId) {
8973
9045
  try {
8974
9046
  const value = JSON.parse(Buffer.from(jobId.slice("character-animation:".length), "base64url").toString("utf8"));
8975
9047
  if (typeof value?.characterId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
8976
- return { characterId: value.characterId, name: value.name, direction: value.direction, jobIds: Array.isArray(value.jobIds) ? value.jobIds : [] };
9048
+ return {
9049
+ characterId: value.characterId,
9050
+ name: value.name,
9051
+ direction: value.direction,
9052
+ jobIds: Array.isArray(value.jobIds) ? value.jobIds : [],
9053
+ ...typeof value.groupId === "string" && value.groupId ? { groupId: value.groupId } : {}
9054
+ };
8977
9055
  }
8978
9056
  } catch {
8979
9057
  }
@@ -9811,11 +9889,18 @@ var PixelLabProvider = class _PixelLabProvider {
9811
9889
  };
9812
9890
  }
9813
9891
  const animation = character.animation;
9814
- const name = characterAnimationName(spec);
9892
+ const ownName = characterAnimationName(spec);
9893
+ const shared = context?.motionGroup;
9894
+ const name = shared?.animationName ?? ownName;
9815
9895
  const replaced = context?.replacedMetadata?.character ?? {};
9816
9896
  const existing = await this.client.getCharacter(parentId);
9897
+ if (shared && !existing.animations.some((group) => group.animation_group_id === shared.animationGroupId && group.directions.some((d) => d.direction !== animation.direction))) {
9898
+ throw new Error(
9899
+ `${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`
9900
+ );
9901
+ }
9817
9902
  for (const group of existing.animations) {
9818
- 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}/`))));
9903
+ 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}/`))));
9819
9904
  if (!ours || !group.animation_group_id) continue;
9820
9905
  if (group.directions.some((d) => d.direction === animation.direction)) {
9821
9906
  await this.client.deleteCharacterAnimations(parentId, { animationGroupId: group.animation_group_id }, animation.direction);
@@ -9830,6 +9915,7 @@ var PixelLabProvider = class _PixelLabProvider {
9830
9915
  frameCount: animation.frames,
9831
9916
  keepFirstFrame: animation.keepFirstFrame,
9832
9917
  directions: [animation.direction],
9918
+ ...shared ? { animationGroupId: shared.animationGroupId } : {},
9833
9919
  seed: spec.seed,
9834
9920
  textGuidanceScale: character.textGuidanceScale,
9835
9921
  isometric: character.isometric,
@@ -9842,10 +9928,11 @@ var PixelLabProvider = class _PixelLabProvider {
9842
9928
  enhancePrompt: animation.enhancePrompt,
9843
9929
  paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
9844
9930
  });
9845
- const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
9931
+ const groupId = res.animation_group_id ?? shared?.animationGroupId;
9932
+ const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids, ...groupId ? { groupId } : {} };
9846
9933
  return {
9847
9934
  jobId: encodeAnimationJob(job),
9848
- metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
9935
+ metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids, ...groupId ? { animationGroupId: groupId } : {} } }
9849
9936
  };
9850
9937
  }
9851
9938
  /**
@@ -10060,13 +10147,13 @@ var PixelLabProvider = class _PixelLabProvider {
10060
10147
  const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
10061
10148
  if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
10062
10149
  const character2 = await this.client.getCharacter(job.characterId);
10063
- const group = findAnimation(character2, job.name, job.direction, animationId);
10064
- return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
10150
+ const group = findAnimation(character2, job.name, job.direction, animationId, job.groupId);
10151
+ return review(frames, animationId, group?.groupId ?? job.groupId ?? null, billedFromUsage(status.usage));
10065
10152
  }
10066
10153
  }
10067
10154
  const recorded = context?.metadata?.character?.animationId ?? null;
10068
10155
  const character = await this.client.getCharacter(job.characterId);
10069
- const found = findAnimation(character, job.name, job.direction, recorded);
10156
+ const found = findAnimation(character, job.name, job.direction, recorded, job.groupId);
10070
10157
  if (found) return review(found.frames, found.animationId, found.groupId, null);
10071
10158
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
10072
10159
  }
@@ -11500,10 +11587,11 @@ function rotationSources(character) {
11500
11587
  }
11501
11588
  return sources;
11502
11589
  }
11503
- function findAnimation(character, name, direction, animationId) {
11590
+ function findAnimation(character, name, direction, animationId, groupId) {
11504
11591
  for (const group of character.animations) {
11505
11592
  const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
11506
11593
  if (!match) continue;
11594
+ if (groupId && group.animation_group_id !== groupId) continue;
11507
11595
  const byName = group.display_name === name || group.animation_type === name;
11508
11596
  const byId = animationId !== null && match.frames.some((url) => url.includes(`/animations/${animationId}/`));
11509
11597
  if (!byName && !byId) continue;
@@ -12638,8 +12726,16 @@ function expandLoopDirections(raw) {
12638
12726
  continue;
12639
12727
  }
12640
12728
  for (const expandedId of ids) claimed.add(expandedId);
12641
- for (const loop of loops) {
12642
- expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
12729
+ const sentence = animation.mode === "skeleton-v3" && !animation.template;
12730
+ for (const [index, loop] of loops.entries()) {
12731
+ expanded[loop.id] = {
12732
+ ...asset,
12733
+ animation: {
12734
+ ...animation,
12735
+ direction: loop.direction,
12736
+ ...sentence && index > 0 ? { motionFrom: loops[0].id } : {}
12737
+ }
12738
+ };
12643
12739
  }
12644
12740
  for (const mirror of mirrors) {
12645
12741
  const inherited = {};
@@ -13255,7 +13351,8 @@ async function resolveSpecs(loaded, filter) {
13255
13351
  `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
13256
13352
  );
13257
13353
  }
13258
- resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
13354
+ const { motionLeader: _motionLeader, ...flipped } = sourceSpec.character;
13355
+ resolved.character = { ...flipped, animation: { ...sourceSpec.character.animation, direction: mirrored } };
13259
13356
  } else if (sourceSpec.character) {
13260
13357
  resolved.character = { ...sourceSpec.character };
13261
13358
  }
@@ -13312,6 +13409,26 @@ async function resolveSpecs(loaded, filter) {
13312
13409
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
13313
13410
  };
13314
13411
  }
13412
+ if (resolved.character?.animation && asset.animation?.motionFrom) {
13413
+ const leaderId = asset.animation.motionFrom;
13414
+ const where = `assets.${assetId}.animation.motionFrom`;
13415
+ if (leaderId === assetId) throw new Error(`${where}: an asset cannot share its own motion`);
13416
+ const leader = await finalize(leaderId);
13417
+ const led = leader.character?.animation;
13418
+ if (leader.character?.kind !== "animation" || leader.mirror || !led || led.mode !== "skeleton-v3" || led.template) {
13419
+ throw new Error(`${where}: ${leaderId} is not a skeleton-v3 loop from a sentence`);
13420
+ }
13421
+ if (leader.character.parentAssetId !== resolved.character.parentAssetId) {
13422
+ throw new Error(`${where}: ${leaderId} animates ${leader.character.parentAssetId}, not ${resolved.character.parentAssetId}`);
13423
+ }
13424
+ if (leader.character.motionLeader) {
13425
+ throw new Error(`${where}: ${leaderId} itself shares ${leader.character.motionLeader.assetId}'s motion; point at the loop that leads`);
13426
+ }
13427
+ if (led.direction === resolved.character.animation.direction) {
13428
+ throw new Error(`${where}: ${leaderId} is drawn in the same direction; sharing a motion needs another direction`);
13429
+ }
13430
+ resolved.character = { ...resolved.character, motionLeader: { assetId: leaderId, spec: leader } };
13431
+ }
13315
13432
  if (resolved.objectPro && characterParent) {
13316
13433
  const parentSpec = await finalize(characterParent);
13317
13434
  if (!parentSpec.objectPro) {
@@ -17497,6 +17614,7 @@ function retireGeneration(entry, retiredAt) {
17497
17614
  provider: entry.provider,
17498
17615
  postprocess: entry.postprocess,
17499
17616
  ...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
17617
+ ...entry.motionShare ? { motionShare: entry.motionShare } : {},
17500
17618
  ...entry.terrainSettings ? { terrainSettings: entry.terrainSettings } : {},
17501
17619
  retiredAt
17502
17620
  };
@@ -17592,6 +17710,8 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
17592
17710
  // A connected set comes back with the connection it was drawn from, so
17593
17711
  // the plan compares the restored generation, not the one it replaced.
17594
17712
  terrainConnect: generation.terrainConnect,
17713
+ // Likewise a loop that shared another direction's motion.
17714
+ motionShare: generation.motionShare,
17595
17715
  // Likewise its settings; a generation from before they were recorded
17596
17716
  // comes back with none and is taken as current.
17597
17717
  terrainSettings: generation.terrainSettings,
@@ -18956,6 +19076,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
18956
19076
  };
18957
19077
  terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
18958
19078
  }
19079
+ const motionLeader = spec.character?.motionLeader;
19080
+ const leaderGroup = motionLeader ? recordedAnimationGroup(lock.entries[lockKey(motionLeader.spec.styleId, motionLeader.spec.assetId)]) : void 0;
19081
+ const motionGroup = leaderGroup?.animationGroupId && leaderGroup.animationName ? { animationGroupId: leaderGroup.animationGroupId, animationName: leaderGroup.animationName } : void 0;
19082
+ if (motionLeader && !motionGroup) {
19083
+ log2(` ${key}: ${motionLeader.assetId} has no animation group recorded; drawing this direction on its own`);
19084
+ }
18959
19085
  upsert(lock, key, {
18960
19086
  styleId: spec.styleId,
18961
19087
  assetId: spec.assetId,
@@ -18997,6 +19123,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
18997
19123
  referenceSha256: spec.character.outfit.reference.sha256
18998
19124
  } : null,
18999
19125
  terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
19126
+ motionShare: motionLeader ? { sourceAssetId: motionLeader.assetId, animationGroupId: motionGroup?.animationGroupId ?? null } : void 0,
19000
19127
  // The terrain knobs specHash leaves out, so the plan can tell a
19001
19128
  // changed one; undefined (no key) for every other generator.
19002
19129
  terrainSettings: terrainSettings(spec),
@@ -19047,6 +19174,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
19047
19174
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
19048
19175
  ...parentOutputs ? { parentOutputs } : {},
19049
19176
  ...spec.terrainConnect ? { terrainLinks } : {},
19177
+ ...motionGroup ? { motionGroup } : {},
19050
19178
  ...replacedMetadata ? { replacedMetadata } : {},
19051
19179
  ...lastGenerationMetadata ? { lastGenerationMetadata } : {},
19052
19180
  ...previousMetadata ? { previousMetadata } : {},