@rig-c/pose 0.22.1 → 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 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` over `nose → lower ankle` standing height — frame-to-frame step ≤ 1 %, frozen spans ≥ 5 frames held at their median — so camera zoom neither shrinks earlier phrases nor lags behind frozen endings, 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)), clamped to [min, max]. 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.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.22.1",
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/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 h = ar.map((r, i) => Math.max(Math.hypot(r[0] - hr[i]![0], r[1] - hr[i]![1]), Math.hypot(al[i]![0] - hl[i]![0], al[i]![1] - hl[i]![1])));
728
- if (h.every((v) => v > 1e-6)) return h;
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/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 chainPosed = hasFaceSliders
68
- ? poseFrames({ ...doc, constraints: doc.constraints.filter((c) => !isFaceSlider(c)) }, animation, r.times)
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) => {