pixelkiln 0.90.0 → 0.91.1
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 +43 -8
- package/docs/ENDPOINTS.md +16 -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 +12 -13
- package/skills/pixelkiln/references/pixellab.md +6 -2
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)
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
12642
|
-
|
|
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
|
-
|
|
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 } : {},
|