@rig-c/pose 0.22.0 → 0.22.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/README.md CHANGED
@@ -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.0",
3
+ "version": "0.22.1",
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/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);