@rig-c/pose 0.31.1 → 0.32.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/README.md +42 -19
- package/cli.ts +84 -25
- package/package.json +1 -1
- package/src/clean.ts +23 -2
- package/src/legs.ts +93 -0
- package/src/map.ts +35 -4
- package/src/problems.ts +9 -2
- package/src/retarget.ts +461 -27
- package/src/run.ts +332 -46
- package/src/verify.ts +111 -2
package/src/legs.ts
CHANGED
|
@@ -321,6 +321,16 @@ export interface LegScale {
|
|
|
321
321
|
scale: number[];
|
|
322
322
|
/** The scale the pose asked for on each frame before the clamp floor (`min(ratio, limits[1])`, rig-c/pose#107). */
|
|
323
323
|
asked: number[];
|
|
324
|
+
/** The picture's scale on each frame, before either limit: picture length ÷ straight (rig-c/pose#237). */
|
|
325
|
+
picture: number[];
|
|
326
|
+
/**
|
|
327
|
+
* rig-c/pose#237: on the frames this segment was clamped (`scale ≠ picture`)
|
|
328
|
+
* while its side was free (the other foot supporting, or tracked), what the
|
|
329
|
+
* clamps on that leg did to the free ankle: its height keyed against its
|
|
330
|
+
* height with every segment of that side at the picture's scale, the other
|
|
331
|
+
* leg as keyed on both. Absent with no supports read, or no clamped frame.
|
|
332
|
+
*/
|
|
333
|
+
free?: FreeAnkleShift;
|
|
324
334
|
/**
|
|
325
335
|
* rig-c/pose#133: under `--plant-feet`, runs of supporting frames on this
|
|
326
336
|
* segment where the asked scale went under `limits[0]` and followed `asked`
|
|
@@ -357,9 +367,90 @@ export function legScaleOf(lengths: readonly number[], limits: [number, number],
|
|
|
357
367
|
...(clamped.length > 0 ? { clamped } : {}),
|
|
358
368
|
scale,
|
|
359
369
|
asked,
|
|
370
|
+
picture: ratio,
|
|
360
371
|
};
|
|
361
372
|
}
|
|
362
373
|
|
|
374
|
+
/** rig-c/pose#237: one mapped leg segment's picture length ÷ its straight length (the one `--leg-scale` scales against), read with or without the flag. */
|
|
375
|
+
export interface LegSegmentRatio {
|
|
376
|
+
chain: string;
|
|
377
|
+
bone: string;
|
|
378
|
+
joints: [string, string];
|
|
379
|
+
/** px; null where it was not measured (`why`). */
|
|
380
|
+
straight: number | null;
|
|
381
|
+
/** Before any clamp: lowest, median, highest; null where it was not measured. */
|
|
382
|
+
ratio: { min: number; median: number; max: number } | null;
|
|
383
|
+
why?: string;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** A free ankle's height moved by the leg-scale clamps (rig-c/pose#237): world units, and in that side's rig leg at setup (thigh origin → ankle). */
|
|
387
|
+
export interface AnkleShift {
|
|
388
|
+
units: number;
|
|
389
|
+
legLengths: number;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** What the clamps on a free leg did to its ankle's height over a set of frames (rig-c/pose#237). */
|
|
393
|
+
export interface FreeAnkleShift {
|
|
394
|
+
side: Side;
|
|
395
|
+
/** Runs of the frames counted. */
|
|
396
|
+
frames: Array<[number, number]>;
|
|
397
|
+
/** How many frames. */
|
|
398
|
+
count: number;
|
|
399
|
+
/** The largest height above the picture's, and the largest below it (both ≥ 0); null where no frame was that way. */
|
|
400
|
+
above: AnkleShift | null;
|
|
401
|
+
below: AnkleShift | null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* rig-c/pose#237: per side and frame, the free ankle's world y as keyed less
|
|
406
|
+
* its world y with that side's leg segments at the picture's scale — on a frame
|
|
407
|
+
* the side is free (`freeOf`) and at least one of its segments' keyed scale is
|
|
408
|
+
* not the picture's; null on every other frame. `tipY` poses the legs by a
|
|
409
|
+
* scale lookup and returns that side's ankle world y; both poses share every
|
|
410
|
+
* other bone's scale, so the supporting leg cancels.
|
|
411
|
+
*/
|
|
412
|
+
export function freeAnkleShiftOf(n: number, legScale: readonly LegScale[], freeOf: (i: number) => Side | null, tipY: (i: number, side: Side, scaleOf: (bone: string) => number | null) => number): Record<Side, Array<number | null>> {
|
|
413
|
+
const out: Record<Side, Array<number | null>> = { r: new Array<number | null>(n).fill(null), l: new Array<number | null>(n).fill(null) };
|
|
414
|
+
const sideOf = (ls: LegScale): Side => (ls.joints[0].startsWith('r_') ? 'r' : 'l');
|
|
415
|
+
const byBone = new Map(legScale.map((ls) => [ls.bone, ls]));
|
|
416
|
+
for (let i = 0; i < n; i++) {
|
|
417
|
+
const f = freeOf(i);
|
|
418
|
+
if (f === null) continue;
|
|
419
|
+
const own = legScale.filter((ls) => sideOf(ls) === f);
|
|
420
|
+
if (!own.some((ls) => ls.scale[i] !== ls.picture[i])) continue;
|
|
421
|
+
const keyed = (b: string): number | null => byBone.get(b)?.scale[i] ?? null;
|
|
422
|
+
const picture = (b: string): number | null => {
|
|
423
|
+
const ls = byBone.get(b);
|
|
424
|
+
if (ls === undefined) return null;
|
|
425
|
+
return sideOf(ls) === f ? ls.picture[i]! : ls.scale[i]!;
|
|
426
|
+
};
|
|
427
|
+
out[f][i] = tipY(i, f, keyed) - tipY(i, f, picture);
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The frames of `frames` with a shift on `side`, as a `FreeAnkleShift` in that side's rig leg `leg` (world units); null when none has one. */
|
|
433
|
+
export function shiftOver(side: Side, frames: readonly number[], shift: Record<Side, ReadonlyArray<number | null>>, leg: number): FreeAnkleShift | null {
|
|
434
|
+
const hit = frames.filter((i) => shift[side][i] !== null && shift[side][i] !== undefined);
|
|
435
|
+
if (hit.length === 0) return null;
|
|
436
|
+
const vs = hit.map((i) => shift[side][i]!);
|
|
437
|
+
const up = Math.max(0, ...vs);
|
|
438
|
+
const down = Math.max(0, ...vs.map((v) => -v));
|
|
439
|
+
const of = (u: number): AnkleShift => ({ units: u, legLengths: u / leg });
|
|
440
|
+
return { side, frames: runsOf(hit), count: hit.length, above: vs.some((v) => v > 0) ? of(up) : null, below: vs.some((v) => v < 0) ? of(down) : null };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Consecutive runs of a sorted list of frames. */
|
|
444
|
+
function runsOf(xs: readonly number[]): Array<[number, number]> {
|
|
445
|
+
const out: Array<[number, number]> = [];
|
|
446
|
+
for (const x of xs) {
|
|
447
|
+
const last = out[out.length - 1];
|
|
448
|
+
if (last !== undefined && last[1] === x - 1) last[1] = x;
|
|
449
|
+
else out.push([x, x]);
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
|
|
363
454
|
function runs(flags: readonly boolean[]): Array<[number, number]> {
|
|
364
455
|
const out: Array<[number, number]> = [];
|
|
365
456
|
for (let i = 0; i < flags.length; i++) {
|
|
@@ -1129,6 +1220,8 @@ export interface StanceRun {
|
|
|
1129
1220
|
k: { min: number; max: number } | null;
|
|
1130
1221
|
/** #87: a run read in `state`, shorter than `STANCE_MIN_SECONDS` between two runs of the other state, which took their state; its frames are in their run too, and `k` is null. */
|
|
1131
1222
|
absorbed?: true;
|
|
1223
|
+
/** rig-c/pose#237, on a `lifted` or `below` run under `--leg-scale`: the frames whose free ankle the clamps moved off the pose's height, and by how much. Absent when the pose's height was kept on every frame. */
|
|
1224
|
+
moved?: FreeAnkleShift;
|
|
1132
1225
|
}
|
|
1133
1226
|
|
|
1134
1227
|
/** A run of planted-both frames whose stretch needed a factor past the bounds (or could not reach: `needed` null). */
|
package/src/map.ts
CHANGED
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
* well. Outside a set's span the retarget clamps the key there, and lists the
|
|
44
44
|
* overflow as demand (`RANGE`).
|
|
45
45
|
*
|
|
46
|
+
* The chain whose joints carry the eye line (`r_eye → l_eye`) keys the head's
|
|
47
|
+
* nod; `"nod": false` on it keys none and couples none (rig-c/pose#241). "nod"
|
|
48
|
+
* on any other chain is refused (`MAP_NOD`): a switch with nothing to turn off
|
|
49
|
+
* would be read as having turned something off.
|
|
50
|
+
*
|
|
46
51
|
* `alternates` lists the rig's alternate drawings as sets: `{ name, slots,
|
|
47
52
|
* hides, chains, pose, drawn?, offsets? }`. While a set shows, every slot in
|
|
48
53
|
* `slots` shows its attachment and every slot in `hides` is empty; `pose` is the
|
|
@@ -73,6 +78,8 @@ export interface Chain {
|
|
|
73
78
|
reach: boolean;
|
|
74
79
|
/** Per bone of the chain: the rotate key's range, degrees relative to setup, [min, max]. */
|
|
75
80
|
range: Record<string, [number, number]>;
|
|
81
|
+
/** On the chain carrying the eye line (`r_eye → l_eye`): false keys no head nod and couples none (rig-c/pose#241); absent or true, the nod is read. */
|
|
82
|
+
nod?: boolean;
|
|
76
83
|
}
|
|
77
84
|
export interface Translate {
|
|
78
85
|
bone: string;
|
|
@@ -160,6 +167,23 @@ export interface RestBone extends RawBone {
|
|
|
160
167
|
}
|
|
161
168
|
export type Rest = ReadonlyMap<string, RestBone>;
|
|
162
169
|
|
|
170
|
+
/** The body-18 points past which nothing is read: no toe past an ankle, no hand point past a wrist. */
|
|
171
|
+
const LIMB_ENDS: Readonly<Record<string, string>> = { r_ankle: 'toe', l_ankle: 'toe', r_wrist: 'hand point', l_wrist: 'hand point' };
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* MAP_CHAIN_JOINTS's remedy (rig-c/pose#240): a chain one joint short whose last
|
|
175
|
+
* joint ends a limb — a three-bone leg on hip, knee, ankle — has a bone body-18
|
|
176
|
+
* gives no segment to, so the remedy is to leave it out, not to find a joint.
|
|
177
|
+
*/
|
|
178
|
+
function endOfLimb(bones: readonly string[], joints: readonly string[]): string {
|
|
179
|
+
const last = joints[joints.length - 1];
|
|
180
|
+
if (joints.length !== bones.length || last === undefined) return '';
|
|
181
|
+
const what = LIMB_ENDS[last];
|
|
182
|
+
if (what === undefined) return '';
|
|
183
|
+
const bone = bones[bones.length - 1]!;
|
|
184
|
+
return ` — body-18 has no ${what} past "${last}", so nothing aims bone "${bone}": list the chain's bones up to the one ending at "${last}" (${bones.slice(0, -1).map((b) => `"${b}"`).join(', ')}) and leave "${bone}" out of "bones"; it stays unkeyed and turns with its parent`;
|
|
185
|
+
}
|
|
186
|
+
|
|
163
187
|
export function readMap(text: string, where: string): JointMap {
|
|
164
188
|
const doc = parseJson(text, where, 'MAP_JSON');
|
|
165
189
|
const p = new Problems();
|
|
@@ -178,10 +202,10 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
178
202
|
doc.chains.forEach((c: unknown, i: number) => {
|
|
179
203
|
const cw = `${where} chains[${i}]`;
|
|
180
204
|
if (!isObject(c)) {
|
|
181
|
-
p.add('MAP_CHAIN_SHAPE', `${cw} is ${typeName(c)}; a chain is { name, bones, joints, aim?, bend?, reach?, range? }`);
|
|
205
|
+
p.add('MAP_CHAIN_SHAPE', `${cw} is ${typeName(c)}; a chain is { name, bones, joints, aim?, bend?, reach?, range?, nod? }`);
|
|
182
206
|
return;
|
|
183
207
|
}
|
|
184
|
-
refuseUnknownKeys(p, 'MAP_KEYS', cw, c, ['name', 'bones', 'joints', 'aim', 'bend', 'reach', 'range']);
|
|
208
|
+
refuseUnknownKeys(p, 'MAP_KEYS', cw, c, ['name', 'bones', 'joints', 'aim', 'bend', 'reach', 'range', 'nod']);
|
|
185
209
|
const name = typeof c.name === 'string' && c.name !== '' ? c.name : null;
|
|
186
210
|
if (name === null) p.add('MAP_CHAIN_NAME', `${cw} "name" is ${typeName(c.name)}; it must be a non-empty string`);
|
|
187
211
|
else if (names.has(name)) p.add('MAP_CHAIN_NAME', `${cw} "name" "${name}" names two chains`);
|
|
@@ -192,7 +216,7 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
192
216
|
if (bones === null || bones.length === 0) p.add('MAP_CHAIN_BONES', `${label} "bones" is ${typeName(c.bones)}; it must be a non-empty array of bone names`);
|
|
193
217
|
if (joints === null || joints.length < 2) p.add('MAP_CHAIN_JOINTS', `${label} "joints" is ${typeName(c.joints)}; it must be an array of at least two joint names`);
|
|
194
218
|
if (bones !== null && joints !== null && bones.length > 0 && joints.length !== bones.length + 1) {
|
|
195
|
-
p.add('MAP_CHAIN_JOINTS', `${label} has ${bones.length} bone(s) and ${joints.length} joint(s); bone k carries the segment from joint k to joint k+1, so it needs ${bones.length + 1}`);
|
|
219
|
+
p.add('MAP_CHAIN_JOINTS', `${label} has ${bones.length} bone(s) and ${joints.length} joint(s); bone k carries the segment from joint k to joint k+1, so it needs ${bones.length + 1}${endOfLimb(bones, joints)}`);
|
|
196
220
|
}
|
|
197
221
|
for (const j of joints ?? []) {
|
|
198
222
|
if (!POINT_NAMES.includes(j)) p.add('MAP_JOINT_NAME', `${label} names joint "${j}", which is not a point this package reads; they are ${POINT_NAMES.join(', ')}`);
|
|
@@ -228,7 +252,14 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
228
252
|
}
|
|
229
253
|
}
|
|
230
254
|
}
|
|
231
|
-
|
|
255
|
+
let nod: boolean | undefined;
|
|
256
|
+
if ('nod' in c) {
|
|
257
|
+
const eyeLine = (joints ?? []).some((j, k, js) => (j === 'r_eye' && js[k + 1] === 'l_eye') || (j === 'l_eye' && js[k + 1] === 'r_eye'));
|
|
258
|
+
if (typeof c.nod !== 'boolean') p.add('MAP_NOD', `${label} "nod" is ${typeName(c.nod)}; it must be true or false (false keys no head nod and couples none into the head's rotate or the face pitch)`);
|
|
259
|
+
else if (!eyeLine) p.add('MAP_NOD', `${label} has "nod" but its joints (${(joints ?? []).join(' → ')}) carry no eye line; the nod is read on the chain whose joints run r_eye → l_eye (or l_eye → r_eye), so "nod" belongs there or nowhere`);
|
|
260
|
+
else nod = c.nod;
|
|
261
|
+
}
|
|
262
|
+
chains.push({ name: name ?? cw, bones: bones ?? [], joints: joints ?? [], aim, bend, reach, range, ...(nod !== undefined ? { nod } : {}) });
|
|
232
263
|
});
|
|
233
264
|
}
|
|
234
265
|
let translate: Translate | null = null;
|
package/src/problems.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface Problem {
|
|
|
10
10
|
code: string;
|
|
11
11
|
/** The object, the value found, the value required. */
|
|
12
12
|
text: string;
|
|
13
|
+
/**
|
|
14
|
+
* The per-frame problems this one collapses (rig-c/pose#238): a refusal of one
|
|
15
|
+
* kind that fires on every frame prints one line with its count, worst value
|
|
16
|
+
* and frame range; these are the lines it stands for, printed under --verbose.
|
|
17
|
+
* `detail.length` is the count the collapsed line states.
|
|
18
|
+
*/
|
|
19
|
+
detail?: readonly Problem[];
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
export class Refusal extends Error {
|
|
@@ -28,8 +35,8 @@ export function lineOf(p: Problem): string {
|
|
|
28
35
|
/** A collector: `add` while reading, `throwIfAny` once at the end. */
|
|
29
36
|
export class Problems {
|
|
30
37
|
readonly list: Problem[] = [];
|
|
31
|
-
add(code: string, text: string): void {
|
|
32
|
-
this.list.push({ code, text });
|
|
38
|
+
add(code: string, text: string, detail?: readonly Problem[]): void {
|
|
39
|
+
this.list.push(detail === undefined ? { code, text } : { code, text, detail });
|
|
33
40
|
}
|
|
34
41
|
get any(): boolean {
|
|
35
42
|
return this.list.length > 0;
|