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.js CHANGED
@@ -310,6 +310,15 @@ var CharacterAnimationSchema = z.object({
310
310
  * `src/loop-directions.ts`.
311
311
  */
312
312
  directions: z.array(CharacterDirectionSchema).min(1).optional(),
313
+ /**
314
+ * A sentence loop (`mode: "skeleton-v3"`, no `template`) of another
315
+ * direction whose motion this one shares: the asset id of the loop that
316
+ * leads. This loop is then submitted after it, into its animation group,
317
+ * so PixelLab draws it from the same 3D motion. Written for you when
318
+ * `directions` expands (the first direction leads); set by hand only when
319
+ * declaring the loops explicitly.
320
+ */
321
+ motionFrom: z.string().min(1).optional(),
313
322
  /**
314
323
  * Frames to generate without a template. A v3 loop takes an even 4 to 16;
315
324
  * a sentence loop (`mode: "skeleton-v3"` with no `template`) takes 3 to 15.
@@ -397,6 +406,9 @@ var CharacterAnimationSchema = z.object({
397
406
  if (!animation.template && animation.mode === "template") {
398
407
  context.addIssue({ code: z.ZodIssueCode.custom, message: "template mode needs a template", path: ["template"] });
399
408
  }
409
+ if (animation.motionFrom !== void 0 && !sentence) {
410
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "motionFrom applies to a skeleton-v3 loop from a sentence (no template)", path: ["motionFrom"] });
411
+ }
400
412
  if (sentence && animation.subject !== void 0) {
401
413
  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"] });
402
414
  }
@@ -1329,6 +1341,7 @@ var LockTerrainConnectSchema = z.record(
1329
1341
  TerrainSideSchema,
1330
1342
  z.object({ sourceAssetId: z.string().min(1), sourceObjectId: z.string().min(1) }).strict()
1331
1343
  );
1344
+ var LockMotionShareSchema = z.object({ sourceAssetId: z.string().min(1), animationGroupId: z.string().min(1).nullable() }).strict();
1332
1345
  var LockTerrainSettingsSchema = z.object({
1333
1346
  terrainMode: z.string().optional(),
1334
1347
  terrainShapeStyle: z.string().optional(),
@@ -1365,6 +1378,8 @@ var LockHistoryEntrySchema = z.object({
1365
1378
  postprocess: PostprocessSchema.optional(),
1366
1379
  /** A connected terrain set's other sets; see LockEntrySchema.terrainConnect. */
1367
1380
  terrainConnect: LockTerrainConnectSchema.optional(),
1381
+ /** A sentence loop that shares another direction's motion; see LockEntrySchema.motionShare. */
1382
+ motionShare: LockMotionShareSchema.optional(),
1368
1383
  /** A terrain set's settings outside specHash; see LockEntrySchema.terrainSettings. */
1369
1384
  terrainSettings: LockTerrainSettingsSchema.optional(),
1370
1385
  /** When the replacement was submitted. */
@@ -1441,6 +1456,16 @@ var LockEntrySchema = z.object({
1441
1456
  * Absent (not null) otherwise, so other entries do not grow the key.
1442
1457
  */
1443
1458
  terrainConnect: LockTerrainConnectSchema.optional(),
1459
+ /**
1460
+ * For a sentence loop of a later direction: the loop whose motion it
1461
+ * shares and the animation group of that loop's generation this was
1462
+ * drawn into when submitted. Tracked the way `terrainConnect` is, not
1463
+ * through `specHash`: the group lives in the lockfile. A regenerated leader
1464
+ * has a new group, so the plan marks this stale. `animationGroupId` is
1465
+ * null when the leader had no group recorded and this was drawn on its own.
1466
+ * Absent otherwise.
1467
+ */
1468
+ motionShare: LockMotionShareSchema.optional(),
1444
1469
  /**
1445
1470
  * For a terrain set: the settings it was submitted with that `specHash`
1446
1471
  * leaves out (see LockTerrainSettingsSchema). The plan marks the entry
@@ -1906,6 +1931,8 @@ var CharacterSubmitSchema = z3.object({
1906
1931
  var AnimateSubmitSchema = z3.object({
1907
1932
  background_job_ids: z3.array(z3.string()),
1908
1933
  directions: z3.array(z3.string()),
1934
+ /** skeleton-v3 only: the group the directions were drawn into. */
1935
+ animation_group_id: z3.string().nullable().optional(),
1909
1936
  status: z3.string().default("processing"),
1910
1937
  usage: UsageSchema
1911
1938
  }).passthrough();
@@ -2629,7 +2656,10 @@ var PixelLabClient = class {
2629
2656
  * the same job list (plus an `animation_group_id`). With no `template` it
2630
2657
  * animates `actionDescription` as a sentence, 3 to 15 `frameCount` frames
2631
2658
  * written into one 3D motion; measured on a v3 character (8 frames, 64px:
2632
- * 3 generations, frames back at 96x96).
2659
+ * 3 generations, frames back at 96x96). `animationGroupId` (skeleton-v3
2660
+ * only) adds the directions to an existing animation of the character,
2661
+ * drawn from its motion; the group's own name must be sent again as
2662
+ * `animationName`, or the group appears renamed.
2633
2663
  */
2634
2664
  async animateCharacter(args) {
2635
2665
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
@@ -2641,6 +2671,7 @@ var PixelLabClient = class {
2641
2671
  animation_name: args.animationName,
2642
2672
  mode: args.mode,
2643
2673
  directions: args.directions,
2674
+ ...args.mode === "skeleton-v3" && args.animationGroupId ? { animation_group_id: args.animationGroupId } : {},
2644
2675
  ...args.template ? { template_animation_id: args.template } : {},
2645
2676
  ...args.actionDescription ? { action_description: args.actionDescription } : {},
2646
2677
  ...args.description ? { description: args.description } : {},
@@ -4013,6 +4044,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
4013
4044
  template: spec.character.template,
4014
4045
  parent: spec.character.parentAssetId,
4015
4046
  parentSha256: spec.character.parentSha256 ?? null,
4047
+ // Which loop leads, not its group: the group is in the lockfile,
4048
+ // and the plan compares it there. Absent for every loop that
4049
+ // shares nothing, so no existing hash moves.
4050
+ motionFrom: spec.character.motionLeader?.assetId,
4016
4051
  state: spec.character.state,
4017
4052
  // Pose images enter as their hashes, never their paths.
4018
4053
  animation: spec.character.animation ? {
@@ -5057,7 +5092,13 @@ function decodeAnimationJob(jobId) {
5057
5092
  try {
5058
5093
  const value = JSON.parse(Buffer.from(jobId.slice("character-animation:".length), "base64url").toString("utf8"));
5059
5094
  if (typeof value?.characterId === "string" && typeof value.name === "string" && typeof value.direction === "string") {
5060
- return { characterId: value.characterId, name: value.name, direction: value.direction, jobIds: Array.isArray(value.jobIds) ? value.jobIds : [] };
5095
+ return {
5096
+ characterId: value.characterId,
5097
+ name: value.name,
5098
+ direction: value.direction,
5099
+ jobIds: Array.isArray(value.jobIds) ? value.jobIds : [],
5100
+ ...typeof value.groupId === "string" && value.groupId ? { groupId: value.groupId } : {}
5101
+ };
5061
5102
  }
5062
5103
  } catch {
5063
5104
  }
@@ -5895,11 +5936,18 @@ var PixelLabProvider = class _PixelLabProvider {
5895
5936
  };
5896
5937
  }
5897
5938
  const animation = character.animation;
5898
- const name = characterAnimationName(spec);
5939
+ const ownName = characterAnimationName(spec);
5940
+ const shared = context?.motionGroup;
5941
+ const name = shared?.animationName ?? ownName;
5899
5942
  const replaced = context?.replacedMetadata?.character ?? {};
5900
5943
  const existing = await this.client.getCharacter(parentId);
5944
+ if (shared && !existing.animations.some((group) => group.animation_group_id === shared.animationGroupId && group.directions.some((d) => d.direction !== animation.direction))) {
5945
+ throw new Error(
5946
+ `${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`
5947
+ );
5948
+ }
5901
5949
  for (const group of existing.animations) {
5902
- 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}/`))));
5950
+ 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}/`))));
5903
5951
  if (!ours || !group.animation_group_id) continue;
5904
5952
  if (group.directions.some((d) => d.direction === animation.direction)) {
5905
5953
  await this.client.deleteCharacterAnimations(parentId, { animationGroupId: group.animation_group_id }, animation.direction);
@@ -5914,6 +5962,7 @@ var PixelLabProvider = class _PixelLabProvider {
5914
5962
  frameCount: animation.frames,
5915
5963
  keepFirstFrame: animation.keepFirstFrame,
5916
5964
  directions: [animation.direction],
5965
+ ...shared ? { animationGroupId: shared.animationGroupId } : {},
5917
5966
  seed: spec.seed,
5918
5967
  textGuidanceScale: character.textGuidanceScale,
5919
5968
  isometric: character.isometric,
@@ -5926,10 +5975,11 @@ var PixelLabProvider = class _PixelLabProvider {
5926
5975
  enhancePrompt: animation.enhancePrompt,
5927
5976
  paletteSwatchBase64: spec.palette.length ? paletteSwatch(spec.palette).toString("base64") : void 0
5928
5977
  });
5929
- const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids };
5978
+ const groupId = res.animation_group_id ?? shared?.animationGroupId;
5979
+ const job = { characterId: parentId, name, direction: animation.direction, jobIds: res.background_job_ids, ...groupId ? { groupId } : {} };
5930
5980
  return {
5931
5981
  jobId: encodeAnimationJob(job),
5932
- metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids } }
5982
+ metadata: { character: { kind: "animation", characterId: parentId, animationName: name, direction: animation.direction, mode: animation.mode, backgroundJobIds: res.background_job_ids, ...groupId ? { animationGroupId: groupId } : {} } }
5933
5983
  };
5934
5984
  }
5935
5985
  /**
@@ -6144,13 +6194,13 @@ var PixelLabProvider = class _PixelLabProvider {
6144
6194
  const animationId = typeof done.animation_id === "string" ? done.animation_id : null;
6145
6195
  if (Array.isArray(frames) && frames.length && frames.every((f) => typeof f === "string")) {
6146
6196
  const character2 = await this.client.getCharacter(job.characterId);
6147
- const group = findAnimation(character2, job.name, job.direction, animationId);
6148
- return review(frames, animationId, group?.groupId ?? null, billedFromUsage(status.usage));
6197
+ const group = findAnimation(character2, job.name, job.direction, animationId, job.groupId);
6198
+ return review(frames, animationId, group?.groupId ?? job.groupId ?? null, billedFromUsage(status.usage));
6149
6199
  }
6150
6200
  }
6151
6201
  const recorded = context?.metadata?.character?.animationId ?? null;
6152
6202
  const character = await this.client.getCharacter(job.characterId);
6153
- const found = findAnimation(character, job.name, job.direction, recorded);
6203
+ const found = findAnimation(character, job.name, job.direction, recorded, job.groupId);
6154
6204
  if (found) return review(found.frames, found.animationId, found.groupId, null);
6155
6205
  return cleanedUp ? { status: "failed", error: `animation job is gone upstream and no "${job.name}" ${job.direction} animation exists on the character` } : { status: "processing" };
6156
6206
  }
@@ -7584,10 +7634,11 @@ function rotationSources(character) {
7584
7634
  }
7585
7635
  return sources;
7586
7636
  }
7587
- function findAnimation(character, name, direction, animationId) {
7637
+ function findAnimation(character, name, direction, animationId, groupId) {
7588
7638
  for (const group of character.animations) {
7589
7639
  const match = group.directions.find((d) => d.direction === direction && d.frames.length > 0);
7590
7640
  if (!match) continue;
7641
+ if (groupId && group.animation_group_id !== groupId) continue;
7591
7642
  const byName = group.display_name === name || group.animation_type === name;
7592
7643
  const byId = animationId !== null && match.frames.some((url) => url.includes(`/animations/${animationId}/`));
7593
7644
  if (!byName && !byId) continue;
@@ -9821,8 +9872,16 @@ function expandLoopDirections(raw) {
9821
9872
  continue;
9822
9873
  }
9823
9874
  for (const expandedId of ids) claimed.add(expandedId);
9824
- for (const loop of loops) {
9825
- expanded[loop.id] = { ...asset, animation: { ...animation, direction: loop.direction } };
9875
+ const sentence = animation.mode === "skeleton-v3" && !animation.template;
9876
+ for (const [index, loop] of loops.entries()) {
9877
+ expanded[loop.id] = {
9878
+ ...asset,
9879
+ animation: {
9880
+ ...animation,
9881
+ direction: loop.direction,
9882
+ ...sentence && index > 0 ? { motionFrom: loops[0].id } : {}
9883
+ }
9884
+ };
9826
9885
  }
9827
9886
  for (const mirror of mirrors) {
9828
9887
  const inherited = {};
@@ -10757,7 +10816,8 @@ async function resolveSpecs(loaded, filter) {
10757
10816
  `assets.${assetId}: mirroring ${asset.mirror} gives another ${facing}-facing loop; mirror a loop facing east, west, or a diagonal`
10758
10817
  );
10759
10818
  }
10760
- resolved.character = { ...sourceSpec.character, animation: { ...sourceSpec.character.animation, direction: mirrored } };
10819
+ const { motionLeader: _motionLeader, ...flipped } = sourceSpec.character;
10820
+ resolved.character = { ...flipped, animation: { ...sourceSpec.character.animation, direction: mirrored } };
10761
10821
  } else if (sourceSpec.character) {
10762
10822
  resolved.character = { ...sourceSpec.character };
10763
10823
  }
@@ -10814,6 +10874,26 @@ async function resolveSpecs(loaded, filter) {
10814
10874
  directions: resolved.character.kind === "base" ? resolved.character.directions : parentSpec.character?.directions ?? resolved.character.directions
10815
10875
  };
10816
10876
  }
10877
+ if (resolved.character?.animation && asset.animation?.motionFrom) {
10878
+ const leaderId = asset.animation.motionFrom;
10879
+ const where = `assets.${assetId}.animation.motionFrom`;
10880
+ if (leaderId === assetId) throw new Error(`${where}: an asset cannot share its own motion`);
10881
+ const leader = await finalize(leaderId);
10882
+ const led = leader.character?.animation;
10883
+ if (leader.character?.kind !== "animation" || leader.mirror || !led || led.mode !== "skeleton-v3" || led.template) {
10884
+ throw new Error(`${where}: ${leaderId} is not a skeleton-v3 loop from a sentence`);
10885
+ }
10886
+ if (leader.character.parentAssetId !== resolved.character.parentAssetId) {
10887
+ throw new Error(`${where}: ${leaderId} animates ${leader.character.parentAssetId}, not ${resolved.character.parentAssetId}`);
10888
+ }
10889
+ if (leader.character.motionLeader) {
10890
+ throw new Error(`${where}: ${leaderId} itself shares ${leader.character.motionLeader.assetId}'s motion; point at the loop that leads`);
10891
+ }
10892
+ if (led.direction === resolved.character.animation.direction) {
10893
+ throw new Error(`${where}: ${leaderId} is drawn in the same direction; sharing a motion needs another direction`);
10894
+ }
10895
+ resolved.character = { ...resolved.character, motionLeader: { assetId: leaderId, spec: leader } };
10896
+ }
10817
10897
  if (resolved.objectPro && characterParent) {
10818
10898
  const parentSpec = await finalize(characterParent);
10819
10899
  if (!parentSpec.objectPro) {
@@ -12808,7 +12888,10 @@ import path14 from "path";
12808
12888
  async function inspectRevisionReadiness(spec, lock) {
12809
12889
  if (spec.mirror) return inspectMirror(spec, lock, /* @__PURE__ */ new Set());
12810
12890
  if (spec.character?.kind === "outfit") return inspectOutfit(spec, lock, /* @__PURE__ */ new Set());
12811
- if (spec.character?.parentSpec) return inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
12891
+ if (spec.character?.parentSpec) {
12892
+ const parent = await inspectCharacterParent(spec, lock, /* @__PURE__ */ new Set());
12893
+ return parent.ready && spec.character.motionLeader ? inspectMotionLeader(spec, lock, /* @__PURE__ */ new Set()) : parent;
12894
+ }
12812
12895
  if (spec.character?.styleAnchor) return inspectStyleAnchor(spec, lock, /* @__PURE__ */ new Set());
12813
12896
  if (spec.objectPro?.parentSpec) return inspectObjectProParent(spec, lock, /* @__PURE__ */ new Set());
12814
12897
  if (spec.scene) return inspectScene(spec);
@@ -12843,6 +12926,17 @@ async function inspectTerrainConnect(spec, lock, seen) {
12843
12926
  }
12844
12927
  return { ready: true, reason: "connected sets are current" };
12845
12928
  }
12929
+ function recordedAnimationGroup(entry) {
12930
+ const character = entry?.providerMetadata?.[entry.provider]?.character;
12931
+ const text = (value) => typeof value === "string" && value ? value : null;
12932
+ return { animationGroupId: text(character?.animationGroupId), animationName: text(character?.animationName) };
12933
+ }
12934
+ async function inspectMotionLeader(spec, lock, seen) {
12935
+ const leader = spec.character.motionLeader;
12936
+ const label = `motion leader ${spec.styleId}/${leader.assetId}`;
12937
+ const dependency = await inspectParent(leader.spec, lock, seen);
12938
+ return dependency.ready ? { ready: true, reason: `${label} is current` } : { ready: false, reason: `${label} is not ready: ${dependency.reason}` };
12939
+ }
12846
12940
  async function inspectScene(spec) {
12847
12941
  const scene = spec.scene;
12848
12942
  const files = [{ label: "scene image", file: scene.file, sha256: scene.sha256 }];
@@ -13001,6 +13095,10 @@ async function inspectParent(spec, lock, seen) {
13001
13095
  const inputs = await inspectCharacterParent(spec, lock, nextSeen);
13002
13096
  if (!inputs.ready) return inputs;
13003
13097
  }
13098
+ if (spec.character?.motionLeader) {
13099
+ const inputs = await inspectMotionLeader(spec, lock, nextSeen);
13100
+ if (!inputs.ready) return inputs;
13101
+ }
13004
13102
  if (spec.character?.styleAnchor) {
13005
13103
  const inputs = await inspectStyleAnchor(spec, lock, nextSeen);
13006
13104
  if (!inputs.ready) return inputs;
@@ -13057,6 +13155,7 @@ function dependencies(spec) {
13057
13155
  spec.mirror?.sourceSpec,
13058
13156
  spec.revision?.sourceSpec,
13059
13157
  spec.character?.parentSpec,
13158
+ spec.character?.motionLeader?.spec,
13060
13159
  spec.character?.styleAnchor?.spec,
13061
13160
  spec.objectPro?.parentSpec,
13062
13161
  spec.terrainConnect?.lower?.spec,
@@ -13073,6 +13172,12 @@ function changedTerrainConnection(spec, entry, lock) {
13073
13172
  }
13074
13173
  return null;
13075
13174
  }
13175
+ function changedMotionLeader(spec, entry, lock) {
13176
+ const leader = spec.character?.motionLeader;
13177
+ if (!leader) return null;
13178
+ const current = recordedAnimationGroup(lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)]).animationGroupId;
13179
+ return (entry.motionShare?.animationGroupId ?? null) === current ? null : leader.assetId;
13180
+ }
13076
13181
  function changedTerrainSettings(spec, entry) {
13077
13182
  const recorded = entry.terrainSettings;
13078
13183
  const current = terrainSettings(spec);
@@ -13114,6 +13219,7 @@ async function buildPlan(specs, lock, opts = {}) {
13114
13219
  let state;
13115
13220
  let reason;
13116
13221
  const changedConnection = entry ? changedTerrainConnection(spec, entry, lock) : null;
13222
+ const changedMotion = entry ? changedMotionLeader(spec, entry, lock) : null;
13117
13223
  const changedSettings = entry ? changedTerrainSettings(spec, entry) : null;
13118
13224
  const revision = await inspectRevisionReadiness(spec, lock);
13119
13225
  if (revision && !revision.ready) {
@@ -13153,6 +13259,9 @@ async function buildPlan(specs, lock, opts = {}) {
13153
13259
  } else if (changedConnection) {
13154
13260
  state = "stale";
13155
13261
  reason = `connected set ${changedConnection} was regenerated; draw this set from its new terrain`;
13262
+ } else if (changedMotion) {
13263
+ state = "stale";
13264
+ reason = `shares motion with ${changedMotion}, which was regenerated; draw this direction into its new motion`;
13156
13265
  } else if (changedSettings) {
13157
13266
  state = "stale";
13158
13267
  reason = changedSettings;
@@ -13216,6 +13325,14 @@ async function buildPlan(specs, lock, opts = {}) {
13216
13325
  }
13217
13326
  }
13218
13327
  }
13328
+ for (const item of items) {
13329
+ const leader = item.spec.character?.motionLeader;
13330
+ if (!leader || !isActionable(item)) continue;
13331
+ const leaderEntry = lock.entries[lockKey(leader.spec.styleId, leader.spec.assetId)];
13332
+ if (leaderEntry?.status === "downloaded" && !recordedAnimationGroup(leaderEntry).animationGroupId) {
13333
+ item.reason += `; ${leader.assetId} has no animation group recorded, so this is drawn on its own and its motion will differ`;
13334
+ }
13335
+ }
13219
13336
  const actionable = items.filter(isActionable);
13220
13337
  const groupsByProvider = /* @__PURE__ */ new Map();
13221
13338
  for (const item of actionable) {
@@ -14625,6 +14742,7 @@ function retireGeneration(entry, retiredAt) {
14625
14742
  provider: entry.provider,
14626
14743
  postprocess: entry.postprocess,
14627
14744
  ...entry.terrainConnect ? { terrainConnect: entry.terrainConnect } : {},
14745
+ ...entry.motionShare ? { motionShare: entry.motionShare } : {},
14628
14746
  ...entry.terrainSettings ? { terrainSettings: entry.terrainSettings } : {},
14629
14747
  retiredAt
14630
14748
  };
@@ -14720,6 +14838,8 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
14720
14838
  // A connected set comes back with the connection it was drawn from, so
14721
14839
  // the plan compares the restored generation, not the one it replaced.
14722
14840
  terrainConnect: generation.terrainConnect,
14841
+ // Likewise a loop that shared another direction's motion.
14842
+ motionShare: generation.motionShare,
14723
14843
  // Likewise its settings; a generation from before they were recorded
14724
14844
  // comes back with none and is taken as current.
14725
14845
  terrainSettings: generation.terrainSettings,
@@ -15759,6 +15879,12 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
15759
15879
  };
15760
15880
  terrainConnect[side] = { sourceAssetId: link.assetId, sourceObjectId: objectId };
15761
15881
  }
15882
+ const motionLeader = spec.character?.motionLeader;
15883
+ const leaderGroup = motionLeader ? recordedAnimationGroup(lock.entries[lockKey(motionLeader.spec.styleId, motionLeader.spec.assetId)]) : void 0;
15884
+ const motionGroup = leaderGroup?.animationGroupId && leaderGroup.animationName ? { animationGroupId: leaderGroup.animationGroupId, animationName: leaderGroup.animationName } : void 0;
15885
+ if (motionLeader && !motionGroup) {
15886
+ log(` ${key}: ${motionLeader.assetId} has no animation group recorded; drawing this direction on its own`);
15887
+ }
15762
15888
  upsert(lock, key, {
15763
15889
  styleId: spec.styleId,
15764
15890
  assetId: spec.assetId,
@@ -15800,6 +15926,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
15800
15926
  referenceSha256: spec.character.outfit.reference.sha256
15801
15927
  } : null,
15802
15928
  terrainConnect: spec.terrainConnect ? terrainConnect : void 0,
15929
+ motionShare: motionLeader ? { sourceAssetId: motionLeader.assetId, animationGroupId: motionGroup?.animationGroupId ?? null } : void 0,
15803
15930
  // The terrain knobs specHash leaves out, so the plan can tell a
15804
15931
  // changed one; undefined (no key) for every other generator.
15805
15932
  terrainSettings: terrainSettings(spec),
@@ -15850,6 +15977,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
15850
15977
  ...anchorEntry?.objectId ? { styleObjectId: anchorEntry.objectId } : {},
15851
15978
  ...parentOutputs ? { parentOutputs } : {},
15852
15979
  ...spec.terrainConnect ? { terrainLinks } : {},
15980
+ ...motionGroup ? { motionGroup } : {},
15853
15981
  ...replacedMetadata ? { replacedMetadata } : {},
15854
15982
  ...lastGenerationMetadata ? { lastGenerationMetadata } : {},
15855
15983
  ...previousMetadata ? { previousMetadata } : {},
@@ -21041,6 +21169,7 @@ export {
21041
21169
  readVfxConversation,
21042
21170
  readVfxRecipe,
21043
21171
  recipeDigest,
21172
+ recordedAnimationGroup,
21044
21173
  referencedHashes,
21045
21174
  refineAsset,
21046
21175
  refineFrameSet,