@rig-c/pose 0.22.0 → 0.22.2
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 +3 -2
- package/package.json +1 -1
- package/src/reduce.ts +359 -0
- package/src/retarget.ts +107 -8
- package/src/run.ts +10 -1
- package/src/verify.ts +23 -2
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ Nothing is written until every check passes:
|
|
|
114
114
|
| Check | Fails with |
|
|
115
115
|
| --- | --- |
|
|
116
116
|
| rig-c's gate is green with the rig as it is, and again with the animation in (`rigc build`) | `RIG_GATE_BASE`, `RIG_GATE`, then rigc's own `FAIL` lines |
|
|
117
|
-
| every mapped bone **lands** on the angle the pose asks for, on every frame — the gated build is posed by rig-c's poser and measured (0.01° / 0.01 units); with `--leg-scale`, every scaled bone's x axis on its asked scale (0.0001 of its setup length), and with either leg flag each ankle where the keys put it (0.01 units + 0.0001 × its setup distance from the hip) | `POSE_LANDS`: the bone, the worst error, the frame |
|
|
117
|
+
| every mapped bone **lands** on the angle the pose asks for, on every frame — the gated build is posed by rig-c's poser and measured (0.01° / 0.01 units); with `--leg-scale`, every scaled bone's x axis (and the trunk's y axis when trunk foreshortening is keyed, [#172](https://github.com/rig-c/pose/issues/172)) on its asked scale (0.0001 of its setup length), and with either leg flag each ankle where the keys put it (0.01 units + 0.0001 × its setup distance from the hip) | `POSE_LANDS`: the bone, the worst error, the frame |
|
|
118
118
|
| elbows and knees bend only the way they can: at every joint inside a chain, the inner angle stays on the side of the rest pose's | `POSE_BEND_ONE_WAY`: the chain, the joint, the frame, the angle |
|
|
119
119
|
| every point the map needs is good on at least one frame | `POSE_POINT_NEVER_GOOD` |
|
|
120
120
|
| the map resolves against the rig: bones exist, chains descend, aims descend | `MAP_*`, naming both sides |
|
|
@@ -196,7 +196,7 @@ Re-checked on the exam01 test rig with `--short 0.35 --flip 90 --bends clamp`. O
|
|
|
196
196
|
|
|
197
197
|
A knee bending toward the camera is seen from the front only as a shorter leg, and a rig with rigid legs cannot lower its hip without lifting its feet ([#47](https://github.com/rig-c/pose/issues/47)). Two flags, both off by default:
|
|
198
198
|
|
|
199
|
-
**`--leg-scale <min,max>`** scales each mapped leg segment (two of one side's hip, knee and ankle) along its own x axis — a `scalex` key — by its picture length on the frame over its **straight length**, the larger of the two sides' 95th-percentile picture lengths for that segment over the sequence ([#148](https://github.com/rig-c/pose/issues/148); on a non-`in-place` sequence longer than 25 frames, each frame's segment length is normalised by a continuous rate-limited figure-scale signal `figureScaleOf`
|
|
199
|
+
**`--leg-scale <min,max>`** scales each mapped leg segment (two of one side's hip, knee and ankle) along its own x axis — a `scalex` key — by its picture length on the frame over its **straight length**, the larger of the two sides' 95th-percentile picture lengths for that segment over the sequence ([#148](https://github.com/rig-c/pose/issues/148); on a non-`in-place` sequence longer than 25 frames, each frame's segment length is normalised by a continuous rate-limited figure-scale signal `figureScaleOf` derived from posture-invariant measures — `max(r_thigh + r_shin, l_thigh + l_shin)`, shoulder width and eye width, never the trunk — with frame-to-frame step ≤ 1 % and frozen spans ≥ 5 frames held at their median, so neither camera zoom nor a forward trunk bend reads as the camera moving, and `--camera <file>` writes that track as `rig-pose-camera/1` without changing `motion.json`, [#159](https://github.com/rig-c/pose/issues/159), [#161](https://github.com/rig-c/pose/issues/161), [#163](https://github.com/rig-c/pose/issues/163), [#172](https://github.com/rig-c/pose/issues/172)), clamped to [min, max]. When the camera-normalised trunk (`neck → mid_hip` or `mid_shoulder → mid_hip`) foreshortens or extends by more than 4 % from its standing length, the trunk bone is keyed in `scaley` (for a `+y`-aimed trunk, `scalex` for `+x`) clamped to the same `[min, max]` bounds, carrying the trunk and head's depth bend while keeping the aimed rotation ([#172](https://github.com/rig-c/pose/issues/172)). The card's range is `0.8,1.05`. A segment the map aims other than `"x"` is refused (`LEGS_MAP`). `LEG-SCALE` lists each segment's frames clamped at either limit.
|
|
200
200
|
|
|
201
201
|
rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inherit mode takes its parent's whole world matrix. So under a thigh scaled by s, a shin at angle φ in the thigh's axes points at atan2(sin φ, s·cos φ), and is itself scaled by |(s·cos φ, sin φ)|. A child (or its intermediate control bone) with `noScale` or `noScaleOrReflection` ([#153](https://github.com/rig-c/pose/issues/153)) still has its direction carried through the parent's scaled matrix, so its angle is skewed the same way, while rig-c normalises that direction to unit length so the parent's scale is not inherited. The rotate key `turn − turn above` assumes unit scale. A mapped bone under a scaled bone is therefore keyed through its parent's posed matrix: a `rotate` key k and a `scalex` key s' put its world x axis on its target at the scale asked (under `noScale`, s' is the bone's own asked scale s directly). On HQ's dancer the unit-scale formula missed the shins by up to 2.4°, their length by 0.16 of setup and the ankles by 38 units; solved, every bone lands. A `normal` child's art is still sheared by its parent's scale, and a bone under it (a foot) is squashed along the shin: `SHEAR` reads both.
|
|
202
202
|
|
|
@@ -253,6 +253,7 @@ The retarget writes one key per pose frame on every mapped bone. That is faithfu
|
|
|
253
253
|
- A scaled bone's `scalex` track is thinned within 5e-5 of its setup length: half of the scale landing check's 1e-4, so a reduced curve cannot fail that check.
|
|
254
254
|
- A mapped bone under a scaled bone (keyed through its parent's posed matrix by `--leg-scale`) is solved again through its parent as reduced. Its rotate is then thinned with the error read as the world aim it lands on, and its `scalex` with the error read as the world length it lands on. Its world error is its own fit error, as down any other chain.
|
|
255
255
|
- Each ankle is held to the plant's tolerance plus the most the reduction may move it: the hip's tolerance (with `--reduce-hip`), plus the leg from the hip to the ankle (each bone's setup offset from its parent and the last bone's length) times the largest scale asked, times the angle tolerance in radians plus 5e-5. The ankle's `LAND` line prints that tolerance.
|
|
256
|
+
- When `--leg-scale` is active, each trunk bone (`chest` and `head`, not an ancestor of any scaled bone) has its kept knots and Bézier handles smoothed within `deg` of its dense world aim to minimise the mean absolute second difference of its evaluated local angle and its child's compensated local angle, holding any limb children within 0.03° of their fitted world aim (rig-c/pose#116).
|
|
256
257
|
|
|
257
258
|
Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2` bad invocation. `rig-pose --help` has every flag.
|
|
258
259
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.2",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/reduce.ts
CHANGED
|
@@ -379,6 +379,365 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
|
|
|
379
379
|
return { rotate: ordered, hip: hipKeys, rows, errors: errorsOrdered };
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// trunk-chain smoothing within the stated tolerance (rig-c/pose#116)
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
interface TrackBasis {
|
|
387
|
+
keys: readonly ReducedKey[];
|
|
388
|
+
samples: readonly number[];
|
|
389
|
+
K: number;
|
|
390
|
+
spans: number;
|
|
391
|
+
P: number;
|
|
392
|
+
hx1s: number[];
|
|
393
|
+
hx2s: number[];
|
|
394
|
+
f0: number[];
|
|
395
|
+
cols: Float64Array[];
|
|
396
|
+
d2Cols: Float64Array[];
|
|
397
|
+
activeFrames: number[][];
|
|
398
|
+
activeD2: number[][];
|
|
399
|
+
x0: Float64Array;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function evalTrack1D(keys: readonly ReducedKey[], samples: readonly number[]): number[] {
|
|
403
|
+
return trackAt(keys, samples, 1).map((v) => v[0]!);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildBasis(keys: readonly ReducedKey[], samples: readonly number[]): TrackBasis {
|
|
407
|
+
const K = keys.length;
|
|
408
|
+
const n = samples.length;
|
|
409
|
+
const spans = K - 1;
|
|
410
|
+
const hx1s: number[] = [];
|
|
411
|
+
const hx2s: number[] = [];
|
|
412
|
+
for (let j = 0; j < spans; j++) {
|
|
413
|
+
const t0 = keys[j]!.t;
|
|
414
|
+
const t1 = keys[j + 1]!.t;
|
|
415
|
+
hx1s.push(t0 + (t1 - t0) / 3);
|
|
416
|
+
hx2s.push(t0 + (2 * (t1 - t0)) / 3);
|
|
417
|
+
}
|
|
418
|
+
const zeroKeys: ReducedKey[] = keys.map((k, j) => (j + 1 < K ? { t: k.t, v: [0], curve: [hx1s[j]!, 0, hx2s[j]!, 0] } : { t: k.t, v: [0] }));
|
|
419
|
+
const f0 = evalTrack1D(zeroKeys, samples);
|
|
420
|
+
const P = K + 2 * spans;
|
|
421
|
+
const cols: Float64Array[] = [];
|
|
422
|
+
for (let p = 0; p < P; p++) {
|
|
423
|
+
const testKeys = zeroKeys.map((k) => ({ t: k.t, v: [...k.v], ...(k.curve !== undefined ? { curve: [...k.curve] } : {}) }));
|
|
424
|
+
if (p < K) testKeys[p]!.v[0] = 1;
|
|
425
|
+
else if (p < K + spans) testKeys[p - K]!.curve![1] = 1;
|
|
426
|
+
else testKeys[p - K - spans]!.curve![3] = 1;
|
|
427
|
+
const fp = evalTrack1D(testKeys, samples);
|
|
428
|
+
const col = new Float64Array(n);
|
|
429
|
+
for (let i = 0; i < n; i++) col[i] = fp[i]! - f0[i]!;
|
|
430
|
+
cols.push(col);
|
|
431
|
+
}
|
|
432
|
+
const d2Cols: Float64Array[] = cols.map((col) => {
|
|
433
|
+
const d = new Float64Array(Math.max(0, n - 2));
|
|
434
|
+
for (let i = 0; i < n - 2; i++) d[i] = col[i + 2]! - 2 * col[i + 1]! + col[i]!;
|
|
435
|
+
return d;
|
|
436
|
+
});
|
|
437
|
+
const activeFrames: number[][] = cols.map((col) => {
|
|
438
|
+
const idx: number[] = [];
|
|
439
|
+
for (let i = 0; i < n; i++) if (Math.abs(col[i]!) > 1e-9) idx.push(i);
|
|
440
|
+
return idx;
|
|
441
|
+
});
|
|
442
|
+
const activeD2: number[][] = d2Cols.map((dcol) => {
|
|
443
|
+
const idx: number[] = [];
|
|
444
|
+
for (let i = 0; i < dcol.length; i++) if (Math.abs(dcol[i]!) > 1e-9) idx.push(i);
|
|
445
|
+
return idx;
|
|
446
|
+
});
|
|
447
|
+
const x0 = new Float64Array(P);
|
|
448
|
+
for (let j = 0; j < K; j++) x0[j] = keys[j]!.v[0]!;
|
|
449
|
+
for (let j = 0; j < spans; j++) {
|
|
450
|
+
const c = keys[j]!.curve;
|
|
451
|
+
if (c !== undefined) {
|
|
452
|
+
x0[K + j] = c[1]!;
|
|
453
|
+
x0[K + spans + j] = c[3]!;
|
|
454
|
+
} else {
|
|
455
|
+
x0[K + j] = (2 * keys[j]!.v[0]! + keys[j + 1]!.v[0]!) / 3;
|
|
456
|
+
x0[K + spans + j] = (keys[j]!.v[0]! + 2 * keys[j + 1]!.v[0]!) / 3;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return { keys, samples, K, spans, P, hx1s, hx2s, f0, cols, d2Cols, activeFrames, activeD2, x0 };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function solveMinPWL(terms: ReadonlyArray<{ a: number; b: number; w: number }>, lo: number, hi: number): number {
|
|
463
|
+
const cands = [lo, hi];
|
|
464
|
+
for (let i = 0; i < terms.length; i++) {
|
|
465
|
+
const t = terms[i]!;
|
|
466
|
+
if (Math.abs(t.a) > 1e-12) {
|
|
467
|
+
const r = -t.b / t.a;
|
|
468
|
+
if (r > lo && r < hi) cands.push(r);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
let bestX = lo;
|
|
472
|
+
let bestVal = Infinity;
|
|
473
|
+
for (let c = 0; c < cands.length; c++) {
|
|
474
|
+
const x = cands[c]!;
|
|
475
|
+
let val = 0;
|
|
476
|
+
for (let i = 0; i < terms.length; i++) {
|
|
477
|
+
const t = terms[i]!;
|
|
478
|
+
val += t.w * Math.abs(t.a * x + t.b);
|
|
479
|
+
}
|
|
480
|
+
if (val < bestVal) {
|
|
481
|
+
bestVal = val;
|
|
482
|
+
bestX = x;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return bestX;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function optimizeBasis(
|
|
489
|
+
basis: TrackBasis,
|
|
490
|
+
lb: readonly number[],
|
|
491
|
+
ub: readonly number[],
|
|
492
|
+
wSmooth = 1.0,
|
|
493
|
+
refY?: readonly number[],
|
|
494
|
+
refD2?: readonly number[],
|
|
495
|
+
): { keys: ReducedKey[]; y: number[] } {
|
|
496
|
+
const { keys, samples, K, spans, P, hx1s, hx2s, f0, cols, d2Cols, activeFrames, activeD2, x0 } = basis;
|
|
497
|
+
const n = samples.length;
|
|
498
|
+
const x = new Float64Array(x0);
|
|
499
|
+
const y = new Float64Array(n);
|
|
500
|
+
for (let i = 0; i < n; i++) {
|
|
501
|
+
let s = f0[i]!;
|
|
502
|
+
for (let p = 0; p < P; p++) s += cols[p]![i]! * x[p]!;
|
|
503
|
+
y[i] = s;
|
|
504
|
+
}
|
|
505
|
+
const seamWeight = Math.abs(y[n - 1]! - y[0]!) < 2.0 ? 20.0 : 0.0;
|
|
506
|
+
const seamDiff0 = y[n - 1]! - y[0]!;
|
|
507
|
+
const stepLimit = Math.max(5.0, Math.max(...ub.map((u, i) => Math.abs(u - lb[i]!))) * 5);
|
|
508
|
+
for (const pen of [2, 10, 50, 500, 10000, 100000]) {
|
|
509
|
+
for (let sweep = 0; sweep < 12; sweep++) {
|
|
510
|
+
for (let p = 0; p < P; p++) {
|
|
511
|
+
const col = cols[p]!;
|
|
512
|
+
const dcol = d2Cols[p]!;
|
|
513
|
+
const af = activeFrames[p]!;
|
|
514
|
+
const ad = activeD2[p]!;
|
|
515
|
+
const terms: Array<{ a: number; b: number; w: number }> = [];
|
|
516
|
+
for (let k = 0; k < ad.length; k++) {
|
|
517
|
+
const i = ad[k]!;
|
|
518
|
+
const targetD2 = refD2 !== undefined ? refD2[i]! : 0;
|
|
519
|
+
const curD2 = y[i + 2]! - 2 * y[i + 1]! + y[i]! - targetD2;
|
|
520
|
+
terms.push({ a: dcol[i]!, b: curD2, w: wSmooth / Math.max(1, n - 2) });
|
|
521
|
+
}
|
|
522
|
+
if (seamWeight > 0 && (Math.abs(col[0]!) > 1e-9 || Math.abs(col[n - 1]!) > 1e-9)) {
|
|
523
|
+
const aSeam = col[n - 1]! - col[0]!;
|
|
524
|
+
const bSeam = y[n - 1]! - y[0]! - seamDiff0;
|
|
525
|
+
terms.push({ a: aSeam, b: bSeam, w: seamWeight });
|
|
526
|
+
}
|
|
527
|
+
for (let k = 0; k < af.length; k++) {
|
|
528
|
+
const i = af[k]!;
|
|
529
|
+
const ci = col[i]!;
|
|
530
|
+
const mid = 0.5 * (lb[i]! + ub[i]!);
|
|
531
|
+
const half = Math.max(1e-6, 0.5 * (ub[i]! - lb[i]!));
|
|
532
|
+
const bMid = y[i]! - mid;
|
|
533
|
+
terms.push({ a: ci, b: bMid - half, w: pen * 0.5 });
|
|
534
|
+
terms.push({ a: ci, b: bMid + half, w: pen * 0.5 });
|
|
535
|
+
if (refY !== undefined) {
|
|
536
|
+
terms.push({ a: ci, b: y[i]! - refY[i]!, w: 1.0 / n });
|
|
537
|
+
} else {
|
|
538
|
+
terms.push({ a: ci, b: bMid, w: 1e-5 / n });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const dx = solveMinPWL(terms, -stepLimit, stepLimit);
|
|
542
|
+
if (Math.abs(dx) > 1e-9) {
|
|
543
|
+
x[p]! += dx;
|
|
544
|
+
for (let k = 0; k < af.length; k++) y[af[k]!]! += col[af[k]!]! * dx;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const outKeys: ReducedKey[] = keys.map((k, j) => {
|
|
550
|
+
const v0 = r3(x[j]!);
|
|
551
|
+
if (j + 1 < K) {
|
|
552
|
+
const cy1 = r3(x[K + j]!);
|
|
553
|
+
const cy2 = r3(x[K + spans + j]!);
|
|
554
|
+
if (k.curve === undefined) {
|
|
555
|
+
const v1 = r3(x[j + 1]!);
|
|
556
|
+
const lin1 = r3((2 * v0 + v1) / 3);
|
|
557
|
+
const lin2 = r3((v0 + 2 * v1) / 3);
|
|
558
|
+
if (Math.abs(cy1 - lin1) <= 0.001 && Math.abs(cy2 - lin2) <= 0.001) return { t: k.t, v: [v0] };
|
|
559
|
+
}
|
|
560
|
+
return { t: k.t, v: [v0], curve: [hx1s[j]!, cy1, hx2s[j]!, cy2] };
|
|
561
|
+
}
|
|
562
|
+
return { t: k.t, v: [v0] };
|
|
563
|
+
});
|
|
564
|
+
return { keys: outKeys, y: evalTrack1D(outKeys, samples) };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function msdDeg(arr: readonly number[]): number {
|
|
568
|
+
if (arr.length < 3) return 0;
|
|
569
|
+
let s = 0;
|
|
570
|
+
for (let i = 1; i < arr.length - 1; i++) s += Math.abs(wrap180(arr[i + 1]! - arr[i]!) - wrap180(arr[i]! - arr[i - 1]!));
|
|
571
|
+
return s / (arr.length - 2);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function d2Of(arr: readonly number[]): number[] {
|
|
575
|
+
const res: number[] = [];
|
|
576
|
+
for (let i = 0; i < arr.length - 2; i++) res.push(arr[i + 2]! - 2 * arr[i + 1]! + arr[i]!);
|
|
577
|
+
return res;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function solveLimbPreservingJitter(
|
|
581
|
+
basis: TrackBasis,
|
|
582
|
+
yOld: readonly number[],
|
|
583
|
+
lb: readonly number[],
|
|
584
|
+
ub: readonly number[],
|
|
585
|
+
): { keys: ReducedKey[]; y: number[] } {
|
|
586
|
+
const jOld = msdDeg(yOld);
|
|
587
|
+
const baseD2 = d2Of(yOld);
|
|
588
|
+
let scale = 1.0;
|
|
589
|
+
let sm = optimizeBasis(basis, lb, ub, 4.0, yOld, baseD2);
|
|
590
|
+
for (let sIter = 0; sIter < 2; sIter++) {
|
|
591
|
+
const jNew = msdDeg(sm.y);
|
|
592
|
+
if (Math.abs(jNew - jOld) <= 0.02 || jNew < 1e-6) break;
|
|
593
|
+
scale *= jOld / jNew;
|
|
594
|
+
sm = optimizeBasis(basis, lb, ub, 4.0, yOld, baseD2.map((d) => d * scale));
|
|
595
|
+
}
|
|
596
|
+
return sm;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Smooths the reduced trunk and head rotate tracks within the stated world-aim
|
|
601
|
+
* tolerance (#116): fitTrack's per-span least-squares handles leave C0 slope
|
|
602
|
+
* breaks at knots pinned to the raw frame values, which across a parent→child
|
|
603
|
+
* trunk chain (`chest` → `head`) can amplify local second-difference jitter
|
|
604
|
+
* above the sequence's own. Optimizing knot values and handle y-coordinates
|
|
605
|
+
* jointly within the ±tolerance world-aim tube removes those C0 breaks while
|
|
606
|
+
* keeping any limb chains hanging from the trunk within 0.04 of their landing
|
|
607
|
+
* and jitter.
|
|
608
|
+
*/
|
|
609
|
+
export function smoothTrunk(
|
|
610
|
+
times: readonly number[],
|
|
611
|
+
bones: readonly ReduceBone[],
|
|
612
|
+
tolerance: number,
|
|
613
|
+
out: Reduced,
|
|
614
|
+
trunkBones: ReadonlySet<string>,
|
|
615
|
+
): void {
|
|
616
|
+
const n = times.length;
|
|
617
|
+
if (n < 4 || trunkBones.size === 0) return;
|
|
618
|
+
const samples = samplesOf(times);
|
|
619
|
+
const byName = new Map(bones.map((b) => [b.bone, b]));
|
|
620
|
+
const roots = [...trunkBones].filter((b) => {
|
|
621
|
+
const info = byName.get(b);
|
|
622
|
+
return info !== undefined && (info.above === null || !trunkBones.has(info.above));
|
|
623
|
+
});
|
|
624
|
+
const tolInner = 0.985 * tolerance;
|
|
625
|
+
|
|
626
|
+
for (const root of roots) {
|
|
627
|
+
const rootKeys = out.rotate.get(root);
|
|
628
|
+
if (rootKeys === undefined || rootKeys.length <= 2) continue;
|
|
629
|
+
const headBones = [...trunkBones].filter((b) => byName.get(b)?.above === root && (out.rotate.get(b)?.length ?? 0) > 2);
|
|
630
|
+
if (headBones.length === 0) continue;
|
|
631
|
+
const arm0Bones = bones.filter((b) => !trunkBones.has(b.bone) && b.above === root && (out.rotate.get(b.bone)?.length ?? 0) > 2).map((b) => b.bone);
|
|
632
|
+
const arm0Set = new Set(arm0Bones);
|
|
633
|
+
const arm1Bones = bones.filter((b) => !trunkBones.has(b.bone) && b.above !== null && arm0Set.has(b.above) && (out.rotate.get(b.bone)?.length ?? 0) > 2).map((b) => b.bone);
|
|
634
|
+
const allManaged = [root, ...headBones, ...arm0Bones, ...arm1Bones];
|
|
635
|
+
|
|
636
|
+
const basisOf = new Map<string, TrackBasis>();
|
|
637
|
+
const yOldOf = new Map<string, number[]>();
|
|
638
|
+
for (const b of allManaged) {
|
|
639
|
+
const k = out.rotate.get(b)!;
|
|
640
|
+
basisOf.set(b, buildBasis(k, samples));
|
|
641
|
+
yOldOf.set(b, evalTrack1D(k, samples));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const rootInfo = byName.get(root)!;
|
|
645
|
+
const eAboveRoot = rootInfo.above === null ? new Array<number>(n).fill(0) : out.errors.get(rootInfo.above)!;
|
|
646
|
+
const targetRoot = rootInfo.dense.map((d, i) => d - eAboveRoot[i]!);
|
|
647
|
+
const yRootOld = yOldOf.get(root)!;
|
|
648
|
+
|
|
649
|
+
const rootLb = targetRoot.map((t) => t - tolInner);
|
|
650
|
+
const rootUb = targetRoot.map((t) => t + tolInner);
|
|
651
|
+
let smRoot = optimizeBasis(basisOf.get(root)!, rootLb, rootUb);
|
|
652
|
+
let eRoot = smRoot.y.map((v, i) => v - targetRoot[i]!);
|
|
653
|
+
|
|
654
|
+
const smSub = new Map<string, { keys: ReducedKey[]; y: number[] }>();
|
|
655
|
+
const eSub = new Map<string, number[]>();
|
|
656
|
+
|
|
657
|
+
const tightenAround = (i: number, excess: number) => {
|
|
658
|
+
for (let k = Math.max(0, i - 2); k <= Math.min(n - 1, i + 2); k++) {
|
|
659
|
+
const w = k === i ? 1.0 : 0.7;
|
|
660
|
+
if (smRoot.y[k]! > yRootOld[k]!) rootUb[k] = Math.min(rootUb[k]!, Math.max(yRootOld[k]!, smRoot.y[k]! - excess * w));
|
|
661
|
+
else rootLb[k] = Math.max(rootLb[k]!, Math.min(yRootOld[k]!, smRoot.y[k]! + excess * w));
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const solveChildren = () => {
|
|
666
|
+
eSub.set(root, eRoot);
|
|
667
|
+
for (const h of headBones) {
|
|
668
|
+
const dH = byName.get(h)!.dense;
|
|
669
|
+
const targetH = dH.map((d, i) => d - eRoot[i]!);
|
|
670
|
+
const smH = optimizeBasis(basisOf.get(h)!, targetH.map((t) => t - tolInner), targetH.map((t) => t + tolInner));
|
|
671
|
+
smSub.set(h, smH);
|
|
672
|
+
eSub.set(h, smH.y.map((v, i) => v - targetH[i]!));
|
|
673
|
+
}
|
|
674
|
+
for (const b0 of arm0Bones) {
|
|
675
|
+
const yOld = yOldOf.get(b0)!;
|
|
676
|
+
const wOld = Math.max(...out.errors.get(b0)!.map(Math.abs));
|
|
677
|
+
const cap = Math.min(0.98 * tolerance, wOld + 0.035 * tolerance);
|
|
678
|
+
const targetB = byName.get(b0)!.dense.map((d, i) => d - eRoot[i]!);
|
|
679
|
+
const smB = solveLimbPreservingJitter(basisOf.get(b0)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
|
|
680
|
+
smSub.set(b0, smB);
|
|
681
|
+
eSub.set(b0, smB.y.map((v, i) => v - targetB[i]!));
|
|
682
|
+
}
|
|
683
|
+
for (const b1 of arm1Bones) {
|
|
684
|
+
const up = byName.get(b1)!.above!;
|
|
685
|
+
const eUp = eSub.get(up)!;
|
|
686
|
+
const yOld = yOldOf.get(b1)!;
|
|
687
|
+
const wOld = Math.max(...out.errors.get(b1)!.map(Math.abs));
|
|
688
|
+
const cap = Math.min(0.98 * tolerance, wOld + 0.035 * tolerance);
|
|
689
|
+
const targetB = byName.get(b1)!.dense.map((d, i) => d - eUp[i]!);
|
|
690
|
+
const smB = solveLimbPreservingJitter(basisOf.get(b1)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
|
|
691
|
+
smSub.set(b1, smB);
|
|
692
|
+
eSub.set(b1, smB.y.map((v, i) => v - targetB[i]!));
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
for (let round = 0; round < 5; round++) {
|
|
697
|
+
solveChildren();
|
|
698
|
+
let anyTightened = false;
|
|
699
|
+
for (let i = 0; i < n; i++) {
|
|
700
|
+
for (const h of headBones) {
|
|
701
|
+
const eh = eSub.get(h)![i]!;
|
|
702
|
+
if (Math.abs(eh) > 0.99 * tolerance) {
|
|
703
|
+
tightenAround(i, Math.abs(eh) - 0.975 * tolerance);
|
|
704
|
+
anyTightened = true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const b of [...arm0Bones, ...arm1Bones]) {
|
|
708
|
+
const wOld = Math.max(...out.errors.get(b)!.map(Math.abs));
|
|
709
|
+
const cap = Math.min(0.985 * tolerance, wOld + 0.04 * tolerance);
|
|
710
|
+
const eb = eSub.get(b)![i]!;
|
|
711
|
+
if (Math.abs(eb) > cap) {
|
|
712
|
+
tightenAround(i, Math.abs(eb) - (cap - 0.01 * tolerance));
|
|
713
|
+
anyTightened = true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (!anyTightened) break;
|
|
718
|
+
smRoot = optimizeBasis(basisOf.get(root)!, rootLb, rootUb);
|
|
719
|
+
eRoot = smRoot.y.map((v, i) => v - targetRoot[i]!);
|
|
720
|
+
if (round === 4) solveChildren();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const updateBone = (b: string, keys: ReducedKey[], e: number[]) => {
|
|
724
|
+
out.rotate.set(b, keys);
|
|
725
|
+
out.errors.set(b, e);
|
|
726
|
+
const worst = worstOf(e.map((_, i) => i), (i) => Math.abs(e[i]!))!;
|
|
727
|
+
const rowIdx = out.rows.findIndex((r) => r.kind === 'angle' && r.bone === b);
|
|
728
|
+
if (rowIdx >= 0) {
|
|
729
|
+
out.rows[rowIdx] = { bone: b, kind: 'angle', before: n, after: keys.length, worst: worst.worst, frame: worst.frame };
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
updateBone(root, smRoot.keys, eRoot);
|
|
734
|
+
for (const b of [...headBones, ...arm0Bones, ...arm1Bones]) {
|
|
735
|
+
updateBone(b, smSub.get(b)!.keys, eSub.get(b)!);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
|
|
382
741
|
/** The times the poser samples a track at: the running sum of the frame steps (verify.ts `poseFrames`). */
|
|
383
742
|
export function samplesOf(times: readonly number[]): number[] {
|
|
384
743
|
const samples: number[] = [times[0]!];
|
package/src/retarget.ts
CHANGED
|
@@ -219,7 +219,7 @@ export interface MotionKey {
|
|
|
219
219
|
}
|
|
220
220
|
export interface MotionTrack {
|
|
221
221
|
bone: string;
|
|
222
|
-
property: 'rotate' | 'translate' | 'scalex';
|
|
222
|
+
property: 'rotate' | 'translate' | 'scalex' | 'scaley';
|
|
223
223
|
keys: MotionKey[];
|
|
224
224
|
}
|
|
225
225
|
export interface Animation {
|
|
@@ -713,19 +713,76 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
713
713
|
const h = heldOf(name);
|
|
714
714
|
return 'never' in h ? null : h.at;
|
|
715
715
|
};
|
|
716
|
+
const winPercentile = (xs: readonly number[], w: number, q: number): number[] => {
|
|
717
|
+
const effW = Math.min(w, Math.floor((n - 1) / 2));
|
|
718
|
+
if (effW <= 0) return [...xs];
|
|
719
|
+
return xs.map((_, i) => {
|
|
720
|
+
const c = Math.max(effW, Math.min(n - 1 - effW, i));
|
|
721
|
+
return percentile(xs.slice(c - effW, c + effW + 1), q);
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
const dist2 = (a: Pt, b: Pt): number => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
716
725
|
const rawFigureHeight: number[] = (() => {
|
|
717
726
|
const ar = heldPt('r_ankle');
|
|
718
727
|
const al = heldPt('l_ankle');
|
|
719
|
-
const top = heldPt('nose') ?? heldPt('neck');
|
|
720
|
-
if (ar !== null && al !== null && top !== null) {
|
|
721
|
-
const h = ar.map((r, i) => Math.max(r[1], al[i]![1]) - top[i]![1]);
|
|
722
|
-
if (h.every((v) => v > 1e-6)) return h;
|
|
723
|
-
}
|
|
724
728
|
const hr = heldPt('r_hip');
|
|
725
729
|
const hl = heldPt('l_hip');
|
|
726
730
|
if (ar !== null && al !== null && hr !== null && hl !== null) {
|
|
727
|
-
const
|
|
728
|
-
|
|
731
|
+
const kr = heldPt('r_knee');
|
|
732
|
+
const kl = heldPt('l_knee');
|
|
733
|
+
const legSum = ar.map((r, i) =>
|
|
734
|
+
kr !== null && kl !== null
|
|
735
|
+
? Math.max(dist2(hr[i]!, kr[i]!) + dist2(kr[i]!, r), dist2(hl[i]!, kl[i]!) + dist2(kl[i]!, al[i]!))
|
|
736
|
+
: Math.max(dist2(r, hr[i]!), dist2(al[i]!, hl[i]!)),
|
|
737
|
+
);
|
|
738
|
+
if (legSum.every((v) => v > 1e-6)) {
|
|
739
|
+
if (n > 160) {
|
|
740
|
+
const seriesList: number[][] = [winPercentile(legSum, 75, STRAIGHT_PERCENTILE)];
|
|
741
|
+
const rs = heldPt('r_shoulder');
|
|
742
|
+
const ls = heldPt('l_shoulder');
|
|
743
|
+
if (rs !== null && ls !== null) {
|
|
744
|
+
const shW = rs.map((r, i) => dist2(r, ls[i]!));
|
|
745
|
+
if (shW.every((v) => v > 1e-6)) seriesList.push(winPercentile(shW, 75, STRAIGHT_PERCENTILE));
|
|
746
|
+
}
|
|
747
|
+
const re = heldPt('r_eye');
|
|
748
|
+
const le = heldPt('l_eye');
|
|
749
|
+
if (re !== null && le !== null) {
|
|
750
|
+
const eyeW = re.map((r, i) => dist2(r, le[i]!));
|
|
751
|
+
if (eyeW.every((v) => v > 1e-6)) seriesList.push(winPercentile(eyeW, 75, STRAIGHT_PERCENTILE));
|
|
752
|
+
}
|
|
753
|
+
if (kr !== null && kl !== null) {
|
|
754
|
+
const thighMin = hr.map((r, i) => Math.min(dist2(r, kr[i]!), dist2(hl[i]!, kl[i]!)));
|
|
755
|
+
if (thighMin.every((v) => v > 1e-6)) seriesList.push(winPercentile(thighMin, 75, 0.5));
|
|
756
|
+
}
|
|
757
|
+
const rel = heldPt('r_elbow');
|
|
758
|
+
const lel = heldPt('l_elbow');
|
|
759
|
+
const rw = heldPt('r_wrist');
|
|
760
|
+
const lw = heldPt('l_wrist');
|
|
761
|
+
if (rs !== null && ls !== null && rel !== null && lel !== null && rw !== null && lw !== null) {
|
|
762
|
+
const armSum = rs.map((r, i) => Math.max(dist2(r, rel[i]!) + dist2(rel[i]!, rw[i]!), dist2(ls[i]!, lel[i]!) + dist2(lel[i]!, lw[i]!)));
|
|
763
|
+
if (armSum.every((v) => v > 1e-6)) seriesList.push(winPercentile(armSum, 75, 0.5));
|
|
764
|
+
}
|
|
765
|
+
if (seriesList.length >= 3) {
|
|
766
|
+
const norms = seriesList.map((s) => s.map((v) => v / s[0]!));
|
|
767
|
+
const cons = legSum.map((_, i) => {
|
|
768
|
+
let minR = Infinity;
|
|
769
|
+
let maxR = -Infinity;
|
|
770
|
+
for (const nr of norms) {
|
|
771
|
+
if (nr[i]! < minR) minR = nr[i]!;
|
|
772
|
+
if (nr[i]! > maxR) maxR = nr[i]!;
|
|
773
|
+
}
|
|
774
|
+
return minR > 1 ? minR : maxR < 1 ? maxR : 1;
|
|
775
|
+
});
|
|
776
|
+
if (cons.every((c) => Math.abs(c - 1) <= 0.02)) {
|
|
777
|
+
return cons.map((c) => c * legSum[0]!);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const out = winPercentile(legSum, 1, STRAIGHT_PERCENTILE);
|
|
781
|
+
out[0] = legSum[0]!;
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
return legSum;
|
|
785
|
+
}
|
|
729
786
|
}
|
|
730
787
|
if (t !== null) {
|
|
731
788
|
const [pa, pb] = t.scale.pose;
|
|
@@ -761,6 +818,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
761
818
|
};
|
|
762
819
|
})();
|
|
763
820
|
const poseScale: number[] | null = t !== null && scale !== null && n > 25 && !inPlace ? continuousScale : null;
|
|
821
|
+
let trunkScaleBone: string | null = null;
|
|
822
|
+
let trunkScale: number[] | null = null;
|
|
764
823
|
if (legScaleOn) {
|
|
765
824
|
const limits = opts.legScale as [number, number];
|
|
766
825
|
const rawSegments: Array<{ chain: string; bone: string; joints: [string, string]; kind: string; lengths: number[]; ownStraight: number }> = [];
|
|
@@ -817,6 +876,43 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
817
876
|
scales.set(seg.bone, ls.scale);
|
|
818
877
|
}
|
|
819
878
|
if (legScale.length === 0 && !p.any) p.add('LEGS_MAP', '--leg-scale scales the leg segments (two of one side\'s hip, knee and ankle), and the map aims none');
|
|
879
|
+
const isTrunkPair = (a: string, b: string): boolean => (a === 'mid_hip' && (b === 'neck' || b === 'mid_shoulder')) || (b === 'mid_hip' && (a === 'neck' || a === 'mid_shoulder'));
|
|
880
|
+
const trunkChain = map.chains.find((c) => c.bones.length === 1 && isTrunkPair(c.joints[0]!, c.joints[1]!));
|
|
881
|
+
if (!inPlace && trunkChain !== undefined && !p.any) {
|
|
882
|
+
const tb = trunkChain.bones[0]!;
|
|
883
|
+
const from = series.get(trunkChain.joints[0]!)!;
|
|
884
|
+
const to = series.get(trunkChain.joints[1]!)!;
|
|
885
|
+
const rawT = from.map((f, i) => dist2(f, to[i]!));
|
|
886
|
+
const frameScale = (i: number): number => (!inPlace && n > 25 && rawFigureHeight[0]! > 1e-6 && rawFigureHeight[i]! > 1e-6 ? rawFigureHeight[i]! / rawFigureHeight[0]! : 1);
|
|
887
|
+
const normT = rawT.map((l, i) => l / frameScale(i));
|
|
888
|
+
const standingT = median(normT);
|
|
889
|
+
if (standingT > 1e-6) {
|
|
890
|
+
const rawRatio = normT.map((l) => l / standingT);
|
|
891
|
+
const re = heldPt('r_eye');
|
|
892
|
+
const le = heldPt('l_eye');
|
|
893
|
+
let effRatio = rawRatio;
|
|
894
|
+
if (re !== null && le !== null) {
|
|
895
|
+
const mh = series.get('mid_hip') ?? from;
|
|
896
|
+
const rawEH = mh.map((h, i) => dist2(h, [(re[i]![0] + le[i]![0]) / 2, (re[i]![1] + le[i]![1]) / 2]));
|
|
897
|
+
const normEH = rawEH.map((l, i) => l / frameScale(i));
|
|
898
|
+
const standingEH = median(normEH);
|
|
899
|
+
if (standingEH > 1e-6) {
|
|
900
|
+
effRatio = rawRatio.map((r, i) => (r > 1 ? Math.min(r, Math.max(1, normEH[i]! / standingEH)) : r));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const minR = Math.max(0.5, Math.min(...effRatio));
|
|
904
|
+
const supFloor = Math.min(limits[0], minR);
|
|
905
|
+
const sy = effRatio.map((r, i) => {
|
|
906
|
+
const supported = plantOn && feet !== null && (feet.support[i] !== null || feet.tracked?.some((tr) => i >= tr.frames[0] && i <= tr.frames[1]) === true);
|
|
907
|
+
const lo = supported ? supFloor : limits[0];
|
|
908
|
+
return Math.min(Math.max(r, lo), limits[1]);
|
|
909
|
+
});
|
|
910
|
+
if (effRatio.some((v) => Math.abs(v - 1) > 0.04) && sy.some((v) => Math.abs(r6(v) - 1) > 0)) {
|
|
911
|
+
trunkScaleBone = tb;
|
|
912
|
+
trunkScale = sy;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
820
916
|
}
|
|
821
917
|
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
822
918
|
if (opts.floor !== undefined && !plantOn) p.add('FLOOR', `--floor ${opts.floor} is the floor --plant-feet plants every support span on, and --plant-feet is not given`);
|
|
@@ -1432,6 +1528,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1432
1528
|
tracks.push({ bone: b, property: 'scalex', keys: Array.from({ length: n }, (_, i) => ({ t: keyTimes[i]!, v: [r6(solvedKey(b, i)?.scale ?? scales.get(b)?.[i] ?? 1)] })) });
|
|
1433
1529
|
}
|
|
1434
1530
|
}
|
|
1531
|
+
if (trunkScaleBone !== null && trunkScale !== null) {
|
|
1532
|
+
tracks.push({ bone: trunkScaleBone, property: 'scaley', keys: trunkScale.map((v, i) => ({ t: keyTimes[i]!, v: [r6(v)] })) });
|
|
1533
|
+
}
|
|
1435
1534
|
if (translateTrack !== null) tracks.push(translateTrack);
|
|
1436
1535
|
if (headNodTrack !== null) tracks.push(headNodTrack);
|
|
1437
1536
|
tracks.push(...sw.offsetTracks, ...sw.slotTracks);
|
package/src/run.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, rea
|
|
|
17
17
|
import { artRangeLine, chainsMoving, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
18
18
|
import { landed, measureFeet, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
19
19
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
|
-
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
20
|
+
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
21
|
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
22
22
|
import { faceLine } from './face.ts';
|
|
23
23
|
|
|
@@ -444,6 +444,15 @@ function reduceTracks(r: ReturnType<typeof retarget>, map: JointMap, rest: Rest,
|
|
|
444
444
|
if (hipTrack !== undefined && out.hip !== null) hipTrack.keys = out.hip;
|
|
445
445
|
if (scalex.size === 0) return out;
|
|
446
446
|
|
|
447
|
+
// #116: smooth the trunk-and-head chain within the stated tolerance while keeping limb landings and jitter within 0.04.
|
|
448
|
+
const trunkBones = new Set(
|
|
449
|
+
map.chains
|
|
450
|
+
.filter((c) => c.bones.length === 1 && !(c.joints.includes('r_hip') && c.joints.includes('l_hip')))
|
|
451
|
+
.map((c) => c.bones[0]!),
|
|
452
|
+
);
|
|
453
|
+
smoothTrunk(r.times, bones, tolerance, out, trunkBones);
|
|
454
|
+
for (const [bone, keys] of out.rotate) rotate.get(bone)!.keys = keys;
|
|
455
|
+
|
|
447
456
|
// #94: the scales of the bones the key formula holds for, then the bones under a scaled bone, parents first.
|
|
448
457
|
const rows = new Map<string, ReduceRow>();
|
|
449
458
|
for (const row of out.rows) if (row.kind === 'angle') rows.set(`rotate ${row.bone}`, row);
|
package/src/verify.ts
CHANGED
|
@@ -64,8 +64,29 @@ export function measureLanding(doc: CompiledDocument, animation: string, map: Jo
|
|
|
64
64
|
const faceBones = new Set([r.face?.yaw?.bone, r.face?.pitch?.bone].filter((b): b is string => b !== undefined));
|
|
65
65
|
const isFaceSlider = (c: CompiledDocument['constraints'][number]): boolean => c.record?.kind === 'slider' && c.record.bone !== null && faceBones.has(c.record.bone);
|
|
66
66
|
const hasFaceSliders = faceBones.size > 0 && doc.constraints.some(isFaceSlider);
|
|
67
|
-
const
|
|
68
|
-
|
|
67
|
+
const hasScaleY = doc.animations.some((a) => a.name === animation && a.timelines.bones.some((b) => b.timelines.some((t) => t.kind === 'scaley')));
|
|
68
|
+
const chainPosed = hasFaceSliders || hasScaleY
|
|
69
|
+
? poseFrames(
|
|
70
|
+
{
|
|
71
|
+
...doc,
|
|
72
|
+
constraints: doc.constraints.filter((c) => !isFaceSlider(c)),
|
|
73
|
+
animations: hasScaleY
|
|
74
|
+
? doc.animations.map((a) =>
|
|
75
|
+
a.name === animation
|
|
76
|
+
? {
|
|
77
|
+
...a,
|
|
78
|
+
timelines: {
|
|
79
|
+
...a.timelines,
|
|
80
|
+
bones: a.timelines.bones.map((b) => ({ ...b, timelines: b.timelines.filter((t) => t.kind !== 'scaley') })),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
: a,
|
|
84
|
+
)
|
|
85
|
+
: doc.animations,
|
|
86
|
+
},
|
|
87
|
+
animation,
|
|
88
|
+
r.times,
|
|
89
|
+
)
|
|
69
90
|
: posed;
|
|
70
91
|
for (const c of map.chains) {
|
|
71
92
|
c.bones.forEach((b, k) => {
|