@rig-c/pose 0.36.0 → 0.37.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
@@ -264,6 +264,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
264
264
  - A **step** releases the plant too: while the supporting ankle moves more than **0.03** leg lengths a frame, the other foot takes support if it is planted (not itself released or moving), and otherwise the hip follows the pose's travel. The released ankle may support again once it has moved less than **0.01** leg lengths a frame for **2 frames**. On `dance-a` the plant held the supporting ankle through a real step (frames 176–181, 579 → 718 px) and threw the hip sideways.
265
265
  - **Read from the lower ankle** ([#113](https://github.com/rig-c/pose/issues/113), [#119](https://github.com/rig-c/pose/issues/119)). On a sequence not declared `"in-place"`, a foot whose ankle is at least **0.02 leg lengths** higher in the picture than the other's never takes support: not when a plant starts, and not when a step releases the other foot. Before, a step handed support to the only foot free to take it, even a folded, lifted one, and the hip was then placed under a foot in the air. A supporting foot that the other ankle has been that much lower than for **0.1 s** hands support to it when it may take it, as before; when the lower foot may not (it is released and not yet still, or moving), the supporting foot is released, and the hip follows the pose's travel until a foot may take support. On a frame after a jump or a step's release where the only foot free under #113 is the higher one (`cause: "higher"`), the lower foot supports **tracked, not planted** ([#119](https://github.com/rig-c/pose/issues/119)): the hip puts it on #85's floor and follows its scaled ankle track (`FEET tracked`, `feet.tracked`) until it has been still for 2 frames (`< 0.01`), when the normal plant takes over at that position. Steps and jumps are read as before, and under `"in-place"` #92's reading stands. `FEET support read from the lower ankle` names every run of frames whose support differs from the reading before #113, the side before and after (`none`: no foot supports), and for each `none` why: `released`, the supporting foot let go because the lower ankle could not take support (released by a step, or moving), or the only foot free to take support was the higher one. The report's `feet.lowered` holds them as `{ frames, was, now, cause }` (absent in place). On the tests' synthetic step under a lifted leg (the lifted ankle 0.2 leg lengths up, the grounded one stepping 0.05 leg lengths on frame 10 and still from 11), the lifted leg took support on frames 10–11 before; now frames 10–11 track the lower foot on the floor (`feet.tracked`) and the grounded one is planted from 12, when it is still again.
266
266
  - **One floor** ([#85](https://github.com/rig-c/pose/issues/85)). A 2D rig has no depth, so its floor is one line, and a planted foot is on it. The picture disagrees: a step toward the camera puts the foot lower in the frame, and a camera residual puts a whole span higher, so a plant held where the picture puts each span leaves the floor stepping between spans. Every support span's supporting ankle is therefore moved in y onto one floor, the hip and everything above with it. A span's *planted height* is the world y (on the rig, posed by rig-c) its supporting ankle would be held at; the floor is the median of the spans' planted heights, or **`--floor <y>`** (world units, y up; only with `--plant-feet`, exit 2 without it). Each span's *shift* is floor − its planted height. A change of support still blends the hip over the 3 frames, now between two spans on the floor, and a planted foot does not leave the floor: on each blend frame the **supporting** leg is stretched along its chain about its root, by the factor `--stance` uses, k = (root y − floor y) / (root y − ankle y), held within **[0.92, 1.08]**, its lower bound on the frame the supporting leg's own range as `--stance`'s ([#95](https://github.com/rig-c/pose/issues/95); where that went below 0.92, a `FEET bound from the leg's own range` line, the report's `feet.blend.range`, and each blend run's `lower`). A frame that needs more is held at the bound, the rest left to the blend, and listed `FEET past the bounds` with the k needed and how far the foot was left from the floor. An ankle already within 0.01 units (the landing check's origin tolerance) is not stretched. The stretched scales are scale targets, so the landing check holds them; `--stance` then stretches the free leg, a different chain, to the supporting ankle. A jump follows the pose's travel from the hip it left, which is on the floor, carried linearly from take-off so the landing frame meets the floor and clamped so no frame inside the flight puts a foot below it ([#103](https://github.com/rig-c/pose/issues/103)), and is held the same way over its first 3 planted frames, with the flight's carry held across the landing blend so it fades out with the blend weights rather than dropping on the first frame after landing ([#147](https://github.com/rig-c/pose/issues/147)). `--stance` is unchanged: it stretches the free leg to the supporting ankle, which is on the floor. On the tests' synthetic stick, three spans 0, 6 and 1 px apart in the picture (0, 9 and 1.5 units on the rig) plant within 0.0001 units of one floor. With the shift forced to 0 they stay 9 units apart; without the stretch, the supporting ankle leaves the floor by 6.75 units on the first blend frame.
267
+ - **A loop's planted heights** ([#285](https://github.com/rig-c/pose/issues/285)). Support passes on the frame the incoming ankle first reads 0.02 leg lengths lower than the outgoing one, and each span's planted height is read through that hand-over from the span before it, so every hand-over steps the planted height the same way: over identical cycles the heights descend as a staircase while the picture's floor stays where it is. Two things keep a loop's floor a loop's. Under `--leg-scale` the outgoing plant, on the hand-over frame and the blend after it, holds its ankle with its leg at the scale the pose asked on its last supporting frame: by the hand-over frame the picture already draws that leg shortened by the lift, and posing it so read the lift as the floor dropping under the incoming foot. And on a sequence declared `"in-place"` the heights' drift over the cycles is closed before the floor is read from them, as the hip's x is: the first and the last span of the side with more spans (not counting one starting on frame 0) are a whole number of cycles apart, and the height difference between them is taken out linearly by each span's first frame, so a span and the span a cycle later have one height. A sequence with fewer than two such spans has nothing to close. On the tests' synthetic run in place (10 cycles of 19 frames, the swinging thigh foreshortened so the foot leaving the floor rises 0.08–0.12 leg lengths in the picture on its first frame off it) with `--leg-scale 0.4,1.05`, the shifts ranged over 2.4047 leg lengths, cycle 10's planted heights lay 323.99 units below cycle 1's, and every hand-over's blend needed a stretch past 0.92 (20 runs `past the bounds`); now the shifts range over 0.0033 leg lengths, cycle 10's heights are cycle 1's to under 0.0001 units, and the blend stretches by 0.9957–0.9979, the same at `--leg-scale 0.8,1.05` and as without `--leg-scale`. Without `--leg-scale` the same run's shifts ranged over 0.0695 leg lengths, 9.37 units from cycle 1 to cycle 10, and now over 0.0033.
267
268
  - `FEET` lists the supports, the jumps and the steps (each with the side, its frames, who took support and where it re-planted; in place, the loop's closure instead), and with `--plant-feet` the floor: its world y and where it came from, each span's shift in units and in that side's rig leg at setup (thigh origin → ankle), and the shifts' range, then one line per run of blend frames whose supporting leg was stretched (`blend held on the floor`) and per run `past the bounds`. The report carries them under `feet` (`supports`, `jumps`, `steps`, `changes`, `floor`, `shifts`, `blend`, `closure`).
268
269
  - **`--stance <leg lengths>`** (only with `--plant-feet`, which it is refused without; off by default) plants **both** feet when the free one is near the floor ([#60](https://github.com/rig-c/pose/issues/60)). Its unit is `FEET`'s leg length, each side's **median** picture length hip → ankle — not `--leg-scale`'s straight length, a 95th percentile — and `STANCE`'s `unit` line prints it in px and as a share of the straight leg ([#237](https://github.com/rig-c/pose/issues/237)). The card's `0.07` was measured on a dance, on a span where both shoes stood flat (`dance-a`, below); **keep `--stance` below the lifted-foot threshold the material is graded at**, since a lifted foot within the tolerance is planted: a walk graded with a 0.04 threshold plants its lifted feet under `--stance 0.07`. On a frame with a supporting side (so outside every jump), the free ankle's height above the supporting one is read in the picture, in the free side's leg length — the leg length `FEET` uses. The tolerance is a **distance** ([#114](https://github.com/rig-c/pose/issues/114)): within it, above the supporting ankle or below it, the frame is **planted both**: every bone of the free leg's chain has its `scalex` key multiplied by k = (root y − floor y) / (root y − ankle y) on the posed rig, the root being the chain's first bone's origin and the floor the supporting ankle, so the foot reaches the floor. A free ankle within the tolerance below the supporting one is planted too: its k is under 1, since a foot slightly under the floor is the same fault as one floating above it. A free ankle **past the tolerance below** the supporting one is not a stance but a support error (the foot read as supporting is the one in the air), so that frame is never planted both and keeps the pose's height; it is named on a `STANCE past the tolerance below` line with the side, the frames and the largest distance, and read `"below"` in the report's `stance.runs`. Before #114 the gap was signed, so any free ankle below the supporting one counted as within, and a lifted foot read as supporting had the grounded leg shrunk up to it. On the tests' synthetic stance, a free ankle 0.5 leg lengths below is read below and not planted; one 0.03 below is planted with k under 1. k is held within **[0.92, 1.08]**, except that the lower bound comes from the leg's own range in the sequence ([#95](https://github.com/rig-c/pose/issues/95)): per side, r = the leg's picture length hip → ankle on the frame ÷ its straight length (the 95th percentile of that length, as `--leg-scale` reads a segment's), and the frame's lower bound is min(0.92, r), so the rig leg may fold as far as the picture folds it on that frame and never less far than 0.92. The upper bound stays 1.08, since a picture leg does not read longer than straight. A sequence whose legs never fold below 0.92 keeps exactly [0.92, 1.08]. Where the range set a bound below 0.92 on a planted-both frame, one `STANCE bound from the leg's own range` line gives each side's lowest, as does the report's `stance.range`. A frame past its bound is listed `past the bounds` with the bound it had, held at the bound and not forced. Past the tolerance above, the foot is **lifted** and keeps the pose's height — unless a `--leg-scale` clamp drew that leg at another length on the frame, which moves its ankle: `STANCE` then says on how many of the run's frames the height was kept, and on which frames, and by how much, the clamp moved it (`stance.runs[].moved`, [#237](https://github.com/rig-c/pose/issues/237)). The state is read with **hysteresis** and a **shortest run** ([#87](https://github.com/rig-c/pose/issues/87)), on the distance: a planted foot leaves the stance when its distance from the supporting one goes past the tolerance, and one past it is planted again only under **0.8** of it (both figures are on the `STANCE` line). A change of state takes effect only when its condition holds for **0.1 s** (by the frames' times). A shorter crossing with the other state on both sides of it keeps their state and is listed `absorbed` with its frames and the height that crossed (`absorbed: true` in the report's `stance.runs`, beside the run that took it in). One that runs to the end of a support span is taken, since nothing of the other state follows it. A threshold read frame by frame turned noise at the threshold into a pop: one frame at 0.0708 against a 0.07 threshold dropped the stretch for one frame and the foot jumped up and back. The stretched scales are the scale targets, so the landing check holds them. Measured on `dance-a`, frames 94–175 outside the jumps: the free ankle's height above the supporting one on the posed build went from median 18.3 and p90 25.4 units to 0.0 and 0.0. On `dance-b` (492 frames at 30 fps) the 29 frames labelled by eye as both feet flat, outside the jumps, read 0.0 and 0.0. Of the 6 labelled one foot lifted, 5 stay above the tolerance; the sixth (frame 360) was 0.042 leg lengths up in the picture, within the tolerance, and so planted; without `--stance` it stood 6.5 units up, also under the tolerance (22.6 units there). That is 83 %, under the 90 % the card asked.
269
270
  - The map needs one chain ending at each ankle whose last bone is aimed `"x"` (the ankle is that bone's tip), and a `translate` the legs hang from (`LEGS_MAP`). A `range`, or a set's `turn` or `offsets`, on a bone between the hip and an ankle is refused (`LEGS_RANGE`): the plant is derived before the range clamp, and a clamp after it would move the planted foot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.36.0",
3
+ "version": "0.37.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
@@ -742,6 +742,8 @@ export interface Floor {
742
742
  }
743
743
 
744
744
  let trackFn: ((i: number, s: Side) => number) | undefined;
745
+ /** rig-c/pose#285: `plantHip`'s `held`, read by `placeHip` as `trackFn` is. */
746
+ let heldFn: ((i: number, s: Side, at: number) => Pt) | undefined;
745
747
 
746
748
  /**
747
749
  * The hip's world origin on every frame, from the supporting foot (header),
@@ -771,21 +773,36 @@ let trackFn: ((i: number, s: Side) => number) | undefined;
771
773
  * planted), and the loop is closed (`closeLoop`, `closure` in the result; null
772
774
  * without `follow`). `track(i, side)` (#119): the same world-x ankle track for
773
775
  * `feet.tracked` frames on a sequence not declared in place.
776
+ *
777
+ * `held(i, side, at)` (rig-c/pose#285, `--leg-scale`): the side's ankle on
778
+ * frame i posed as `offset` poses it, but with the leg scales of frame `at`.
779
+ * A plant hands over on the frame the outgoing foot first reads higher than the
780
+ * incoming one, so on that frame and the blend after it the outgoing foot has
781
+ * left the floor, and the picture already draws its leg shortened by the lift;
782
+ * the outgoing plant then holds that ankle with the leg as it was on its last
783
+ * supporting frame, so the lift is not read as the floor dropping under the
784
+ * incoming foot. Absent, `offset` everywhere.
785
+ *
786
+ * In place (`follow`, #285) the spans' planted heights are a loop's: their drift
787
+ * over the cycles is closed (`closeHeights`) before the floor is read from them.
774
788
  */
775
- export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floor?: number, follow?: (i: number, s: Side) => number, track?: (i: number, s: Side) => number): { hip: Pt[]; floor: Floor | null; blends: BlendFrame[]; closure: Closure | null } {
789
+ export function plantHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: number) => Pt, start: Pt, floor?: number, follow?: (i: number, s: Side) => number, track?: (i: number, s: Side) => number, held?: (i: number, s: Side, at: number) => Pt): { hip: Pt[]; floor: Floor | null; blends: BlendFrame[]; closure: Closure | null } {
776
790
  trackFn = track;
791
+ heldFn = held;
777
792
  try {
778
793
  const picture = placeHip(feet, offset, travel, start, null, follow);
779
- const heights = feet.supports.map((sp) => picture.anchors.get(sp.frames[0]));
780
- if (heights.length === 0) return { hip: picture.hip, floor: null, blends: picture.blends, closure: null };
781
- if (heights.some((h) => h === undefined)) throw new Error('plantHip: a support span starts on a frame no plant started on');
782
- const y = floor ?? percentile(heights as number[], 0.5);
794
+ const read = feet.supports.map((sp) => picture.anchors.get(sp.frames[0]));
795
+ if (read.length === 0) return { hip: picture.hip, floor: null, blends: picture.blends, closure: null };
796
+ if (read.some((h) => h === undefined)) throw new Error('plantHip: a support span starts on a frame no plant started on');
797
+ const heights = follow === undefined ? (read as number[]) : closeHeights(read as number[], feet.supports);
798
+ const y = floor ?? percentile(heights, 0.5);
783
799
  const shifts = feet.supports.map((sp, k) => ({ side: sp.side, frames: [sp.frames[0], sp.frames[1]] as [number, number], shift: y - heights[k]! }));
784
800
  const placed = placeHip(feet, offset, travel, start, y, follow);
785
801
  const closure = follow === undefined ? null : closeLoop(placed.hip, feet.supports);
786
802
  return { hip: placed.hip, floor: { y, given: floor !== undefined, shifts }, blends: placed.blends, closure };
787
803
  } finally {
788
804
  trackFn = undefined;
805
+ heldFn = undefined;
789
806
  }
790
807
  }
791
808
 
@@ -828,6 +845,32 @@ export function closeLoop(hip: Pt[], supports?: Feet['supports']): Closure {
828
845
  return { drift, frames: n };
829
846
  }
830
847
 
848
+ /**
849
+ * The planted heights of an in-place loop's support spans, their drift over the
850
+ * cycles closed (rig-c/pose#285). A span's planted height is chained from the
851
+ * span before it through the hand-over frame, and support passes only to a foot
852
+ * already lower than the other, so every hand-over steps the chain the same way:
853
+ * over identical cycles the heights descend as a staircase while the picture's
854
+ * floor stays where it is. As `closeLoop` closes the hip's x, the drift between
855
+ * matching phases — the first and the last span of the side with more spans not
856
+ * starting on frame 0, a whole number of cycles apart — is taken out linearly by
857
+ * each span's first frame, so span k and the span a cycle later have one height
858
+ * and what stays is the within-cycle difference between the two feet. Fewer than
859
+ * two such spans: nothing to close, the heights as read.
860
+ */
861
+ export function closeHeights(heights: readonly number[], supports: Feet['supports']): number[] {
862
+ const bySide: Record<Side, number[]> = { r: [], l: [] };
863
+ supports.forEach((sp, k) => {
864
+ if (sp.frames[0] > 0) bySide[sp.side].push(k);
865
+ });
866
+ const ks = bySide.r.length >= bySide.l.length ? bySide.r : bySide.l;
867
+ if (ks.length < 2) return [...heights];
868
+ const a = ks[0]!;
869
+ const b = ks[ks.length - 1]!;
870
+ const perFrame = (heights[b]! - heights[a]!) / (supports[b]!.frames[0] - supports[a]!.frames[0]);
871
+ return heights.map((h, k) => h - perFrame * supports[k]!.frames[0]);
872
+ }
873
+
831
874
  /** A frame inside the blend after a change of support (or a landing): the side now supporting, and the world y its plant holds the ankle at. */
832
875
  export interface BlendFrame {
833
876
  frame: number;
@@ -853,9 +896,17 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
853
896
  | { kind: 'plant'; side: Side; anchor: Pt; from: number }
854
897
  | { kind: 'track'; side: Side; anchor: Pt; from: number; at: Pt }
855
898
  | { kind: 'travel'; from: number; at: Pt; end?: number; land?: number; carry?: number };
899
+ /** #285: a plant's ankle on frame i — past its span (the hand-over frame, the blend after it), posed with its leg as on its last supporting frame. */
900
+ const held = heldFn;
901
+ const plantOffset = (side: Side, i: number, from: number): Pt => {
902
+ if (held === undefined || (feet.support[i] ?? null) === side) return offset(i, side);
903
+ let last = i;
904
+ while (last > from && (feet.support[last] ?? null) !== side) last--;
905
+ return held(i, side, last);
906
+ };
856
907
  const of = (mode: Mode, i: number): Pt => {
857
908
  if (mode.kind === 'plant') {
858
- const o = offset(i, mode.side);
909
+ const o = plantOffset(mode.side, i, mode.from);
859
910
  // In place (#92) the anchor's x follows the supporting ankle's own track from the frame it planted.
860
911
  const dx = follow === undefined ? 0 : follow(i, mode.side) - follow(mode.from, mode.side);
861
912
  return [mode.anchor[0] + dx - o[0], mode.anchor[1] - o[1]];
package/src/retarget.ts CHANGED
@@ -1214,9 +1214,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
1214
1214
  p.throwIfAny();
1215
1215
  const askedScales = new Map<string, number[]>();
1216
1216
  for (const ls of legScale) askedScales.set(ls.bone, ls.asked);
1217
- /** Every bone on the legs' path posed on frame i, by the targets and the given scale series. */
1218
- const poseLegsWith = (i: number, sc: ReadonlyMap<string, number[]>): PathPose => {
1219
- const r = posePath(rest, legPath!, (b): Posing | null => (targets.has(b) ? { turn: restTurn(b, i), target: targets.get(b)![i]!, aimX: aimX.get(b)! } : null), (b) => sc.get(b)?.[i] ?? null);
1217
+ /** Every bone on the legs' path posed on frame i by the targets, with the given scale series read on frame `at` (#285; i by default). */
1218
+ const poseLegsWith = (i: number, sc: ReadonlyMap<string, number[]>, at = i): PathPose => {
1219
+ const r = posePath(rest, legPath!, (b): Posing | null => (targets.has(b) ? { turn: restTurn(b, i), target: targets.get(b)![i]!, aimX: aimX.get(b)! } : null), (b) => sc.get(b)?.[at] ?? null);
1220
1220
  if (typeof r === 'string') throw new Refusal([{ code: 'LEGS_MAP', text: r }]);
1221
1221
  return r;
1222
1222
  };
@@ -1241,7 +1241,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
1241
1241
  // #119: a tracked support follows the lower ankle's own picture x, scaled, on the floor until it is still.
1242
1242
  const track = (i: number, s: Side) => series.get(`${s}_ankle`)![i]![0] * scale!.factor;
1243
1243
  const follow = inPlace ? track : undefined;
1244
- const planted = plantHip(feet!, (i, s) => offsets[i]![s], (i) => poseTravel[i]!, [tb.worldX, tb.worldY], opts.floor, follow, track);
1244
+ // #285: past its span, an outgoing plant holds its ankle with the leg as the pose asked it on its last supporting frame.
1245
+ const ankleBone = (s: Side) => (legs !== null && 'legs' in legs ? legs.legs[s].ankleBone : '');
1246
+ const held = legScaleOn ? (i: number, s: Side, at: number) => tipOf(rest, poseLegsWith(i, askedScales, at), ankleBone(s)) : undefined;
1247
+ const planted = plantHip(feet!, (i, s) => offsets[i]![s], (i) => poseTravel[i]!, [tb.worldX, tb.worldY], opts.floor, follow, track, held);
1245
1248
  const hip = planted.hip;
1246
1249
  floor = planted.floor;
1247
1250
  closure = planted.closure;