@rig-c/pose 0.20.0 → 0.20.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
@@ -136,7 +136,7 @@ and what the run cannot decide for you it lists:
136
136
  - `RAISED` — with `--raised-by-head` ([below](#alternate-drawings-alternates)): one line per span of a set that draws an arm raised, per chain. It gives the frames and the median eye distances (the rig's in units, the reference's in px). It gives the wrist's offset from the head centre in eye distances: the median of the reference's, of where the wrist was placed, of the rig's on the pose's angles, and of the rig's as keyed. It gives the worst |rig − reference| before and after, the worst |after − placed|, and the frames whose upper arm the set's `turn` held. `RAISED none` when the flag is given and no such set showed.
137
137
  - `BY-HIP` — with a set's `"place": "by-hip"` ([below](#alternate-drawings-alternates)): the `RAISED` line against the hip, one per span of such a set per chain it places. The unit is the hip width, the offset is from the mid-hip. `BY-HIP none` when the map has such a set and none of its spans showed.
138
138
  - `REACH` — with `--raised-by-head`, or a `"by-hip"` set: a placed span whose target was out of the arm's reach, too far or too near. It names the frames, which way, and the shortfall in eye distances (hip widths for `"by-hip"`). The wrist was placed at the reach limit instead. This is not `ART-RANGE`: the rig's proportions were the limit, not the art's range. A too-near frame is placed at the limit like a too-far one, and graded at the placed distance.
139
- - `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it, and the frames clamped at each limit.
139
+ - `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it, the frames clamped at each limit, and (with `--plant-feet`) one `past the floor (support)` line per supporting run that asked under the floor ([#133](https://github.com/rig-c/pose/issues/133), `legScale[].support` in the report).
140
140
  - `PIVOT` — when the map translates a bone and has both legs: the translated bone's origin against its thigh roots' midpoint at setup, in units and hip widths — the pivot every hip turn swings the legs about.
141
141
  - `FEET` — when the map has both legs: the supports and jumps read off the reference ankles (planted or not), and on the posed build, outside the jumps, the lowest ankle's vertical range and each support span's worst vertical range and horizontal drift of the supporting ankle, in leg lengths.
142
142
  - `STANCE` — with `--stance` ([below](#the-legs-foreshortening-and-planted-feet)): one line per run of frames planted both (with the stretch keyed, its largest k), lifted, or past the tolerance below (the pose's height kept, [#114](https://github.com/rig-c/pose/issues/114)), with the free side, the frames and the tolerance; and one per run whose stretch fell past its bounds (`past the bounds`), with the k the floor needed, the bound it had and how far the foot was left from it; and, where the leg's own range set a lower bound below 0.92, one `bound from the leg's own range` line with each side's lowest.
@@ -199,7 +199,7 @@ A knee bending toward the camera is seen from the front only as a shorter leg, a
199
199
 
200
200
  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 φ)|. 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' make its local matrix R(k) · L · diag(s', 1), L its setup local matrix, and k and s' are solved so its world x axis lands on its target at the scale asked. 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. The 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.
201
201
 
202
- **`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp holds the bone and does not lift the hip by what the clamp withheld, [#107](https://github.com/rig-c/pose/issues/107)), instead of moving it by the pose's mid_hip travel:
202
+ **`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp does not lift the hip by what it withheld, [#107](https://github.com/rig-c/pose/issues/107); on a supporting leg, the segment's `scalex` follows the asked scale down to the bound #95 derives from the leg's own range so the supporting ankle stays on #85's floor while the free leg keeps the configured clamp, and any frame clamped at #95's bound lifts the hip by the remainder so the ankle never sinks, listed as `LEG-SCALE past the floor (support)`, [#133](https://github.com/rig-c/pose/issues/133)), instead of moving it by the pose's mid_hip travel:
203
203
 
204
204
  - The supporting foot is the one whose reference ankle is lower in the picture. It changes only when the other ankle has been at least **0.02 leg lengths** lower for at least **0.1 s** (timed as `END-ON` runs are), and once confirmed takes support from the frame it crossed that margin ([#102](https://github.com/rig-c/pose/issues/102)); a leg length is each side's median picture length hip → ankle, and the switch compares against their mean.
205
205
  - The new support is held where it is on the frame it takes over, and the hip blends in y from the old derivation to the new over **3 frames** while taking the new derivation's x immediately ([#102](https://github.com/rig-c/pose/issues/102)), so the planted foot does not slide across the blend.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.20.0",
3
+ "version": "0.20.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/legs.ts CHANGED
@@ -253,6 +253,17 @@ export function rootOf(rest: Rest, b: string): string {
253
253
 
254
254
  // --- leg scale -------------------------------------------------------------
255
255
 
256
+ export interface SupportScaleRun {
257
+ side: Side;
258
+ frames: [number, number];
259
+ /** The lowest and highest scale asked on the run's frames. */
260
+ asked: { min: number; max: number };
261
+ /** The lowest and highest scale keyed on the run's frames (clamped at `bound` when `asked` went under #95's bound). */
262
+ used: { min: number; max: number };
263
+ /** Present when `asked` went under #95's bound on any frame of the run: the lowest bound on the run, and the largest hip rise (world units, ≥ 0). */
264
+ clamped?: { bound: number; hipRise: number };
265
+ }
266
+
256
267
  export interface LegScale {
257
268
  chain: string;
258
269
  bone: string;
@@ -270,6 +281,13 @@ export interface LegScale {
270
281
  scale: number[];
271
282
  /** The scale the pose asked for on each frame before the clamp floor (`min(ratio, limits[1])`, rig-c/pose#107). */
272
283
  asked: number[];
284
+ /**
285
+ * rig-c/pose#133: under `--plant-feet`, runs of supporting frames on this
286
+ * segment where the asked scale went under `limits[0]` and followed `asked`
287
+ * down to the bound #95 derives from the leg's own range (`min(limits[0], r(i))`).
288
+ * Absent when no supporting frame on this segment asked under `limits[0]`.
289
+ */
290
+ support?: SupportScaleRun[];
273
291
  }
274
292
 
275
293
  /** One segment's scale per frame: picture length ÷ its 95th percentile, clamped; its asked scale without the clamp floor (#107); and the runs clamped. */
@@ -800,6 +818,96 @@ export function stanceRangeOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Si
800
818
  return out;
801
819
  }
802
820
 
821
+ /**
822
+ * Under `--plant-feet` and `--leg-scale`, a supporting leg segment that asks a
823
+ * scale under `--leg-scale`'s floor (`limits[0]`) follows its asked scale down
824
+ * to the bound #95 derives from the leg's own range (`min(limits[0], r(i))`) so
825
+ * the supporting ankle stays on #85's floor after #107 (rig-c/pose#133). The
826
+ * free leg keeps the configured clamp. When an asked scale goes under #95's
827
+ * bound (`lower`, defaulting to the segment's own range `asked[i]`), it is
828
+ * clamped there and the hip rises by the vertical remainder (`askedOffset −
829
+ * usedOffset`), never sinks.
830
+ */
831
+ export function scaleSupport(
832
+ legScale: LegScale[],
833
+ scales: Map<string, number[]>,
834
+ feet: Feet,
835
+ legs: Record<Side, { chain: Chain; ankleBone: string }>,
836
+ hip: Pt[],
837
+ askedOffset: (i: number, s: Side) => Pt,
838
+ usedOffset: (i: number, s: Side) => Pt,
839
+ lower?: StanceLower,
840
+ ): void {
841
+ const n = feet.support.length;
842
+ const tracked = new Array<Side | null>(n).fill(null);
843
+ for (const t of feet.tracked ?? []) for (let i = t.frames[0]; i <= t.frames[1]; i++) tracked[i] = t.side;
844
+ const byBone = new Map(legScale.map((ls) => [ls.bone, ls]));
845
+ const copied = new Set<string>();
846
+ const ensureCopy = (b: string): number[] => {
847
+ const cur = scales.get(b)!;
848
+ if (copied.has(b)) return cur;
849
+ const copy = [...cur];
850
+ scales.set(b, copy);
851
+ copied.add(b);
852
+ return copy;
853
+ };
854
+ const perBone = new Map<string, Array<{ frame: number; side: Side; asked: number; used: number; bound: number; clamped: boolean; hipRise: number }>>();
855
+ for (let i = 0; i < n; i++) {
856
+ const s = feet.support[i] ?? tracked[i] ?? null;
857
+ if (s === null) continue;
858
+ const touched: Array<{ bone: string; asked: number; used: number; bound: number; clamped: boolean }> = [];
859
+ for (const b of legs[s].chain.bones) {
860
+ const ls = byBone.get(b);
861
+ if (ls === undefined) continue;
862
+ const asked = ls.asked[i]!;
863
+ const floor = ls.limits[0];
864
+ if (!(asked < floor)) continue;
865
+ const r = lower !== undefined ? lower[s][i]! : asked;
866
+ const bound = Math.min(floor, r);
867
+ const used = Math.max(asked, bound);
868
+ const clamped = used > asked + 1e-12;
869
+ ensureCopy(b)[i] = used;
870
+ touched.push({ bone: b, asked, used, bound, clamped });
871
+ }
872
+ if (touched.length === 0) continue;
873
+ let hipRise = 0;
874
+ if (touched.some((t) => t.clamped)) {
875
+ hipRise = Math.max(0, askedOffset(i, s)[1] - usedOffset(i, s)[1]);
876
+ hip[i]![1] += hipRise;
877
+ }
878
+ for (const t of touched) {
879
+ const list = perBone.get(t.bone) ?? [];
880
+ list.push({ frame: i, side: s, ...t, hipRise });
881
+ perBone.set(t.bone, list);
882
+ }
883
+ }
884
+ for (const ls of legScale) {
885
+ const entries = perBone.get(ls.bone);
886
+ if (entries === undefined || entries.length === 0) continue;
887
+ const out: SupportScaleRun[] = [];
888
+ for (const e of entries) {
889
+ const last = out[out.length - 1];
890
+ if (last !== undefined && last.side === e.side && last.frames[1] === e.frame - 1 && (last.clamped !== undefined) === e.clamped) {
891
+ last.frames[1] = e.frame;
892
+ last.asked = { min: Math.min(last.asked.min, e.asked), max: Math.max(last.asked.max, e.asked) };
893
+ last.used = { min: Math.min(last.used.min, e.used), max: Math.max(last.used.max, e.used) };
894
+ if (last.clamped !== undefined) {
895
+ last.clamped = { bound: Math.min(last.clamped.bound, e.bound), hipRise: Math.max(last.clamped.hipRise, e.hipRise) };
896
+ }
897
+ } else {
898
+ out.push({
899
+ side: e.side,
900
+ frames: [e.frame, e.frame],
901
+ asked: { min: e.asked, max: e.asked },
902
+ used: { min: e.used, max: e.used },
903
+ ...(e.clamped ? { clamped: { bound: e.bound, hipRise: e.hipRise } } : {}),
904
+ });
905
+ }
906
+ }
907
+ ls.support = out;
908
+ }
909
+ }
910
+
803
911
  /** The lower bound on frame i for side s: the leg's own (#95), or the default. */
804
912
  function lowerOf(lower: StanceLower | undefined, s: Side, i: number): number {
805
913
  return lower?.[s][i] ?? STANCE_BOUNDS[0];
package/src/retarget.ts CHANGED
@@ -82,7 +82,7 @@ import { Problems, Refusal } from './problems.ts';
82
82
  import type { ArtRange, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
83
83
  import { clampArt, coveredChains, isRaised, measureArt, overflow, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
84
84
  import type { BlendHold, Closure, Feet, Floor, LegScale, PathPose, Posing, Side, Stance } from './legs.ts';
85
- import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf } from './legs.ts';
85
+ import { holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, plantHip, posePath, rootOf, scaleSupport, SIDES, stanceOf, stanceRangeOf, supportsOf, tipOf } from './legs.ts';
86
86
 
87
87
  const DEG = 180 / Math.PI;
88
88
 
@@ -739,8 +739,13 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
739
739
  const hip = planted.hip;
740
740
  floor = planted.floor;
741
741
  closure = planted.closure;
742
- for (let i = 0; i < n; i++) travel[i] = [hip[i]![0] - tb.worldX, hip[i]![1] - tb.worldY];
743
742
  const L = (legs as { legs: Record<Side, { chain: Chain; ankleBone: string }> }).legs;
743
+ // #133: on a frame where the supporting leg asks under --leg-scale's floor, its scalex follows the asked
744
+ // value down to #95's bound so the ankle stays on #85's floor; the free leg keeps the configured clamp.
745
+ if (legScaleOn && feet !== null) {
746
+ scaleSupport(legScale, scales, feet, L, hip, (i, s) => offsets[i]![s], (i, s) => tipOf(rest, poseLegs(i), L[s].ankleBone));
747
+ }
748
+ for (let i = 0; i < n; i++) travel[i] = [hip[i]![0] - tb.worldX, hip[i]![1] - tb.worldY];
744
749
  const world = (i: number, v: Pt): Pt => [hip[i]![0] + v[0], hip[i]![1] + v[1]];
745
750
  /** Both legs posed on frame i by the scales as they stand, world (y up): each chain's root and ankle. */
746
751
  const posedFeet = (i: number) => {
package/src/run.ts CHANGED
@@ -18,7 +18,7 @@ import { artRangeLine, chainsMoving, raisedLine, RAISED_WITHIN, reachLine, switc
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
20
  import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
21
- import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance } from './legs.ts';
21
+ import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
22
22
 
23
23
  export interface RetargetArgs extends RetargetOptions {
24
24
  rig: string;
@@ -138,6 +138,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
138
138
  for (const e of r.byHip) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
139
139
  for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
140
140
  for (const l of r.legScale) say(` LEG-SCALE ${legScaleLine(l, r.times)}`);
141
+ for (const l of r.legScale) for (const s of l.support ?? []) say(` LEG-SCALE ${supportScaleLine(l, s, r.times)}`);
141
142
  if (r.pivot !== null) say(` PIVOT bone "${r.pivot.bone}"'s origin is ${r.pivot.distance.toFixed(2)} units (${r.pivot.hipWidths.toFixed(3)} hip widths) from its thigh roots' midpoint (${r.pivot.roots.join(' + ')}): dx ${r.pivot.offset[0].toFixed(2)}, dy ${r.pivot.offset[1].toFixed(2)}, at setup — the pivot every hip turn swings the legs about`);
142
143
  if (r.feet !== null) {
143
144
  const f = r.feet;
@@ -673,6 +674,16 @@ function legScaleLine(l: LegScale, times: readonly number[]): string {
673
674
  return `chain "${l.chain}" bone "${l.bone}" (${l.joints[0]} → ${l.joints[1]}): straight length ${l.straight.toFixed(1)}px (95th percentile of ${times.length} frames); picture length ÷ straight ${l.ratio.min.toFixed(3)} … ${l.ratio.max.toFixed(3)} (median ${l.ratio.median.toFixed(3)}); scalex keyed in [${l.limits[0]}, ${l.limits[1]}]: ${clamp(l.low, l.limits[0], 'under')}; ${clamp(l.high, l.limits[1], 'over')}`;
674
675
  }
675
676
 
677
+ /** A LEG-SCALE past the floor (support) line (rig-c/pose#133): the supporting leg segment scaled below `--leg-scale`'s floor down to #95's bound. */
678
+ export function supportScaleLine(l: LegScale, s: SupportScaleRun, times: readonly number[]): string {
679
+ const span = (x: [number, number]) => (x[0] === x[1] ? `frame ${x[0]} (t=${times[x[0]]!.toFixed(3)}s)` : `frames ${x[0]}–${x[1]} (t=${times[x[0]]!.toFixed(3)}–${times[x[1]]!.toFixed(3)}s)`);
680
+ const val = (r: { min: number; max: number }) => (r.min === r.max ? r.min.toFixed(4) : `${r.min.toFixed(4)}–${r.max.toFixed(4)}`);
681
+ if (s.clamped !== undefined) {
682
+ return `past the floor (support): chain "${l.chain}" bone "${l.bone}" (supporting foot ${s.side}) ${span(s.frames)} — asked ${val(s.asked)} under ${l.limits[0]}, clamped at the leg's own range ${s.clamped.bound.toFixed(4)} (#95), used ${val(s.used)}; hip rose by up to +${s.clamped.hipRise.toFixed(2)} units (never sinks)`;
683
+ }
684
+ return `past the floor (support): chain "${l.chain}" bone "${l.bone}" (supporting foot ${s.side}) ${span(s.frames)} — asked ${val(s.asked)} under ${l.limits[0]}, used ${val(s.used)} (within the leg's own range, #95; ankle stays on the floor)`;
685
+ }
686
+
676
687
  /**
677
688
  * POSE_NOT_CLEANED (#67): retarget reads only a sequence `rig-pose clean` wrote.
678
689
  * The message is the command to run on this file; the output name is a