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 +142 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +144 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +247 -2
- package/dist/index.d.ts +247 -2
- package/dist/index.js +143 -14
- package/dist/index.js.map +1 -1
- package/docs/CHARACTERS.md +38 -8
- package/docs/ENDPOINTS.md +10 -3
- package/docs/MANIFEST.md +1 -1
- package/docs/PIXELLAB.md +46 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +4 -0
- package/skills/pixelkiln/references/pixellab-roadmap.md +10 -13
- package/skills/pixelkiln/references/pixellab.md +5 -2
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
10207
|
-
|
|
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
|
-
|
|
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)
|
|
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,
|