@rig-c/pose 0.40.0 → 0.42.0

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
@@ -141,10 +141,12 @@ A sequence carries x and y only, so nothing downstream can say which limb is nea
141
141
  | segment | leans | why |
142
142
  | --- | --- | --- |
143
143
  | thigh | forward | a hip flexes forward, and extends back too little to foreshorten past the band |
144
- | shin | forward in a walk, back in a run | the gait rule ([#281](https://github.com/rig-c/pose/issues/281)): a walk's lifted foot comes forward, a run's is kicked back. The knee bends forward in both, so the gait is the flag's value |
144
+ | shin | back | the owner's walk statement ([#336](https://github.com/rig-c/pose/issues/336)): the lifted foot is behind the supporting leg. A knee folds the shin back, in a walk and in a run |
145
145
  | upper arm | with the opposite leg | in a gait each arm swings with the other side's leg: forward when the other thigh is the more foreshortened, back when its own is, forward (a shoulder flexes forward) when neither is |
146
146
  | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
147
147
 
148
+ The thigh's row is the owner's run statement ([#336](https://github.com/rig-c/pose/issues/336)): the lifted leg is in front, its knee protruding. The table is the same for both gaits, so `--depth walk` and `--depth run` write the same signs; the gait the flag records decides which joint `--draw-order depth` orders the legs by ([below](#draw-order-from-depth---draw-order-depth)).
149
+
148
150
  The trunk faces the camera (forward is +) when its left shoulder and hip are drawn right of its right ones, away (forward is −) when they are drawn left, and is **side-on** when the shoulders and hips are drawn, on average, less than 0.1 trunk lengths (neck → mid-hip) wide: forward is then across the picture, so every sign on that frame is 0 — undecided, not guessed.
149
151
  - **`source`** is `"derived"`. `"measured"` is for a 3D detector, which may fill the same field: it is read, never written here. `clean` carries a measured depth through as it was read (`DEPTH measured depth … carried through`), and refuses `--depth` on one (`CLEAN_DEPTH`), since a derived reading would replace a measurement. A sequence carrying derived depth is refused without `--depth` (`CLEAN_DEPTH`): its depth was read off positions the clean moves.
150
152
 
@@ -154,7 +156,7 @@ The record says it: `"depth": { "gait" }` in `params`, and the `CLEAN` line ends
154
156
 
155
157
  **Layered** — `retarget` orders the limbs (`r_arm`, `l_arm`, `r_leg`, `l_leg`) front to back on every frame of a sequence that carries depth. Each joint's depth is its parent's plus its segment's, sign × rest × √(1 − ratio²) px (rest = drawn length ÷ ratio), from the shoulders and hips at 0; a limb's depth is the mean of its middle and end joints'; the nearest comes first, and equal depths (to 0.01 px) share a layer. The `DEPTH` line counts the frames ordered and which limb was in front alone how often, and the report carries `layers` ([below](#what-it-does)). Nothing is keyed from it unless `--draw-order depth` asks ([below](#draw-order-from-depth---draw-order-depth)): without the flag the motion is byte for byte the same with and without depth.
156
158
 
157
- On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-depth.ts): the walk's proportions, made in 3D from joint angles and drawn by a camera in front, behind or beside, so the generator knows every joint's true depth; `test/depth.test.ts`): on the frontal run in place, the 24 shin-frames drawn at ≤ 0.5 of rest (down to 0.409), four on every stride of each leg, read −1 under `--depth run`, as the generator draws them, and +1 under `--depth walk`; on the frontal walk, the 32 shin-frames the generator draws toward the camera read +1, and none reads −1; seen from behind, the run's kicked-back shins read +1; seen from the side, the 480 segment-frames read 0.995–1.000, every sign 0, and no segment is drawn warm or cool. `layers` agrees with the generator on 60 of the run's 60 frames and 44 of the walk's 48, two limbs counting as ordered when their true depths differ by at least the depth a limb can hold while both its segments read flat (√(1 − 0.95²) × (upper + lower ÷ 2): 31.2 px for an arm, 35.9 for a leg), and tied otherwise. The walk's four (frames 6, 18, 30, 42) are where both thighs read flat while the generator has the forward leg 36.4 px in front: the band hides it.
159
+ On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-depth.ts): the walk's proportions, made in 3D from joint angles and drawn by a camera in front, behind or beside, so the generator knows every joint's true depth; `test/depth.test.ts`): on the frontal run in place, the 24 shin-frames drawn at ≤ 0.5 of rest (down to 0.409), four on every stride of each leg, read −1 under `--depth run` and under `--depth walk`, as the generator draws them, and the two files differ only in the gait they record; on the frontal walk in place, the 28 shin-frames the generator draws away from the camera (the lifted foot behind) read −1, and none reads +1; seen from behind, the run's kicked-back shins read +1; seen from the side, the 480 segment-frames read 0.995–1.000, every sign 0, and no segment is drawn warm or cool. `layers` agrees with the generator on 60 of the run's 60 frames and 48 of the walk's 48, two limbs counting as ordered when their true depths differ by at least the depth a limb can hold while both its segments read flat (√(1 − 0.95²) × (upper + lower ÷ 2): 31.2 px for an arm, 35.9 for a leg), and tied otherwise.
158
160
 
159
161
  ### Draw order from depth: `--draw-order depth`
160
162
 
@@ -162,13 +164,19 @@ On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/ri
162
164
  rig-pose retarget --rig rig/ --map map.json --sequence run.pose.json --out motion.json --name run --draw-order depth
163
165
  ```
164
166
 
165
- With `--draw-order depth` (off by default; without it nothing changes) `retarget` keys the legs' draw order from the sequence's depth ([#304](https://github.com/rig-c/pose/issues/304)), so a run's kicked-back leg is drawn behind the standing one and a walk's forward foot in front. It writes rig-c's draw-order timeline, `"drawOrder": [{ "t", "offsets": [{ "slot", "offset" }] }, …]` on the animation beside its `tracks`: each key moves slots against the setup order, and a key with no `offsets` is the setup order again.
167
+ With `--draw-order depth` (off by default; without it nothing changes) `retarget` keys the legs' draw order from the sequence's depth ([#304](https://github.com/rig-c/pose/issues/304)), so a walk's lifted foot is drawn behind the supporting leg and a run's lifted leg, its knee forward, in front ([#336](https://github.com/rig-c/pose/issues/336)). It writes rig-c's draw-order timeline, `"drawOrder": [{ "t", "offsets": [{ "slot", "offset" }] }, …]` on the animation beside its `tracks`: each key moves slots against the setup order, and a key with no `offsets` is the setup order again. A key's `t` is half a frame before the frame it shows on (0 on the first frame), as the switches' attachment keys are: a key exactly on a frame time is a tie, and the render breaks it a frame late on about half of them ([#325](https://github.com/rig-c/pose/issues/325)).
166
168
 
167
169
  - **The pair.** The legs: a leg is the map's chains naming its knee or ankle, their bones and the bones under them no other chain maps. Its slots are the slots on those bones. A slot of an alternate set (`alternates`) on none of them goes with the leg the set depicts, so a set keeps its own switch and takes the order of the leg it replaces. The legs are keyed when their slots overlap in the draw order, filling one unbroken run of the setup order. A slot of anything else drawn between them would be re-layered by the swap, and the setup order is not changed here. The pair is then not keyed, and the line names that slot. The arms across the trunk are a later card.
168
- - **The rule.** On each frame the leg whose depth (`layers`, above) is the lower is drawn behind: its slots first in the pair's run, in their setup order, then the other's. The depths must differ by at least #281's tie, √(1 − 0.95²) × (thigh + shin ÷ 2) of the rest lengths (35.89 px on the fixtures). Inside the tie, or where a leg is not ordered, the order before holds. Before the first decided frame it is the setup order, or on a looped sequence the order the clip ends in. A key is written where the order changes.
170
+ - **The rule.** On each frame the leg whose depth is the lower is drawn behind: its slots first in the pair's run, in their setup order, then the other's. The depths must differ by at least a tie, the depth a flat chain can hold there. Inside the tie, or where a leg is not ordered, the order before holds. Before the first decided frame it is the setup order, or on a looped sequence the order the clip ends in. A key is written where the order changes.
171
+ - **The joint the gait names** ([#336](https://github.com/rig-c/pose/issues/336), the owner's two statements). On a sequence cleaned with `--depth walk` a leg's depth is its ankle's (the hip at 0, plus the thigh's, plus the shin's), the tie √(1 − 0.95²) × (thigh + shin) of the rest lengths (46.83 px on the fixtures): in a walk the lifted foot is behind the supporting leg. With `--depth run` it is its knee's (the thigh's), the tie √(1 − 0.95²) × thigh (24.97 px): in a run the lifted leg is in front, its knee protruding. The limb mean (`layers`, above) says neither: a lifted leg's thigh forward and shin back cancel in it, and a walk whose thighs stay inside the band leaves it at most half a shin (35 px on the fixtures), under its tie of √(1 − 0.95²) × (thigh + shin ÷ 2) (35.89 px). A measured depth names no gait and keeps the limb mean and its tie ([#304](https://github.com/rig-c/pose/issues/304)).
169
172
  - **No depth.** On a sequence without depth the flag is refused (`DRAWORDER_NO_DEPTH`): the order would come from nowhere, and a motion written without it would read as ordered.
173
+ - **A walk cleaned before #336** ([#336](https://github.com/rig-c/pose/issues/336)). A derived depth whose foreshortened shin leans the way the trunk faces carries the sign table before #336, where a walk's lifted foot read forward; the ankle would draw that foot in front. The flag refuses it by name (`DEPTH_RECLEAN`), listing the frames, until it is cleaned again with this version. The table since leans the shin back in both gaits, so a record it wrote never matches. A run's signs did not change, so a run cleaned by an earlier release is read as it is, and so is a walk with no foreshortened shin (its signs read the same under both tables). Measured depth is not checked. The file format does not change.
174
+
175
+ On #281's fixtures, retargeted onto a limb-cut rig whose legs overlap (the left leg drawn first; `test/draworder.test.ts`):
170
176
 
171
- On #281's fixtures, retargeted onto a limb-cut rig whose legs overlap (the left leg drawn first; `test/draworder.test.ts`): on the frontal run the order changes on frames 8, 18, … 58, within a frame of each change of support (the last within two of the loop's seam). On each of the 53 frames where the swinging shin reads away, that leg is drawn behind. On frame 12 none of the 8,763 pixels the standing leg draws shows the kicked-back leg, against 2,192 in the setup order. On the walk, each of the 32 shin-frames read toward draws that leg in front. The tie costs 18 of the 36 run frames the generator orders: the late swing (frames 5–7, 15–17, …), where the folding knee comes forward of the push-off leg by more than the tie in 3D but by 22.7–31.7 px in the derived depth, so the order holds. Without the tie the run would change order mid-swing (frames 3, 13, …), and 30 of the 53 kicked-back frames would draw the kicked-back leg in front.
177
+ - **The walk** (by the ankle): each of the 36 frames with one leg lifted draws that leg behind; the order changes on frames 3, 15, 27 and 39, where a lifted foot first goes back past the tie, and holds through the double support between. The generator orders the true ankles on 16 of the 48 frames, and the keyed order agrees on all 16.
178
+ - **The run** (by the knee): the order changes on frames 2, 12, … 52. Of the 42 frames with one leg lifted, the 30 whose lifted knee protrudes (its thigh leaning forward past the band) draw that leg in front. The other 12 (frames 0–1, 10–11, …) are the early swing: the lifted thigh is still inside the band, the generator's knees are within the tie, and the order holds — the landing leg in front, as the generator's ankles have it. The generator orders the true knees on 42 of the 60 frames, and the keyed order agrees on all 42. On frame 12, on a rig that draws the right leg first, none of the 8,763 pixels the lifted right leg draws shows the standing leg, against 2,192 in the setup order.
179
+ - **Read otherwise**: by the limb mean or by the ankle, the run draws the lifted leg behind on all 30 frames whose knee protrudes; by the limb mean or by the knee, the walk keys nothing (the setup order) and draws the lifted right leg in front on its 18 frames. Knees planted 10 px apart, inside the knee tie, hold the order before; without the tie they swap it.
172
180
 
173
181
  ## What it does
174
182
 
@@ -220,7 +228,7 @@ and what the run cannot decide for you it lists:
220
228
  - `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.
221
229
  - `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length in px and as a share of `FEET`'s leg length, the spread of its picture length over it, the limits in px, the frames clamped at each limit, where its leg was free on clamped frames how far the clamps on that leg left the free ankle above or below the picture's (in the rig's leg at setup, thigh origin → ankle, and in `FEET`'s px; `legScale[].free` in the report, [#237](https://github.com/rig-c/pose/issues/237)), 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).
222
230
  - `DEPTH` — when the sequence carries depth ([above](#derived-depth-clean---depth)): where it came from, on how many frames the limbs were ordered front to back, and which limb was in front alone how often — `DEPTH layers from the sequence's derived depth (clean --depth run): the limbs ordered front to back on N of N frame(s); in front alone: r_arm a, l_arm b, r_leg c, l_leg d; tied in front on k (the report's layers)`.
223
- - `DRAWORDER` — with `--draw-order depth` ([above](#draw-order-from-depth---draw-order-depth)): the keys and the spans they key, per leg drawn behind, cut to 260 characters — `DRAWORDER from depth, 6 key(s) over 3 span(s) (leg_r behind leg_l 8–17, 28–37, 48–57)` — and a line per pair not keyed, saying why.
231
+ - `DRAWORDER` — with `--draw-order depth` ([above](#draw-order-from-depth---draw-order-depth)): the keys and the spans they key, per leg drawn behind, cut to 260 characters — `DRAWORDER from depth by the knee (run; tie 24.97 px), 6 key(s) over 3 span(s) (leg_r behind leg_l 2–11, 22–31, 42–51)`, naming the joint and its tie for a keyed pair ([#336](https://github.com/rig-c/pose/issues/336)) — and a line per pair not keyed, saying why.
224
232
  - `PELVIS` — when the map has a pelvis chain (one bone, joints `r_hip → l_hip`) or the sequence a pelvis track ([above](#the-pelvis-track-rig-pose-pelvis)): where the pelvis bone's rotation came from and its swing — `PELVIS: from <source>, swing N°`, or `PELVIS: from hip line (no track)` — with the frames keyed from the hip line on a track that declined them.
225
233
  - `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.
226
234
  - `FEET` — when the map has both legs: one `segment` line per mapped leg segment with its picture length ÷ straight length (min · median · max), the scale `--leg-scale` would ask before its clamp, printed with or without the flag so the minimum can be read before choosing the range ([#237](https://github.com/rig-c/pose/issues/237)); 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.
@@ -244,7 +252,7 @@ and what the run cannot decide for you it lists:
244
252
  Each figure is formatted as the full line formats it, from the same record. Refusals print the same in every mode, and the exit code and the gate do not change with it; `--verbose` with `--summary` is a usage error.
245
253
 
246
254
  `--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `inputs` (consumed rig spec, map and pose files, each with invocation-relative `path` and exact-byte `sha256`; a folder rig resolves to its actual rig.json; these three identities do not claim to bind optional base motion, sets or images), `frames`, `duration`, `frameTimes` (the exact motion-key time for each pose frame; never FPS inferred from duration), `render` (the separate renderer sampling domain, `{ fps, frames }` from frame-set metadata, or `null` when not measured), `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included; `reduceKeepPlanted: true` only with `--reduce-keep-planted`; `reduceChains` only with a per-chain `--reduce`), `scale`, `scaleFit` (the `SCALE` reading: `segment`, `leg`, `hip`, `outside`, `travel`; `null` without a scale), `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `jitter`, `reach`, `posture`, `folds`, `seams`, `seamPairs`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `pelvis` (the `PELVIS` reading, [above](#the-pelvis-track-rig-pose-pelvis): `{ source, trackSource, bone, chain, swing, track, hipLine, declined }` — `source` the track's or `"hip line"`, `trackSource` the sequence's `pelvisSource` or `null`, `swing` the peak-to-peak of the bone's rotate keys in degrees, `track` and `hipLine` the frames each keyed, `declined` the frames the track declined as `[first, last]` pairs; `null` when the map has no pelvis chain and the sequence no track), `feet` (`planted`, `legLength`, `straight`, `rigLeg`, `segments`, `supports`, `jumps`, `jumpEvents`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; `straight` is each side's 95th-percentile picture length hip → ankle in px and `rigLeg` each side's rig leg at setup (thigh origin → ankle) in world units, both `null` without both legs; `segments` holds one `{ chain, bone, joints, straight, ratio: { min, median, max }, why? }` per mapped leg segment, read with or without `--leg-scale` (`straight` and `ratio` `null`, and `why` said, where it was not measured), [#237](https://github.com/rig-c/pose/issues/237); a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed?, moved? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths; `moved`, on a `lifted` or `below` run, the frames whose free ankle a `--leg-scale` clamp moved off the pose's height, as `legScale[].free` is written, [#237](https://github.com/rig-c/pose/issues/237)), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances, each row of `REDUCE` and `ankleDrift`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
247
- With a sequence that carries depth ([#281](https://github.com/rig-c/pose/issues/281), [above](#derived-depth-clean---depth)) the report also carries `layers`, one `{ frame, t, order, depth }` per frame: `order` the limbs front to back as layers, each an array of the limbs that share it (`[["r_arm"], ["l_arm", "r_leg"], ["l_leg"]]`), and `depth` each ordered limb's depth in px, + toward the camera. With `--draw-order depth` ([#304](https://github.com/rig-c/pose/issues/304)) it carries `drawOrder`: `{ source: "depth", pairs, keys, spans }`. `pairs` gives per pair `{ a, b, limbs, slots, keyed, tie }` (and `why` when not keyed), `keys` the timeline with each key's `frame`, and `spans` every span of one order `{ behind, front, frames, keyed }` (`keyed` false where the setup order already draws it so). The options carry `drawOrder: "depth"`. A run without the flag writes neither. A sequence without depth writes no `layers` key, and its report is what it was.
255
+ With a sequence that carries depth ([#281](https://github.com/rig-c/pose/issues/281), [above](#derived-depth-clean---depth)) the report also carries `layers`, one `{ frame, t, order, depth }` per frame: `order` the limbs front to back as layers, each an array of the limbs that share it (`[["r_arm"], ["l_arm", "r_leg"], ["l_leg"]]`), and `depth` each ordered limb's depth in px, + toward the camera. With `--draw-order depth` ([#304](https://github.com/rig-c/pose/issues/304)) it carries `drawOrder`: `{ source: "depth", pairs, keys, spans }`. `pairs` gives per pair `{ a, b, limbs, slots, keyed, tie }` (`by`, `"knee"` or `"ankle"`, when the sequence's gait names the joint the pair was read at, [#336](https://github.com/rig-c/pose/issues/336), the tie then that joint's; and `why` when not keyed), `keys` the timeline with each key's `frame` (the frame it first shows on; `t` there is that frame's time, as everywhere in the report, and the motion's key sits half a frame before it), and `spans` every span of one order `{ behind, front, frames, keyed }` (`keyed` false where the setup order already draws it so). The options carry `drawOrder: "depth"`. A run without the flag writes neither. A sequence without depth writes no `layers` key, and its report is what it was.
248
256
 
249
257
  With a map that has a `reach` chain ([#282](https://github.com/rig-c/pose/issues/282)) the report also carries `reachOrigin`, one `{ chain, bone, joints, anchor, setup, posed: { max, frame, t }, segment, ofSegment, warn, threshold }` per reach chain in map order: the `REACH-ORIGIN` line's figures. `setup` and `posed.max` are the distance from the bone's origin to the translated bone's (where the rig puts the source joint), in rig units; `segment` the reached length; `ofSegment` the larger distance ÷ `segment`; `warn` whether it is past `threshold` (0.2). A map without one writes no `reachOrigin` key.
250
258
 
@@ -339,6 +347,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
339
347
 
340
348
  - 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.
341
349
  - **Frontal views** ([#280](https://github.com/rig-c/pose/issues/280)). Seen from the front, a foot's depth is drawn as height: the nearer foot is the lower one, and it may be the one in the air. So every clip is also read by a second signal, the ankle's **vertical speed**: a planted foot moves with the ground — still under a fixed camera, or sliding back at the ground's speed in a gait drawn in place. Each side's ground speed is the median of its ankle's vertical step over the frames outside every jump (a walking foot is down on more than half of them), and a foot is down on a frame when the step into it or the step out of it departs from that speed by less than **0.01 leg lengths** (the re-plant's stillness); both feet can be down. The two signals **contradict** on a frame where the lower ankle (by 0.02 leg lengths) moves while the higher moves with the ground. A clip is read as frontal when that happens on more than **5 %** of the frames outside the jumps **and** its stride shows in depth: on each side the ankle's range against its hip (5th to 95th percentile) is larger in y than in x — a side view's stride is in x, and its stance ankle rises and falls with the leg's angle, so there the vertical speed does not decide. 5 % sits below the shortest contradiction a frontal walk makes (one frame a step: 2 of the 24 frames of a 1 s cycle, 8.3 %) and above every fixture the tests read in depth (0 %); the tests' synthetic frontal walk contradicts on 18 of 96 frames (18.8 %). On a frontal clip each frame's support is the vertical speed's: the one foot down; on frames both are down, the foot that landed, so support passes on the frame it lands (as #102 takes it from the landing frame); ankle height's only where neither foot moves with the ground. A plant then ends where the other foot lands, so no step releases one and #113's and #119's readings are not made. On a frontal clip `FEET` prints two `support:` lines — the frames contradicted and their share against 5 %, then the ankle ranges — and one `stride` line per support span: the signal that decided its frames, the frames both feet were down, and what ankle height alone would have read there, e.g. `stride l 8–15: ankle vertical speed on 8 frame(s) (both feet down on 3); ankle height alone would read r on 2 of them`. On any other clip ankle height decided as before and nothing is printed, in any output mode, so such a run's output is as it was. On the tests' synthetic frontal walk (an 8-drawing cycle on twos interpolated to 24 fps, both feet down on 4 of the 8 drawings, the nearer foot lower, the ankles moving 17 and 23 px in y) the feet read down are its contact table on all 96 frames, where ankle height alone supported a foot in the air on 16. The third signal the card names, the sole's distance to a floor line, is not read: no floor line in the picture can be declared (`--floor` is a world y, placed from the support it would have to decide).
350
+ - **Contact keys, by hand** ([#303](https://github.com/rig-c/pose/issues/303), the route for drawn references that [#313](https://github.com/rig-c/pose/issues/313) asked for). A drawn frontal walk can defeat every reading above: its nearer foot is drawn lower, so ankle height names the foot in the air, and a slow walk can step under the vertical speed's 0.01-leg-length stillness, so #280 never reads it as frontal. A single floor line in the picture does not help either: seen from the front the ground is a band, not a line (#303's reading). The artist's key sheet already carries the contact table, so `--contact-keys <file>` takes it: `rig-pose-contact-keys/1`, `{ "spec": "rig-pose-contact-keys/1", "keys": [ { "frame", "side" } ], "note"? }`, `side` one of `l`, `r`, `both`, `none`. Frames are the sequence's, may be fractional (a key sheet timed off the frame grid, as `--pelvis-keys`, #294), and strictly increase within `[0, frames − 1]`. **A key holds until the next one**, and the last to the clip's end: contact is a state, not an angle, so there is nothing between `l` and `r` to interpolate. A fractional key takes effect on the first frame at or after it; frames before the first key are not covered and keep the reading without keys. **The precedence is contact keys, then the vertical speed (#280, on a frontal clip), then ankle height**: the keys are applied last, over whatever the readings decided, on every frame they cover. `l` or `r` is that foot; `none` is flight, no foot supports; `both` is the foot that landed, as #280 hands over on landing: the other side from the `l` or `r` key before the run, or, where the run starts the cover or follows a `none` key (flight), the side the next `l` or `r` key names (the side that stays down after). Those neighbours are read from the keys, so a key that reaches no whole frame still counts: `l@0, none@1.1, both@1.2, l@3` holds `l` on frame 2 (rig-c/pose#338). The support spans and changes are then rebuilt from the per-frame support, as #280's are; a step, #113's `lowered` or #119's `tracked` run is kept only where it starts before the cover, cut at the frame before it (a step's re-plant inside the cover is not read), and jumps are kept as read. `FEET` prints one `support: contact keys` line (the keys, the frames they cover, how many frames they changed, the flight they declare, the frames before the first key) and one `stride` line per support span naming what decided its frames and what the reading without keys would have named, e.g. `stride l 19–37: of 19 frame(s), decided by contact keys 19; the reading without keys would name r on 4 of them`; these stride lines stand in for #280's. The report gains a `contactKeys` key (the file's path and SHA-256, the key count, the first covered frame, the runs changed) and `options.contactKeys`, only with the flag. On the tests' slow synthetic frontal walk (8 drawings a stride, each held 4.75 frames at 24 fps, keyed from its third drawing on), the support equals the keys on all 142 covered frames, where the reading without keys misses them on 16; the 10 frames before the first key keep that reading (`test/contactkeys.test.ts`). A malformed file, a `side` outside the four, a frame outside the span or one not after the key before it is refused by name (`CONTACT_KEYS`), every problem in one run, and so are keys on a map whose legs give no support to decide. Without the flag nothing is read and the output is as it was.
342
351
  - 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.
343
352
  - A **jump** — both reference ankles **and** mid_hip more than **0.05** leg lengths above their **rolling floor**, the 95th-percentile picture y over ±1.0 s around the frame, for at least **2 frames**, starting from the first immediately preceding push-off frame where both ankles are already above their rolling floor while mid_hip lags ([#142](https://github.com/rig-c/pose/issues/142)) — releases the plant: the hip follows the pose's travel from where it was, and re-plants on landing (blended the same way). At a walking hand-over the front foot has not landed yet while the back ankle lifts its heel and the hip rises 0.051–0.065 leg lengths; when the front foot lands while the back ankle stays above its floor, a back ankle that holds its height in y (< **0.01** leg lengths a frame for **2 frames**) is on a raised heel rather than in flight and is not read as a jump ([#101](https://github.com/rig-c/pose/issues/101)). The body has to lift: an ankle's picture y drifts with depth, and under one floor for the whole sequence the subject in `dance-a` standing still (frames 0–29 and 277–312, mid_hip moving 10 and 14 px) read as jumping, 152–155 of 313 frames in all.
344
353
  - **In place** ([#92](https://github.com/rig-c/pose/issues/92)). On a sequence with `"travel": "in-place"`, the supporting foot of a figure that does not travel moves back in the picture while the body stays. Holding it where it planted turns that slide into the body's travel, so the hip walks off and never comes back. Under the declaration each plant's anchor follows its own ankle's picture x, scaled into the rig, from the frame it planted, and keeps its y on the one floor. No step releases a foot, since a foot that follows its track has no plant to drag. Every foot may take support, and the lower one does, by the rules above. The loop is then closed: the hip's x on frame i moves by −drift · i / (n − 1), where drift is its x on the last frame less its x on the first. The last frame's hip x is then the first's, and the loop carries no net travel. The y is the floor's and is not touched. `FEET` prints the drift closed, in place of the steps line, and the report's `feet.closure` carries `{ drift, frames }` (`null` otherwise). On the tests' synthetic in-place loop (40 frames, each supporting ankle sliding back 2 px a frame, the hips still), the hip moves less than 10⁻⁹ px. The same joints without the declaration put it 68 px forward over the loop. With a slide of 0.04 leg lengths a frame, both feet are released as stepping and never re-plant without the declaration, and the supports alternate with it. Jumps are read as before. **What planting is and is not needed for** ([#242](https://github.com/rig-c/pose/issues/242)): the declaration is the sequence's, so the support reading (no step releases a foot), the camera-zoom normalisations and the head nod (no detrend window, no clamp) take it with or without `--plant-feet`. Planting is needed only for what acts on a planted hip: the anchor following its ankle's track and the loop closure, since without a plant there is no anchor to follow and the hip is the pose's own travel. Without `--plant-feet` the run says so in a `TRAVEL` line: `sequence declares travel: in-place but --plant-feet is off — its supports and the head nod are read in place as with it; the loop closure is --plant-feet's (it closes the planted hip), so the hip follows the pose's own travel: x +0.00 units from the first frame to the last, not closed` (the figure is the hip's measured x drift; `and the map translates no bone` when nothing is translated). On the tests' synthetic walk in place (7 cycles of 24 frames, the supporting ankle sliding up to 0.09 leg lengths a frame, the eyes bobbing 3 px twice a cycle), without `--plant-feet` the supports are 15 spans covering all 168 frames (the seam splits the first) and the head's rotate key and nod are the same on cycle 1 and cycle 7 to 0.001; reading the declaration only under `--plant-feet`, as before, they were 2 spans covering 3 frames, and the head's cycle 7 differed from cycle 1 by 1.057° and 0.661 units. That head figure came from the nod's detrend window cut at the clip's ends, which [#241](https://github.com/rig-c/pose/issues/241) wraps on a loop, so it no longer reproduces under that reading either.
@@ -392,6 +401,7 @@ The retarget writes one key per pose frame on every mapped bone. That is faithfu
392
401
  - 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.
393
402
  - 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, and at the end of the run `REDUCE ankle drift allowed: bone "…:tip" X units = Y leg lengths on any one frame` prints the same figure, split `(plant … + reduce …)` (`reduce.ankleDrift` in the report, `{ bone, side, plant, reduce, units, legLengths }`, the row's `units` the landing's `tolerance` itself), with the slide between two frames it lets through (2X) — so a slide bar is read against the reduction before it is run, not after ([#240](https://github.com/rig-c/pose/issues/240)). On the wave with `--leg-scale 0.8,1.05 --plant-feet --reduce 0.5 --reduce-hip 0.5`, that is 1.97 units = 0.0132 leg lengths a frame, 0.0263 between two.
394
403
  - **Per chain: `--reduce <deg>,<chain>=<deg>,…`** ([#283](https://github.com/rig-c/pose/issues/283)). The first item is every chain's tolerance; each `<chain>=<deg>` after it gives a chain the map names its own, on each of its bones (the smaller, when two named chains list one bone). One tolerance on every bone trades the ankle's slide against the trunk's jitter; this holds the legs tight while the trunk is smoothed. A chain the map does not have is refused by name (`POSE_REDUCE`, beside the chains it has), and a malformed item, a tolerance outside (0, 180) or a chain named twice by the command line. Each bone's `LAND` line holds it to 0.01° plus its chain's tolerance, #116's trunk smoothing keeps each bone in its own, and each ankle's allowance above is taken at the largest tolerance of the bones from the hip down to it. The run line and the `REDUCE` total name each chain's tolerance, each bone reduced within its chain's says so (`— within its chain's 0.2°`), and each `ankle drift allowed` line names the tolerance its leg was reduced within. The report carries them as `reduce.chains` and `options.reduceChains` (`{ chain: deg }`) and `reduce.ankleDrift[].legTolerance`, written only when given; without them a run's bytes are unchanged. Measured on the tests' synthetic travelling walk (`examples/walk`'s cycle three times, 72 frames, the swinging ankle 12 px fore and aft, on the wave's rig with the walk's map and its trunk given `reach`, `--plant-feet --leg-scale 0.8,1.05`), against slide (the FEET worst horizontal drift) ≤ 0.005 leg lengths and jitter (the worst `JITTER` ratio) ≤ 1.1: `--reduce 0.2` gives 0.0042 and 1.62, `--reduce 3` 0.0547 and 1.01; with `--reduce-keep-planted`, 0.0000 and 1.62 at 0.2, 0.0000 and 1.42 at 3 (the supporting leg's seam where its kept frames meet its reduced ones); `--reduce 3,leg_r=0.2,leg_l=0.2` gives 0.0042 and 0.99.
404
+ - **A parent held to its tightest child** ([#320](https://github.com/rig-c/pose/issues/320)). Under a per-chain `--reduce`, every mapped bone above a planted or aimed chain the command names is reduced within that chain's tolerance when its own is looser (the tightest such chain, when several hang below it). A chain is planted when it carries an ankle the landing check holds, aimed when the map gives it an `aim` other than `"x"`; a bent chain (an arm, a leg with no ankle check) holds nothing above it. The lever: a turn error θ on a bone moves a chain below it by θ·L (L the distance from the bone's origin to the chain's first bone) and turns it by θ. The reduction refits the chain against the turn, so its aim stays its own, but the move it cannot undo, and the ankle allowance counts L at the leg's own degrees — θ·L ≤ θ_leg·L is θ ≤ θ_leg whatever L is. An aimed chain is refitted, and on the trunk smoothed, against its parent's error, which held to the chain's degrees never asks it to absorb more than its own tolerance. Each held bone prints `REDUCE held: bone "hip" (chain "pelvis") reduced within 0.2°, not 3° — for chain "leg_r" (a planted ankle; L = 15 units to bone "leg_r0"), …`, its `REDUCE` row ends `— held to 0.2° for chain "leg_r", chain "leg_l" below it`, its `LAND` line takes 0.01° plus the held tolerance, and the report carries `reduce.held` (`[{ bone, chain, from, to, for: [{ chain, by, bone, lever }] }]`, `by` is `ankle` or `aim`), written only when a bone was held. A plain `--reduce` names no chain, so nothing is held and its bytes are unchanged. #116's trunk smoothing is kept only where it leaves every bone it manages within that bone's own tolerance; past it, the fitted keys stand and the run prints `REDUCE smoothing (rig-c/pose#116) from bone "chest" down not kept: …` (`reduce.unsmoothed`, `[{ root, bone, worst, tolerance }]`, written only then). Measured on the tests' synthetic walk with a drawn pelvis tilt (`examples/walk/make-pelvis.ts`, drawn, read by the silhouette, cleaned with `--loop`, on the wave's rig with the walk's map and a pelvis chain on `hip`, `--plant-feet`): `--reduce 3,leg_r=0.2,leg_l=0.2` without the hold missed both ankles (0.697 and 0.677 units against an allowance of 0.628); with it, bone `hip` is held to 0.2° and the ankles land at 0.248 and 0.232, the head at 2.63° within 3.01°, the same motion bytes as a hand `pelvis=0.2` entry.
395
405
  - **`--reduce-keep-planted`** (only with `--reduce` and `--plant-feet`, refused by name otherwise; [#251](https://github.com/rig-c/pose/issues/251)) adds every frame of each support span to the frames kept regardless, on every mapped bone from the hip down to that side's ankle (rotate, and `scalex` where scaled). A kept frame is keyed at its target, so on its support frames the planted ankle lands as dense but for the hip: its `LAND` line there is `ankle on its support frames (kept dense)`, held to the plant's tolerance plus `--reduce-hip`'s (0 without it), and off them `ankle off its support frames`, held to the allowance above. `reduce.ankleDrift[].support` carries the tighter figure (`{ reduce, units, legLengths }`, `reduce` being the hip's share; `null` for a side that never supports). The tolerance has no separate rounding term: on the tests' synthetic walk (72 frames, six support spans) the supporting ankle on its support frames sits 1.22e-3 units from the dense run's, against the plant's 0.0253. Measured on that walk with `--reduce 1`, the FEET slide (the worst horizontal drift of a supporting ankle over a support span) goes from 0.0226 leg lengths to 0.00002, the dense run's; with `--reduce-hip 0.5` beside it, 0.0048, since the hip is not kept. The price is the keys: the supporting leg keeps one per support frame (the walk's four leg rotate tracks, 47 → 135 keys).
396
406
  - 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).
397
407
 
@@ -457,7 +467,7 @@ Which bones carry which joints. You write it for your rig:
457
467
  ⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (`dance-a`, 313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material, arms go `"free"`. Legs on `"rest"` with `--bends clamp` hold only while the knees stay near straight. On `dance-b` (a second dance with deeper knee bends, rig-c/pose#27), that setting straightened the left knee for most of the dance: `leg_l1` was 48.3° off at p90, against 0.0–0.3° with free legs. Leave legs `"free"` unless the dance keeps its knees straight.
458
468
  - `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
459
469
  - `translate.scale` is one length measured on both: two points on the rig, two points in the pose (its median over the frames where both are good). Pick a long segment that does not foreshorten much.
460
- - `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides` (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
470
+ - `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides`, each half a frame before the frame it shows on as the switches' and the draw order's keys are ([#334](https://github.com/rig-c/pose/issues/334)) (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
461
471
  - `body` (optional, [#173](https://github.com/rig-c/pose/issues/173)): `{ "yaw"?: { "bone": "body_yaw", "range": [-30, 30], "sets"?: [{ "set": "T8", "above": 45 }] }, "sets"?: [{ "set": "T8", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Maps the per-frame body yaw read from the camera-normalised shoulder and hip width (`BODY`) into `range` with the same `0.8 × range` knee mapping as `face`, keying the named dial bone's `rotate` track, and switches any declared `sets` (`body.sets` or `body.yaw.sets`) with the same `above` / `above − 10°` hysteresis (overriding `face` sets and arm `alternates` on slots they share while active). Without `body` in the map, `BODY` and `--report`'s `body` still report the reading while leaving every track in `motion.json` unchanged.
462
472
 
463
473
  ### A joint's range: `range`
package/cli.ts CHANGED
@@ -82,8 +82,10 @@ clean (required before retarget; rig-c/pose#67)
82
82
  (the trunk side-on). Which way a foreshortened segment leans comes from the body, with
83
83
  "forward" the trunk's facing: thigh and forearm forward (a hip flexes forward, an elbow
84
84
  bends back); an upper arm with the opposite leg (forward when the other thigh is the more
85
- foreshortened); a shin forward in a walk (the lifted foot comes forward) and back in a run
86
- (it is kicked back) — the gait is this flag's value. tools/skeleton.ts colours the segments
85
+ foreshortened); a shin back (a knee folds it back) — the same in a walk and a run, after
86
+ the owner's two statements (rig-c/pose#336): a walk's lifted foot is behind the supporting
87
+ leg, a run's lifted leg is in front, its knee protruding. The gait is this flag's value and
88
+ decides the joint --draw-order depth reads. tools/skeleton.ts colours the segments
87
89
  by it, and retarget --report orders the limbs front to back by it (layers)
88
90
 
89
91
  In order, each step printed with its figures: (1) spikes — a joint more than 0.2 trunk lengths
@@ -191,10 +193,20 @@ retarget
191
193
  (the shortfall listed). RAISED lines (default: off; rig-c/pose#62)
192
194
  --draw-order depth key the legs' draw order from the sequence's depth (clean --depth; rig-c/pose#304): on
193
195
  each frame the leg whose depth reads farther from the camera is drawn behind the other,
194
- a tie (equal depths) holding the order before, in rig-c's drawOrder timeline. The legs'
196
+ a tie (equal depths) holding the order before, in rig-c's drawOrder timeline. The depth is
197
+ read at the joint the gait names (rig-c/pose#336): a walk (clean --depth walk) by the ankle,
198
+ the lifted foot behind; a run by the knee, the lifted leg in front. The legs'
195
199
  slots must fill one unbroken run of the setup draw order (else the pair is not keyed and
196
200
  the line names the slot between); a set's slot goes with the leg it depicts. Refused on
197
- a sequence without depth (DRAWORDER_NO_DEPTH). DRAWORDER lines (default: off)
201
+ a sequence without depth (DRAWORDER_NO_DEPTH), and on a walk cleaned before rig-c/pose#336
202
+ (a shin leaning forward: DEPTH_RECLEAN; clean it again). DRAWORDER lines (default: off)
203
+ --contact-keys <file> which foot supports, by hand (rig-c/pose#303): rig-pose-contact-keys/1, { "spec":
204
+ "rig-pose-contact-keys/1", "keys": [{ "frame", "side" }], "note"? }, side l | r | both |
205
+ none; frames the sequence's, fractional allowed, strictly increasing in [0, N − 1]. A key
206
+ holds until the next one (contact is a state, not interpolated); before the first key the
207
+ support is read as without it. On every frame they cover the keys decide, over the
208
+ vertical speed (#280) and ankle height: none is flight; both, the foot that landed. Malformed
209
+ keys are refused by name (CONTACT_KEYS). FEET support and stride lines (default: off)
198
210
  --idle <name> the rig's animation read as its idle (default: idle). Every idle track the new
199
211
  animation does not key, on any channel, is listed as IDLE-DROPPED (a blink, a
200
212
  breath); one it keys is IDLE-KEPT-OUT, naming the track that keys it, and so is a
@@ -383,7 +395,7 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
383
395
  const RETARGET_ALIASES = { sequence: 'pose', animation: 'name' } as const;
384
396
 
385
397
  function retargetArgs(argv: string[]): RetargetArgs {
386
- const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'reference', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor', 'draw-order'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
398
+ const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'reference', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor', 'draw-order', 'contact-keys'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
387
399
  if (f.has('verbose') && f.has('summary')) throw new Usage('--verbose and --summary are both given; --verbose prints every line in full and --summary one line per measure, so give one');
388
400
  if (f.has('reference') && !f.has('build')) throw new Usage(`--reference needs --build: the record it goes in is written beside the build (${PROVENANCE_FILE})`);
389
401
  const bends = f.get('bends') ?? 'refuse';
@@ -433,6 +445,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
433
445
  ...reduceOf(f),
434
446
  output: f.has('verbose') ? 'verbose' : f.has('summary') ? 'summary' : 'default',
435
447
  ...(drawOrder !== undefined ? { drawOrder: 'depth' as const } : {}),
448
+ ...(f.has('contact-keys') ? { contactKeysFile: f.get('contact-keys')! } : {}),
436
449
  };
437
450
  }
438
451
 
@@ -558,7 +571,7 @@ function runClean(argv: string[], say: (l: string) => void): void {
558
571
  const input = need(f, 'pose');
559
572
  const out = need(f, 'out');
560
573
  const gait = f.get('depth');
561
- if (gait !== undefined && !(DEPTH_GAITS as readonly string[]).includes(gait)) throw new Usage(`--depth is "${gait}"; it must be ${DEPTH_GAITS.join(' or ')} (the gait decides which way a foreshortened shin leans: forward in a walk, back in a run)`);
574
+ if (gait !== undefined && !(DEPTH_GAITS as readonly string[]).includes(gait)) throw new Usage(`--depth is "${gait}"; it must be ${DEPTH_GAITS.join(' or ')} (the gait decides the joint --draw-order depth orders the legs by: the ankle in a walk, the knee in a run)`);
562
575
  const outPath = new Problems();
563
576
  const outDirs = planOutDirs([['--out', out]], outPath);
564
577
  outPath.throwIfAny();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
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/clean.ts CHANGED
@@ -116,9 +116,10 @@ export interface CleanParams {
116
116
  */
117
117
  hold?: { speed: number; reach: number } | 'off';
118
118
  /**
119
- * rig-c/pose#281: derive each frame's depth (`src/depth.ts`) for this gait, on
120
- * the cleaned positions. Absent, no depth is derived and the output is what it
121
- * was before #281.
119
+ * rig-c/pose#281: derive each frame's depth (`src/depth.ts`) on the cleaned
120
+ * positions, and record this gait (rig-c/pose#336: it decides the joint the
121
+ * draw order reads, not a sign). Absent, no depth is derived and the output is
122
+ * what it was before #281.
122
123
  */
123
124
  depth?: DepthGait;
124
125
  }
@@ -841,7 +842,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
841
842
  // rig-c/pose#281: derived on the cleaned positions, or a measured depth carried through as read.
842
843
  let depth: DerivedDepth | null = null;
843
844
  if (params.depth !== undefined) {
844
- depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {}), params.depth);
845
+ depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {}));
845
846
  depth.frames.forEach((d, i) => (frames[i]!.depth = d));
846
847
  record.params.depth = { gait: params.depth };
847
848
  } else if (carried === 'measured') seq.frames.forEach((f, i) => (frames[i]!.depth = f.depth ?? {}));
@@ -0,0 +1,250 @@
1
+ /**
2
+ * `--contact-keys` (rig-c/pose#303): the support declared by hand, frame by
3
+ * frame, for a clip no reading gets right — a drawn frontal walk. Seen from the
4
+ * front, depth is drawn as height, so the ground is a band in the picture and
5
+ * the nearer foot is drawn lower (#303's reading: no single floor line matches a
6
+ * contact table); a slow drawn walk can also step under the vertical speed's
7
+ * still tolerance, so #280 never takes over. The artist's key sheet already
8
+ * carries the contact table: these keys are that data, not a guess.
9
+ *
10
+ * `rig-pose-contact-keys/1`: `{ spec, keys: [{ frame, side }], note? }`, `side`
11
+ * one of `l | r | both | none`. Frames are the sequence's, may be fractional (a
12
+ * key sheet timed off the frame grid, as `--pelvis-keys`, #294), and strictly
13
+ * increase within [0, frames − 1].
14
+ *
15
+ * **A key holds until the next one** (a step function; the last holds to the
16
+ * clip's end). Contact is a state, not an angle: a foot is down or it is not, and
17
+ * there is no value between `l` and `r` to interpolate. A key at a fractional
18
+ * frame takes effect on the first frame at or after it. Frames before the first
19
+ * key are not covered: the support there is the reading without keys.
20
+ *
21
+ * Precedence: on every covered frame the keys decide, over #280's vertical
22
+ * speed (a frontal clip) and ankle height. `l`/`r`: that foot supports. `none`:
23
+ * flight, no foot supports. `both`: the foot that landed supports, as #280 hands
24
+ * over on landing. The run's neighbours are read from the keys, not the frames,
25
+ * so a key that reaches no frame counts (rig-c/pose#338): after an `l` or `r`
26
+ * key, the other side; after a `none` key (flight), or where the run starts the
27
+ * cover, the side the next `l` or `r` key names (the side that stays down after);
28
+ * else, at the cover's start, the support on the frame before (the reading
29
+ * without keys); a run none of those name keeps the reading without keys (`kept`).
30
+ */
31
+ import type { Feet, Side } from './legs.ts';
32
+ import { isObject, parseJson, Problems, refuseUnknownKeys, typeName } from './problems.ts';
33
+
34
+ export const CONTACT_KEYS_SPEC = 'rig-pose-contact-keys/1';
35
+
36
+ /** A contact key's value: the foot down, both, or none (flight). */
37
+ export type ContactSide = Side | 'both' | 'none';
38
+ const VALUES: readonly ContactSide[] = ['l', 'r', 'both', 'none'];
39
+
40
+ export interface ContactKeys {
41
+ /** The file's keys, as read: strictly increasing frames within the sequence's span. */
42
+ keys: Array<{ frame: number; side: ContactSide }>;
43
+ /** Per frame, the key in force (the last key at or before the frame), or null before the first key. */
44
+ key: Array<ContactSide | null>;
45
+ /** The frames of keys no frame takes: the next key comes before the next whole frame (two keys between two frames). */
46
+ unreached: number[];
47
+ }
48
+
49
+ /**
50
+ * `rig-pose-contact-keys/1`, read against a sequence of `frames` frames. Every
51
+ * problem is collected and refused by name (`CONTACT_KEYS`): a file that is not
52
+ * JSON or not the shape, a `spec` that is not this one, a `side` outside the
53
+ * four, a frame outside [0, frames − 1], a frame not after the key before it.
54
+ */
55
+ export function readContactKeys(text: string, where: string, frames: number): ContactKeys {
56
+ const doc = parseJson(text, where, 'CONTACT_KEYS');
57
+ const p = new Problems();
58
+ const shape = `{ "spec": "${CONTACT_KEYS_SPEC}", "keys": [{ "frame", "side" }], "note"? }`;
59
+ if (!isObject(doc)) {
60
+ p.add('CONTACT_KEYS', `${where} is ${typeName(doc)}; it must be ${shape}`);
61
+ p.throwIfAny();
62
+ throw new Error('unreachable');
63
+ }
64
+ refuseUnknownKeys(p, 'CONTACT_KEYS', where, doc, ['spec', 'keys', 'note']);
65
+ if (doc.spec !== CONTACT_KEYS_SPEC) p.add('CONTACT_KEYS', `${where} "spec" is ${typeName(doc.spec)}; it must be "${CONTACT_KEYS_SPEC}"`);
66
+ if ('note' in doc && typeof doc.note !== 'string') p.add('CONTACT_KEYS', `${where} "note" is ${typeName(doc.note)}; it must be a string`);
67
+ const keys: ContactKeys['keys'] = [];
68
+ if (!Array.isArray(doc.keys) || doc.keys.length === 0) p.add('CONTACT_KEYS', `${where} "keys" is ${typeName(doc.keys)}; it must be a non-empty array of { frame, side }`);
69
+ else doc.keys.forEach((k: unknown, i: number) => {
70
+ const kw = `${where} keys[${i}]`;
71
+ if (!isObject(k)) {
72
+ p.add('CONTACT_KEYS', `${kw} is ${typeName(k)}; a key is { frame, side }`);
73
+ return;
74
+ }
75
+ refuseUnknownKeys(p, 'CONTACT_KEYS', kw, k, ['frame', 'side']);
76
+ const okF = typeof k.frame === 'number' && Number.isFinite(k.frame) && k.frame >= 0 && k.frame <= frames - 1;
77
+ const okS = typeof k.side === 'string' && (VALUES as readonly string[]).includes(k.side);
78
+ if (!okF) p.add('CONTACT_KEYS', `${kw} "frame" is ${typeName(k.frame)}; it must be a frame of the sequence in [0, ${frames - 1}] (fractional allowed)`);
79
+ if (!okS) p.add('CONTACT_KEYS', `${kw} "side" is ${typeName(k.side)}; it must be one of l, r, both, none (the foot down, both feet, or flight)`);
80
+ if (!okF || !okS) return;
81
+ const prev = keys[keys.length - 1];
82
+ if (prev !== undefined && (k.frame as number) <= prev.frame) p.add('CONTACT_KEYS', `${kw} "frame" is ${k.frame as number}, not after the key before it (${prev.frame}); frames strictly increase`);
83
+ else keys.push({ frame: k.frame as number, side: k.side as ContactSide });
84
+ });
85
+ p.throwIfAny();
86
+ const key: Array<ContactSide | null> = [];
87
+ const reached = new Set<number>();
88
+ let at = -1;
89
+ for (let i = 0; i < frames; i++) {
90
+ while (at + 1 < keys.length && keys[at + 1]!.frame <= i) at++;
91
+ key.push(at < 0 ? null : keys[at]!.side);
92
+ if (at >= 0) reached.add(at);
93
+ }
94
+ return { keys, key, unreached: keys.filter((_, k) => !reached.has(k)).map((k) => k.frame) };
95
+ }
96
+
97
+ /** rig-c/pose#303: what the contact keys did to a clip's support, set on `Feet` by `contactKeysOf`. */
98
+ export interface ContactKeysReading {
99
+ keys: ContactKeys['keys'];
100
+ /** Per frame, the key in force, or null before the first key. */
101
+ key: Array<ContactSide | null>;
102
+ /** The first frame a key covers; every frame from it to the last is covered. */
103
+ from: number;
104
+ /** Per frame, the support read without the keys (#280's or ankle height's). */
105
+ before: Array<Side | null>;
106
+ /** Per frame, whether a key decided its support (false outside the cover, and on a `both` run no side names, `kept`). */
107
+ keyed: boolean[];
108
+ /** The frames of `both` runs that keep the reading without keys: no keyed support before, no single-foot key after. */
109
+ kept: number[];
110
+ /** Runs of frames whose support the keys changed: the support without them (`was`) and with them (`now`; null: flight). */
111
+ changed: Array<{ frames: [number, number]; was: Side | null; now: Side | null }>;
112
+ /** As `ContactKeys.unreached`. */
113
+ unreached: number[];
114
+ }
115
+
116
+ /**
117
+ * The support with the contact keys applied, last, over the reading `supportsOf`
118
+ * made (the header's precedence), then rebuilt as #280's frontal reading
119
+ * rebuilds it: support spans and changes from the per-frame support. Outside
120
+ * the cover the support is the reading without keys, unchanged. A step that
121
+ * released a plant (report only) and #113's `lowered` and #119's `tracked` runs
122
+ * are kept where they start before the cover, cut at the frame before it
123
+ * (rig-c/pose#338; a step's re-plant inside the cover is not read, null): inside
124
+ * it a plant ends where the keys hand support over. Jumps are kept as read (the
125
+ * hip's rise); on a jump frame a key still decides the support.
126
+ */
127
+ export function contactKeysOf(feet: Feet, keys: ContactKeys): Feet {
128
+ const n = feet.support.length;
129
+ const before = feet.support.map((s) => s ?? null);
130
+ const support = [...before];
131
+ const keyed = new Array<boolean>(n).fill(false);
132
+ const kept: number[] = [];
133
+ const other = (s: Side): Side => (s === 'r' ? 'l' : 'r');
134
+ const from = keys.key.findIndex((k) => k !== null);
135
+ /** Per frame, the index in `keys.keys` of the key in force (−1 before the first). */
136
+ const at: number[] = [];
137
+ for (let i = 0, k = -1; i < n; i++) {
138
+ while (k + 1 < keys.keys.length && keys.keys[k + 1]!.frame <= i) k++;
139
+ at.push(k);
140
+ }
141
+ for (let i = 0; i < n; i++) {
142
+ const k = keys.key[i] ?? null;
143
+ if (k === null) continue;
144
+ if (k !== 'both') {
145
+ support[i] = k === 'none' ? null : k;
146
+ keyed[i] = true;
147
+ continue;
148
+ }
149
+ let b = i;
150
+ while (b + 1 < n && keys.key[b + 1] === 'both') b++;
151
+ // rig-c/pose#338: the states either side come from the keys themselves, not from the frames — a key that reaches no frame (none@1.1 before both@1.2) is the state the run follows.
152
+ let k0 = at[i]!;
153
+ while (k0 > 0 && keys.keys[k0 - 1]!.side === 'both') k0--;
154
+ let k1 = at[b]! + 1;
155
+ while (k1 < keys.keys.length && keys.keys[k1]!.side === 'both') k1++;
156
+ const prevSide = k0 > 0 ? keys.keys[k0 - 1]!.side : null;
157
+ const nextSide = k1 < keys.keys.length ? keys.keys[k1]!.side : null;
158
+ const landed: Side | null = prevSide === 'r' || prevSide === 'l' ? other(prevSide) : nextSide === 'r' || nextSide === 'l' ? nextSide : prevSide === null && i > 0 ? support[i - 1]! : null;
159
+ for (let f = i; f <= b; f++) {
160
+ if (landed === null) kept.push(f);
161
+ else {
162
+ support[f] = landed;
163
+ keyed[f] = true;
164
+ }
165
+ }
166
+ i = b;
167
+ }
168
+ const supports: Feet['supports'] = [];
169
+ const changes: Feet['changes'] = [];
170
+ const changed: ContactKeysReading['changed'] = [];
171
+ for (let i = 0; i < n; i++) {
172
+ const s = support[i] ?? null;
173
+ if (s !== before[i]) {
174
+ const last = changed[changed.length - 1];
175
+ if (last !== undefined && last.frames[1] === i - 1 && last.was === before[i] && last.now === s) last.frames[1] = i;
176
+ else changed.push({ frames: [i, i], was: before[i]!, now: s });
177
+ }
178
+ if (s === null) continue;
179
+ const prev = i > 0 ? (support[i - 1] ?? null) : null;
180
+ if (prev !== s) changes.push({ frame: i, from: prev, to: s });
181
+ const last = supports[supports.length - 1];
182
+ if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
183
+ else supports.push({ side: s, frames: [i, i] });
184
+ }
185
+ const cut = <T extends { frames: [number, number] }>(runs: readonly T[] | undefined): T[] => (runs ?? []).filter((r) => r.frames[0] < from).map((r) => ({ ...r, frames: [r.frames[0], Math.min(r.frames[1], from - 1)] as [number, number] }));
186
+ const lowered = feet.lowered === undefined ? undefined : cut(feet.lowered);
187
+ const tracked = feet.tracked === undefined ? undefined : cut(feet.tracked);
188
+ return {
189
+ legLength: feet.legLength,
190
+ support,
191
+ supports,
192
+ jumps: feet.jumps,
193
+ // rig-c/pose#338: a step running into the cover ends on the frame before it, as lowered and tracked do; a re-plant inside the cover is the keys' to decide, so it is not read (null).
194
+ steps: cut(feet.steps).map((s) => (s.replant !== null && s.replant >= from ? { ...s, replant: null } : s)),
195
+ changes,
196
+ ...(lowered !== undefined && lowered.length > 0 ? { lowered } : {}),
197
+ ...(tracked !== undefined && tracked.length > 0 ? { tracked } : {}),
198
+ ...(feet.signal !== undefined ? { signal: feet.signal } : {}),
199
+ contactKeys: { keys: keys.keys, key: keys.key, from, before, keyed, kept, changed, unreached: keys.unreached },
200
+ };
201
+ }
202
+
203
+ /** The signal names a stride line uses for the frames a key did not decide. */
204
+ const SIGNAL_NAMES: Record<string, string> = { speed: 'ankle vertical speed', height: 'ankle height' };
205
+
206
+ /**
207
+ * The FEET lines of a run with contact keys (rig-c/pose#303), in place of #280's
208
+ * stride lines: one `support: contact keys` line — the keys, the frames they
209
+ * cover, how many they changed, the flight they declare, what stands outside —
210
+ * then one `stride` line per support span naming the signal that decided its
211
+ * frames (`contact keys`, else the reading without keys) and what that reading
212
+ * alone would have named.
213
+ */
214
+ export function contactKeysLines(feet: Feet): string[] {
215
+ const c = feet.contactKeys;
216
+ if (c === undefined) return [];
217
+ const n = feet.support.length;
218
+ const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
219
+ const changed = c.changed.reduce((s, r) => s + r.frames[1] - r.frames[0] + 1, 0);
220
+ const flight = c.key.filter((k) => k === 'none').length;
221
+ const tail = [
222
+ ...(flight > 0 ? [`${flight} keyed none (flight, no foot supports)`] : []),
223
+ ...(c.kept.length > 0 ? [`${c.kept.length} keyed both with no side to hold (no keyed support before, no single-foot key after) keep it`] : []),
224
+ ...(c.from > 0 ? [`frames ${span([0, c.from - 1])}, before the first key, keep it`] : []),
225
+ ...(c.unreached.length > 0 ? [`key(s) at ${c.unreached.join(', ')} reach no frame (the next key comes first)`] : []),
226
+ ];
227
+ const out = [`support: contact keys (rig-c/pose#303, --contact-keys) — ${c.keys.length} key(s) cover frames ${span([c.from, n - 1])} of ${n}, each holding until the next one; they change the reading without them on ${changed} frame(s)${tail.length > 0 ? `; ${tail.join('; ')}` : ''}`];
228
+ const by = feet.signal?.by;
229
+ for (const sp of feet.supports) {
230
+ let keyed = 0;
231
+ let differ = 0;
232
+ const rest = new Map<string, number>();
233
+ const was = new Set<string>();
234
+ for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
235
+ if (c.keyed[i]) keyed++;
236
+ else {
237
+ const name = SIGNAL_NAMES[by?.[i] ?? 'height'] ?? 'ankle height';
238
+ rest.set(name, (rest.get(name) ?? 0) + 1);
239
+ }
240
+ if (c.before[i] !== sp.side) {
241
+ differ++;
242
+ was.add(c.before[i] ?? 'none');
243
+ }
244
+ }
245
+ const parts = [...(keyed > 0 ? [`contact keys ${keyed}`] : []), ...[...rest].map(([k, v]) => `${k} ${v}`)];
246
+ const alone = differ === 0 ? 'the reading without keys names the same' : `the reading without keys would name ${[...was].join(' or ')} on ${differ} of them`;
247
+ out.push(`stride ${sp.side} ${span(sp.frames)}: of ${sp.frames[1] - sp.frames[0] + 1} frame(s), decided by ${parts.join(', ')}; ${alone}`);
248
+ }
249
+ return out;
250
+ }