@rig-c/pose 0.38.0 → 0.40.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
@@ -25,7 +25,7 @@ The map names **chains**: each limb as bones that descend from one another, pare
25
25
  ## Clean first: `rig-pose clean`
26
26
 
27
27
  ```sh
28
- rig-pose clean --pose dance.scan.json --out dance.pose.json [--loop] [--legs 5] [--rest 7] [--hips-inferred] [--hold off] [--hold-speed 0.015] [--hold-reach 0.03]
28
+ rig-pose clean --pose dance.scan.json --out dance.pose.json [--loop] [--legs 5] [--rest 7] [--hips-inferred] [--hold off] [--hold-speed 0.015] [--hold-reach 0.03] [--depth walk|run]
29
29
  ```
30
30
 
31
31
  `retarget` reads only a sequence that `clean` wrote ([#67](https://github.com/rig-c/pose/issues/67)). Where the video's camera moves, the order is **scan → clean → retarget** ([#80](https://github.com/rig-c/pose/issues/80)): a zoom step is a jump of the picture, and `clean`'s spike and smoothing steps would read it as motion of the body, so `tools/scan.py` takes the camera out of the raw sequence first and `clean` reads what it writes. A pose model reads each frame alone, so a hanging wrist moves ±20 px a frame and a cloak-hidden leg jumps 100+ px, and every key carries that shake. `clean` takes it out once, at the sequence, where it can be inspected. A sequence without the record is refused by name (`POSE_NOT_CLEANED`), and the message is the `clean` command to run on that file. There is no flag to skip the step. Windows of 1 (`--legs 1 --rest 1`) are a legitimate run that smooths nothing, and they are recorded as such.
@@ -113,7 +113,7 @@ A pose detector keeps little of a drawn pelvis tilt ([#288](https://github.com/r
113
113
  `--frames` is a folder of PNG frames, one per sequence frame, taken in file-name order and of the sequence's size (`PELVIS_FRAMES`, `PELVIS_FRAME`). There is no video decoding: extract the frames first. The hip keypoints place the reading — where the hips are, and which of them is the figure's right — but their angle is not read. Two readers, both on the hip span (the columns between the two hip keypoints):
114
114
 
115
115
  - **`silhouette`** (the default): the straightest edge of the drawn figure across the hip span, near the hips' height. An edge pixel is a figure pixel (a channel more than 64 from the background colour, the median of the frame's border) beside a background pixel, so a drawn outline and a filled shape both have one. Every straight line within ±40° of level and one hip width of the hips is scored by the edge pixels within 1 px of it; the best is read when its pixels cover at least 60 % of the span's columns, and fitted by least squares (one 3-MAD rejection pass) within 2.5 px rms. It reads a drawn pelvis line, or the edge of a drawn hip.
116
- - **`waistband`**: the top edge of a garment covering the hips (a skirt, shorts). The garment's colour is the median at the hips' midpoint, and must not be the background's. In each column of the span, the first row from 1.5 hip widths above the hips where three pixels in a row carry it (within 40 per channel) is the top edge, and the line is fitted through those tops on the same terms.
116
+ - **`waistband`**: the top edge of a garment covering the hips (a skirt, shorts). The garment's colour is the median at the hips' midpoint, and must not be the background's. In each column of the span, the first row from 1.5 hip widths above the hips where three pixels in a row carry it (within 40 per channel) is the top edge, and the line is fitted through those tops on the same terms. The colour must **hold** on its patch: at least 75 % of the patch's pixels carry its median ([#293](https://github.com/rig-c/pose/issues/293)). A dithered or paletted garment (a 216-colour GIF, whose channels alternate between palette steps pixel by pixel) does not, so its pixels are read through the mean of a 5×5 window, which does. Hips on the band's top edge give a patch that is half garment and half ground, so the patch is also tried one and two patch radii (0.3 hip widths) below the hips, and the scan then runs down to that patch. The first patch that holds and is not the background gives the colour, trying each height as is and then through the window mean. If none holds, the midpoint's median is read as before. The `PELVIS` line counts the frames read through the window mean and those whose colour was found below the hips.
117
117
 
118
118
  Both pass over the forearms: pixels within 0.15 hip widths of a segment elbow → wrist (1.5 times that of a wrist, the hand) are not read, and a waistband top right under a forearm is not taken. A frame neither reader can read is **declined**: its `pelvisTilt` is `null` — marked, not guessed — and a `DECLINED` line names the frames and the reason (the pelvis blank, no garment, no straight edge across 60 % of the span, the hips side-on or missing). The `PELVIS` line counts the frames read and declined and gives the swing read.
119
119
 
@@ -125,7 +125,50 @@ Both pass over the forearms: pixels within 0.15 hip widths of a segment elbow
125
125
 
126
126
  **`retarget`** keys the map's pelvis chain — one bone, joints `r_hip → l_hip` (either way round) — from the track on every frame it reads, and from the hip line on the frames it declined; such a bone is not held across the hip pair's foreshortening (#184), since the pair no longer sets its angle. The `PELVIS` line says which: `PELVIS: from <source>, swing N°` (N the peak-to-peak of the bone's rotate keys), with the frames on the hip line named; `PELVIS: from hip line (no track)` when the sequence has none; `PELVIS: from hip line (the <source> track read none of the N frames)` when it read nothing; and `PELVIS: not keyed — …` when the sequence has a track and the map no pelvis chain. The report carries it as `pelvis`. A sequence without a track keys exactly what it keyed before.
127
127
 
128
- On the synthetic walk with a drawn tilt ([`examples/walk/make-pelvis.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-pelvis.ts), the hip keypoints keeping 0.15 of it), posed by rig-c's poser from the gated build, the pelvis swings 30.67° against the drawn 30° (1.02×, r = 1.000) from the `silhouette` track, and 4.51° (0.15×) from the hip line without it; under a garment band whose top edge turns 24° peak to peak out of phase with the hips, the `waistband` track gives 23.76° (0.99×, r = 1.000 against the band's edge); with the pelvis painted out, no frame is read and the motion is the hip line's, byte for byte (`test/pelvis.test.ts`).
128
+ On the synthetic walk with a drawn tilt ([`examples/walk/make-pelvis.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-pelvis.ts), the hip keypoints keeping 0.15 of it), posed by rig-c's poser from the gated build, the pelvis swings 30.67° against the drawn 30° (1.02×, r = 1.000) from the `silhouette` track, and 4.51° (0.15×) from the hip line without it; under a garment band whose top edge turns 24° peak to peak out of phase with the hips, the `waistband` track gives 23.76° (0.99×, r = 1.000 against the band's edge); with the pelvis painted out, no frame is read and the motion is the hip line's, byte for byte (`test/pelvis.test.ts`). The same band reduced to the 216-colour palette by a 4×4 ordered dither (the `dithered` variant) is read on 24 of 24 frames, giving 23.95° (1.00×, r = 0.999 against the band's edge). With the hip keypoints also moved up onto the band's edge (`dithered-edge`) it is read on 24 of 24, giving 23.94° (1.00×, r = 0.999); the reader without the change reads none of either.
129
+
130
+ ## Derived depth: `clean --depth`
131
+
132
+ ```sh
133
+ rig-pose clean --pose run.scan.json --out run.pose.json --loop --depth run
134
+ ```
135
+
136
+ A sequence carries x and y only, so nothing downstream can say which limb is nearer the camera ([#281](https://github.com/rig-c/pose/issues/281)). Part of it is in the picture: a segment drawn shorter than its rest length leans out of the picture plane. With `--depth walk|run` (off by default; without it the output is byte for byte what it was), `clean` writes on every frame, for each limb segment whose two joints are placed, `"depth": { "<segment>": { "ratio", "sign", "source": "derived" } }`, read on the cleaned positions. The segments are `r_upper_arm`, `r_forearm`, `r_thigh`, `r_shin` and the same on the left, each from its joint nearer the trunk to the farther one.
137
+
138
+ - **`ratio`** is the segment's drawn length ÷ its rest length. The rest length is, per kind of segment, the larger of the two sides' 95th-percentile picture lengths over the clip: the straight length `--leg-scale` reads (#148). A segment drawn foreshortened on more than 5 % of the clip on both sides has a rest length shorter than its own, and its ratios read high: on the synthetic run below, the forearms (elbows bent 70° forward through the whole stride) rest at 41.05 px against their true 60.
139
+ - **`sign`** is +1 when the segment's far end is nearer the camera than its near end, −1 when it is farther, and 0 when the ratio is within the flat band, **0.95–1.05**, or above it (drawn longer than rest is not a lean). cos 18.2° = 0.95, so a segment within about 18° of the picture plane reads flat; the card set the same band for its flat fixture. Which way a foreshortened segment leans the length cannot say, so the body says it, "forward" being the way the trunk faces:
140
+
141
+ | segment | leans | why |
142
+ | --- | --- | --- |
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 |
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
+ | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
147
+
148
+ 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
+ - **`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
+
151
+ The record says it: `"depth": { "gait" }` in `params`, and the `CLEAN` line ends `depth derived per segment (gait <gait>)`. The `DEPTH` line gives the rest lengths, the band, the segment-frames toward, away and flat, and the frames the trunk faced the camera, faced away and was side-on. The sequence is written as `rig-pose-sequence/1.2` ([below](#the-pose-rig-pose-sequence1)).
152
+
153
+ **Drawn** — `tools/skeleton.ts` (not shipped, the demo's stick-figure pane) colours each segment a frame's `depth` names: toward the camera warm (orange), away cool (blue), flat neutral (grey), the rest of the figure thin and dark, R and L at the wrists and ankles, and a legend in the frame's top-left corner naming the source and the gait. A sequence without depth is drawn exactly as before.
154
+
155
+ **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
+
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.
158
+
159
+ ### Draw order from depth: `--draw-order depth`
160
+
161
+ ```sh
162
+ rig-pose retarget --rig rig/ --map map.json --sequence run.pose.json --out motion.json --name run --draw-order depth
163
+ ```
164
+
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.
166
+
167
+ - **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.
169
+ - **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.
170
+
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.
129
172
 
130
173
  ## What it does
131
174
 
@@ -135,7 +178,7 @@ rig-pose retarget --rig out/rig --map map.json --sequence dance.pose.json --out
135
178
 
136
179
  **Names** ([#243](https://github.com/rig-c/pose/issues/243)). `--sequence` and `--animation` are the names the rest of the toolchain gives these inputs (rig-c/exam's grader, `rigc render --animation`); `--pose` and `--name`, this command's first names, are the same flags. Both of a pair given with the same value is accepted; with different values the run is refused before anything is read, naming both (`--pose "a.json" and --sequence "b.json" are both given and differ; --sequence is another name for --pose, so give one`). Either spelling writes the same bytes.
137
180
 
138
- **Paths** ([#239](https://github.com/rig-c/pose/issues/239)). `--rig` is the directory @rig-c/parts writes, `<out>/rig/`: it reads `rig.json` and the `motion.json` beside it. Naming the `rig.json` itself reads no motion spec, so a rig that needs its own animations — a slider applies one — is red before anything is added, and the `RIG_GATE_BASE` line says the `motion.json` beside it was not read and names the directory form (or `--base-motion`). When the gate is green without it, the run goes on and `--out` carries none of the rig's own animations, so a `MOTION` line says the same thing, in every output mode ([#250](https://github.com/rig-c/pose/issues/250)). A directory `--out`, `--report` or `--camera` names that is not there yet is made once the run is green, and a `made directory <dir>` line says so and which paths it holds; a red run makes none, and a file where a directory has to be made is refused (`ARG_PATH`). The report is always written: without `--report` it goes beside the motion as `<out>.report.json` (`motion.json` → `motion.report.json`), and its `wrote` line says it was the default. A report that is already at that path is replaced, and the `wrote` line names when the replaced one was last written (its mtime, ISO 8601), so a second run to the same path is never silent. None of this changes the motion's bytes.
181
+ **Paths** ([#239](https://github.com/rig-c/pose/issues/239)). `--rig` is the directory @rig-c/parts writes, `<out>/rig/`: it reads `rig.json` and the `motion.json` beside it. Naming the `rig.json` itself reads no motion spec, so a rig that needs its own animations — a slider applies one — is red before anything is added, and the `RIG_GATE_BASE` line says the `motion.json` beside it was not read and names the directory form (or `--base-motion`). When the gate is green without it, the run goes on and `--out` carries none of the rig's own animations, so a `MOTION` line says the same thing, in every output mode ([#250](https://github.com/rig-c/pose/issues/250)). A directory `--out`, `--report` or `--camera` names that is not there yet is made once the run is green, and a `made directory <dir>` line says so and which paths it holds; a red run makes none, and a file where a directory has to be made, or a directory that cannot be written into, is refused (`ARG_PATH`) before anything is written; `--build` and `--render` make their own and are held to the same refusal. `clean --out`, `pelvis --out`, `convert --out` and `compare --report`/`--sheet` do the same ([#284](https://github.com/rig-c/pose/issues/284)). The report is always written: without `--report` it goes beside the motion as `<out>.report.json` (`motion.json` → `motion.report.json`), and its `wrote` line says it was the default. A report that is already at that path is replaced, and the `wrote` line names when the replaced one was last written (its mtime, ISO 8601), so a second run to the same path is never silent. None of this changes the motion's bytes.
139
182
 
140
183
  For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. When a head chain carries the eye line (`r_eye → l_eye`), the eyes' vertical travel against `neck` in the reference, scaled into the rig's units, keys a vertical `translate` nod channel on the head bone ([#111](https://github.com/rig-c/pose/issues/111)) clamped within `±2%` of the setup neck length (`head − neck`, [#181](https://github.com/rig-c/pose/issues/181)), while on sequences where that clamp acts, the clamped vertical nod (`clampedY`) is coupled in phase into `face.pitch`'s dial (`+2.8°/unit`, mapped through `face.pitch.range`) and the head bone's `rotate` track (`+1.6°/unit`, [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205); gains calibrated against the hand-polished reference `p1`). **How the nod's baseline is read** ([#241](https://github.com/rig-c/pose/issues/241)): on a sequence declared `"travel": "in-place"` it is frame 0, with no window, no clamp and so no coupling ([#242](https://github.com/rig-c/pose/issues/242)); otherwise it is a ±0.5 s running median of the eyes' height against the neck, averaged again over the same window, and the nod is the eyes' height less it. On a sequence `clean --loop` wrote (its `cleaned.params.loop`) and not declared in place, that window **wraps**, the last frame running into the first, as clean's own windows do; cut at the clip's ends instead, the two ends are read against one-sided baselines, and the coupling carries the difference into the head's rotate. On the tests' synthetic loop (#242's walk, 7 cycles of 24 frames, the eyes bobbing 3 px twice a cycle, `travel` deleted), the wrapped window puts cycle 7 on cycle 1 to 0.000° and 0.000 units; cut at the ends it was 1.057° on the rotate and 0.661 units on the nod. `"nod": false` on the eye-line chain ([below](#the-map-rig-pose-map1)) keys no nod and couples none. The `NOD` line says which of these happened. The result is one animation in rig-c's motion format (`rigc-motion/1`): a `rotate` track per mapped bone, a `translate` track on the hip (and on the head when the reference nods), one key per pose frame — and, when the map lists alternate drawings, an `attachment` track per slot they switch (and a `translate` track per bone a set offsets).
141
184
 
@@ -164,9 +207,11 @@ and what the run cannot decide for you it lists:
164
207
  - `SEAM` — rig-c's seam reading (`rigc render --seam`, rig-c ≥ 2.60.0) over the new animation, for the rig's declared neighbour pairs ([#216](https://github.com/rig-c/pose/issues/216)): one line per span of a **gap** (background inside the joint) or a **ghost** (one piece's overlap swung clear of its partner) over the one-cell floor, with the pair, the frames and their times, the rate and the most cells; per pair the frames rigc named that were one cell thick (`under the floor — not reported`), a pair not read and why, or rigc's SKIP (the two drawings share no cell at rest — nothing measured, never a seam that held); then `n span(s) over k pair(s) at <fps> fps — r of d declared pair(s) read`. **The pairs**: a rig spec carries none — `rig.json` holds bones, slots and skins — so they come from @rig-c/parts' `parts.json`, named with `--parts <file or directory>` (its build's `--out`, beside `rig/`): each limb's consecutive segments (a part's `limb` record: `name`, `segment`, and below a joint `joint`), a pair named twice counting once, each part drawn by the slot of its name — the pairs @rig-c/parts' own `check` reads (rig-c/parts#503). A pair whose part has no slot of its name is `not read`, named. Without `--parts`, or with a `parts.json` that holds no limb, the line is `SEAM no neighbour pair declared — <why>; seams: []`, and where the rig's parent directory holds a `parts.json` it says so. **The floor** is @rig-c/parts' so the two tools agree: a frame's gap or ghost counts when it holds more cells than its box is long (its longer side, in cells) — thicker than one cell; a crack one cell deep is the half-opacity edge moving by less than a cell. Its own render, at the rate the folds are read at; it moves no byte of the motion or the build, and never changes the exit or the gate. A malformed `parts.json` is an input and is refused (`PARTS_JSON`).
165
208
  - `JITTER` — one line per bone a chain keys (a face or body dial bone excepted), in map order ([#236](https://github.com/rig-c/pose/issues/236)): the mean |second difference| of its local rotation as rig-c's poser posed the gated build (its world matrix in its parent's axes, so the rotate key up to a constant), against the same figure of its map pair's picture angle less the pair of the nearest keyed bone above it, in °/frame², and the ratio. Both are read on the same triples, three consecutive frames on which every pose point the two pairs need is good. A pair whose figure rounds to 0 is still, and gets no ratio. A group's jitter ratio is its bones' together; the line names the bone that moved it. A reading, not a gate.
166
209
  - `REACH` — a `reach` chain ([below](#the-map-rig-pose-map1)) whose aim departs from its own pair's angle ([#236](https://github.com/rig-c/pose/issues/236)): why (its origin sits off the anchor's at setup, a lever; and the bones above it the animation turns, or translates other than by the anchor's travel), the largest departure with its frame, the departure's mean |second difference|, and the aim's against the pair's, in °/frame², with the ratio. A reach whose bone sits on the anchor's origin with nothing above it moved is aimed along the pair and prints none. This line also names, with `--raised-by-head` or a `"by-hip"` set, a placed arm out of reach (below); the two read `REACH chain …` and `REACH set …`. A reading, not a gate.
167
- - `POSTURE` — when the map has a pelvis chain (joints `r_hip`, `l_hip`) and a `translate`: the hip line's tilt range, and the sway — the x-range of mid_hip − mid_shoulder — in rig units and in shoulder widths, the pose's beside the posed rig's. A reading, not a gate. A frame on which either hip is `inferred` is left out of the tilt. When that is every frame, as after `clean --hips-inferred`, the line is `hip tilt not read: hips inferred (declared at clean)` and the report's `posture` has `"tilt": null` with `"tiltWhy"`; on some frames only, the tilt is read over the others and the line counts the frames left out (`posture.hipsInferred`). The source is read from the clean record, or is `source not stated` when the joints carry the state and the record does not. With a `measured` record ([#86](https://github.com/rig-c/pose/issues/86), [#106](https://github.com/rig-c/pose/issues/106)), the read, interpolated, scaled and held frames are the measurement, so the tilt is read on all four, while `unmeasured` frames (`fit.corr < 0.9`) are left out and named. The line adds `tilt measured (<method>) on N frame(s): R read, I interpolated, H held (held: <spans>)` (plus `scaled` and `unmeasured` spans when present). The report's `posture` carries `tiltMeasured`. The sway reads the hips as before. With `--plant-feet`, `POSTURE` also prints one `bounce` line per support span ([#107](https://github.com/rig-c/pose/issues/107), `posture.bounce` in the report): the detrended (centred ±12-frame moving mean removed) vertical standard deviation, rig/reference ratio and correlation for the hip, the neck and `eyes − neck`.
210
+ - `REACH-ORIGIN` — one line per `reach` chain ([#282](https://github.com/rig-c/pose/issues/282)): how far its bone's origin starts from the source joint it re-aims from (the translated joint, at the translated bone's origin), at setup and the most on the frames with its frame, in rig units and as a fraction of the segment (the pair's median length, scaled). Past **0.20** of the segment it is followed by `WARN REACH_ORIGIN: …`, naming the `JITTER` line that shows the cost. 0.20 is the lever's 1.25×: the aim from an origin a fraction *f* up the pair turns about 1 ÷ (1 − *f*) times the pair's turn — measured on a synthetic trunk with its origin 0, 15, 30, 45 and 75 units up a 150-unit segment, the chest's `JITTER` read 1.00×, 1.11×, 1.25×, 1.43× and 2.00×. A map without a `reach` chain prints neither. A reading, not a gate.
211
+ - `POSTURE` — when the map has a pelvis chain (joints `r_hip`, `l_hip`) and a `translate`: the hip line's tilt range, and the sway — the x-range of mid_hip − mid_shoulder — in rig units and in shoulder widths, the pose's beside the posed rig's. A reading, not a gate. A frame on which either hip is `inferred` is left out of the tilt. When that is every frame, as after `clean --hips-inferred`, the line is `hip tilt not read: hips inferred (declared at clean)` and the report's `posture` has `"tilt": null` with `"tiltWhy"`; on some frames only, the tilt is read over the others and the line counts the frames left out (`posture.hipsInferred`). The source is read from the clean record, or is `source not stated` when the joints carry the state and the record does not. With a `measured` record ([#86](https://github.com/rig-c/pose/issues/86), [#106](https://github.com/rig-c/pose/issues/106)), the read, interpolated, scaled and held frames are the measurement, so the tilt is read on all four, while `unmeasured` frames (`fit.corr < 0.9`) are left out and named. The line adds `tilt measured (<method>) on N frame(s): R read, I interpolated, H held (held: <spans>)` (plus `scaled` and `unmeasured` spans when present). The report's `posture` carries `tiltMeasured`. The sway reads the hips as before. When the pelvis bone was keyed from the sequence's pelvis track ([#295](https://github.com/rig-c/pose/issues/295)), the reference tilt is the track's on the frames it read and the hip line's on the frames it declined, and the line names it as the `PELVIS` line does (`reference 30.7° from silhouette (…; all 24 frames from the track)`, `posture.tiltTrack`). The track's turn also swings what the pelvis bone carries, so a second `sway:` line states that the rig's x-range includes the pelvis track's turn, how far it carries the trunk's origin, and the trunk alone — mid_shoulder about that origin — beside the reference (`posture.sway.carried`). Without a track the lines are unchanged. With `--plant-feet`, `POSTURE` also prints one `bounce` line per support span ([#107](https://github.com/rig-c/pose/issues/107), `posture.bounce` in the report): the detrended (centred ±12-frame moving mean removed) vertical standard deviation, rig/reference ratio and correlation for the hip, the neck and `eyes − neck`.
168
212
  - `SWITCH` — with `alternates` in the map ([below](#alternate-drawings-alternates)): each span on which a set of alternate drawings shows, its slots, its frames, how far the pose was from the set's key skeleton, how far each bone with a `drawn` angle was turned from it, and where each of its boundaries was moved to (`moved`, [below](#alternate-drawings-alternates)).
169
213
  - `PHASE` — one per set with a `phase` ([below](#alternate-drawings-alternates), [#286](https://github.com/rig-c/pose/issues/286)): the supporting foot it shows on, every span it showed and its frame count, and the support hand-over each span was switched on and off at (`on frame 19 r → l, off frame 31 l → r`; `none` where no foot supports). The report's `switches` row of such a set carries the same under `phase`: `{ step, side, on: { frame, from } | null, off: { frame, to } | null }`.
214
+ - `SWITCH-MIN` — one per set the minimum span held back ([#279](https://github.com/rig-c/pose/issues/279)): the runs on which it was the nearest set within the tolerance but shorter than `--switch-min`, so not shown, each with its frames, frame count and seconds (the first four, then how many more), and how many runs it was nearest on with the longest. A set that matched and was held back reads apart from one that never matched; a set with nothing held back prints no line, and the report's `switchRuns` lists every set.
170
215
  - `UNCOVERED` — with `alternates` in the map: every fold on a frame where no switch took the folding mesh off screen. This is the list of poses still to paint.
171
216
  - `RANGE` — a chain's `range` ([below](#a-joints-range-range)): a bone whose rotate key the pose asks past its range, outside every span of a set depicting its chain, is clamped at the limit; one line per run, with the frames, the seconds and the largest overflow. The demand for an alternate.
172
217
  - `ART-RANGE` — inside a set's span: a bone with a `drawn` angle in the set, turned from it past its limits — the set's `turn` for that bone (held at the limit) or, without one, its chain's `range` (the bone follows the pose). Listed as the pose asked it either way: the demand for another drawing of that set.
@@ -174,6 +219,8 @@ and what the run cannot decide for you it lists:
174
219
  - `BY-HIP` — with a set's `"place": "by-hip"` ([below](#alternate-drawings-alternates)): the `RAISED` line against the hip, one per span of such a set per chain it places. The unit is the hip width, the offset is from the mid-hip. `BY-HIP none` when the map has such a set and none of its spans showed.
175
220
  - `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.
176
221
  - `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
+ - `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.
177
224
  - `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.
178
225
  - `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.
179
226
  - `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.
@@ -196,7 +243,11 @@ and what the run cannot decide for you it lists:
196
243
 
197
244
  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.
198
245
 
199
- `--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`), `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:
246
+ `--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.
248
+
249
+ 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
+
200
251
  Report event objects with a pose `frame` or inclusive `frames` also carry exact `t` or `times` from `frameTimes`; existing `seconds` remain span lengths, with additive `spanSeconds`. Holds preserve their frame-index `from`/`to`. Existing scan joint/chain identities remain beside map-derived `bone` (a unique mapped target) or `bones` (all actual mapped targets); no joint-name heuristic chooses a target. `feet.jumps` stays an array of original frame pairs; `feet.jumpEvents` adds matching `{ frames, times }` objects. Fold, uncovered and seam rows preserve their discrete `times` and gaps, declare `domain: "render"`, and use the separate root `render` domain, never fabricated nearest pose-frame indices. The run hashes map/pose from the exact buffers passed to their readers, snapshots the resolved rig file before its gate, and rechecks all three before user-output writes. The writer validates coordinates before output writes and never changes motion keys. The switch diagnostic `moved.frame` is a selected-to-keyed pair with signed `frames`/`seconds` displacement; it is preserved as diagnostic data, not reinterpreted as an inclusive interval. An explicitly supplied uniform writer clock requires both positive `fps` and seconds offset `frame0`; canonical pose input does not yet declare FPS, so the retarget producer always writes exact `frameTimes`.
201
252
 
202
253
 
@@ -215,6 +266,7 @@ Report event objects with a pose `frame` or inclusive `frames` also carry exact
215
266
  The demand lists:
216
267
 
217
268
  - `switches: [{ set, chains, slots, hides, offsets, from, to, frames: [first, last], distance: { max, median }, artTurn: { <bone>: { max, median } }, moved: { on, off } }]` — one per span shown; `from` is the first frame's time, `to` the time the base art comes back (the animation's end when the span runs to it), `distance` in degrees over the frames shown, `artTurn` |wrap(target − drawn)| in degrees per bone the set has a `drawn` angle for. `moved.on` is the boundary the span starts on (null from the first frame), `moved.off` the one the base art or the next set comes back on (null to the end), each `{ frame: [selected, keyed], frames, seconds, window: [first, last], speed: { old, new } }`: signed frames and seconds, the frames it could move to, and the speed in degrees per frame on the old and the new frame. A boundary between two sets is the same entry in both.
269
+ - `switchRuns: [{ set, nearest: [{ frames: [first, last], count, seconds }], suppressed: [{ frames: [first, last], count, seconds }] }]` — one per set without a `phase`, in the map's order ([#279](https://github.com/rig-c/pose/issues/279)); `[]` when the map has none. `nearest` holds the runs of frames on which the set was the nearest within `--switch-tolerance` (inside its `frames` window), before bridging, hysteresis and the minimum span; `suppressed` the runs the minimum span dropped as shorter than `--switch-min` where they stood, on frames the set did not show in the end. `count` is the run's frames; `seconds` runs from the first frame's time to the next frame's (the animation's end for the last frame), the length `--switch-min` is compared with. A set with `phase` is not selected by distance and has no row.
218
270
  - `uncovered: [{ slot, attachment, chains, times }]` — every entry of `folds` on the times its slot still shows that mesh, with the map's chains that move it (the deepest: the arm, not the trunk it hangs from). `rigc render --folds` reads only what the pose shows, so a mesh swapped out or hidden by a switch is not read on those frames at all; `uncovered` is the fold times less the switched ones.
219
271
  - `endOn` — above, under the short hold.
220
272
  - `turns: [{ line, joints, frames, seconds, minFraction, reversed }]` — `line` is `hip`, `shoulder` or `eye`.
@@ -286,6 +338,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
286
338
  **`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp does not lift the hip by what it withheld, [#107](https://github.com/rig-c/pose/issues/107); on a supporting leg, the segment's `scalex` follows the asked scale down to the bound #95 derives from the leg's own range so the supporting ankle stays on #85's floor while the free leg keeps the configured clamp, and any frame clamped at #95's bound lifts the hip by the remainder so the ankle never sinks, listed as `LEG-SCALE past the floor (support)`, [#133](https://github.com/rig-c/pose/issues/133)), instead of moving it by the pose's mid_hip travel:
287
339
 
288
340
  - 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
+ - **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).
289
342
  - 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.
290
343
  - 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.
291
344
  - **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.
@@ -338,6 +391,7 @@ The retarget writes one key per pose frame on every mapped bone. That is faithfu
338
391
  - A scaled bone's `scalex` track is thinned within 5e-5 of its setup length: half of the scale landing check's 1e-4, so a reduced curve cannot fail that check.
339
392
  - 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.
340
393
  - 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
+ - **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.
341
395
  - **`--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).
342
396
  - 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).
343
397
 
@@ -363,7 +417,8 @@ Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2
363
417
  - Joints are body-18 (`nose neck r_shoulder r_elbow r_wrist l_shoulder l_elbow l_wrist r_hip r_knee r_ankle l_hip l_knee l_ankle r_eye l_eye r_ear l_ear`); `r`/`l` are the **subject's** sides. `state` is `observed`, `occluded` or `missing`, as in the keypoints file, or `inferred` ([#63](https://github.com/rig-c/pose/issues/63)): a position the detector supplied but did not see. An `inferred` joint carries `at` (one without it is refused, `POSE_JOINT_AT`), and `retarget` keys it as it keys `observed`. An unlisted joint is missing.
364
418
  - Two differences from the keypoints file: the unit is `picture-px`, and `at` may lie outside the picture (a hand leaves the frame; the score says how much to believe it).
365
419
  - More than one person: `--person <id>`.
366
- - **The pelvis track** ([#288](https://github.com/rig-c/pose/issues/288)) is the minor `rig-pose-sequence/1.1`: a `pelvisTilt` on every frame (beside `t` and `people`; Spine degrees in (−180, 180] of the direction r_hip → l_hip, or `null` where the reading declined the frame) and `pelvisSource` on the sequence (`"silhouette"`, `"waistband"` or `"keys"`), written by `rig-pose pelvis` ([above](#the-pelvis-track-rig-pose-pelvis)). A sequence carrying them holds one person; a track on some frames only, a source without a track, or a track without a source is refused by name (`POSE_PELVIS`). The writer writes the minor the content needs — `/1.1` with a track, `/1` without — so a sequence without one stays readable by a reader of `/1`; a `/1` file carrying a 1.1 key is refused naming the minor (`POSE_SPEC`). The spec string is what the file records of the version that wrote it: a file of a newer minor than this reader's is read, and each key it does not know is refused with the minor the file says it was written as (`… the file says it was written as "rig-pose-sequence/1.2", a newer minor than this reader's "rig-pose-sequence/1.1" — read it with a rig-pose that reads that minor`). A reader of `/1` (rig-pose 0.37 and before) refuses a `/1.1` file by its spec (`POSE_SPEC`, naming `"rig-pose-sequence/1.1"`) and its keys (`POSE_KEYS`).
420
+ - **The pelvis track** ([#288](https://github.com/rig-c/pose/issues/288)) is the minor `rig-pose-sequence/1.1`: a `pelvisTilt` on every frame (beside `t` and `people`; Spine degrees in (−180, 180] of the direction r_hip → l_hip, or `null` where the reading declined the frame) and `pelvisSource` on the sequence (`"silhouette"`, `"waistband"` or `"keys"`), written by `rig-pose pelvis` ([above](#the-pelvis-track-rig-pose-pelvis)). A sequence carrying them holds one person; a track on some frames only, a source without a track, or a track without a source is refused by name (`POSE_PELVIS`). The writer writes the minor the content needs — `/1.1` with a track, `/1` without — so a sequence without one stays readable by a reader of `/1`; a `/1` file carrying a 1.1 key is refused naming the minor (`POSE_SPEC`). The spec string is what the file records of the version that wrote it: a file of a newer minor than this reader's is read, and each key it does not know is refused with the minor the file says it was written as (`… the file says it was written as "rig-pose-sequence/1.3", a newer minor than this reader's "rig-pose-sequence/1.2" — read it with a rig-pose that reads that minor`). A reader of `/1` (rig-pose 0.37 and before) refuses a `/1.1` file by its spec (`POSE_SPEC`, naming `"rig-pose-sequence/1.1"`) and its keys (`POSE_KEYS`).
421
+ - **Depth** ([#281](https://github.com/rig-c/pose/issues/281)) is the minor `rig-pose-sequence/1.2`: a `depth` on every frame (beside `t` and `people`), `{ "<segment>": { "ratio", "sign", "source" } }` — the segments `r_upper_arm`, `r_forearm`, `r_thigh`, `r_shin`, `l_upper_arm`, `l_forearm`, `l_thigh`, `l_shin`, each present where its two joints are placed on the frame (`{}` where none is); `ratio` a number ≥ 0, `sign` −1, 0 or 1, `source` `"derived"` (`clean --depth`, [above](#derived-depth-clean---depth)) or `"measured"` (a 3D detector). A minor includes the keys of the minors before it, so a sequence with depth and a pelvis track is `/1.2`, and one with the track alone stays `/1.1`. A sequence with depth holds one person; depth on some frames only, a malformed entry, an unknown segment, derived and measured depth together, and derived depth without the `cleaned` record's `"depth": { "gait" }` (or that record without derived depth on every frame) are refused by name (`POSE_DEPTH`); a `/1` or `/1.1` file carrying `depth` is refused naming the minor (`POSE_SPEC`).
367
422
  - `travel` (optional, [#92](https://github.com/rig-c/pose/issues/92)) is the clip's frame of reference. Its one value is `"in-place"`: a gait filmed without the figure travelling, such as a treadmill or reel walk, or a loop. `clean --loop` writes it, and it may be written by hand on any sequence. Any other value is refused by name (`POSE_TRAVEL`). Without it the figure's travel in the picture is its travel. It is a property of the sequence, not of a flag ([#242](https://github.com/rig-c/pose/issues/242)): with or without `--plant-feet`, retarget reads the supports in place, skips the camera-zoom normalisations (a loop has no zoom, and their rolling windows do not wrap) and reads the head nod without its detrend window or clamp. Only the following foot and the loop closure need `--plant-feet` ([below](#the-legs-foreshortening-and-planted-feet)).
368
423
  - `score` is whatever the pose model gives. RTMPose's goes above 1, so `--min-score` is a cut on that model's scale, not a probability.
369
424
 
@@ -396,7 +451,7 @@ Which bones carry which joints. You write it for your rig:
396
451
  - A chain's bone *k* carries the segment from joint *k* to joint *k*+1, so it lists one joint more than bones; each bone descends from the one before (a control bone between them is fine). body-18 has no toe and no hand point, so a leg ends at the ankle and an arm at the wrist: a rig's foot (or hand) bone below them stays out of `bones`, unkeyed, turning with its parent. A chain one joint short whose last joint is an ankle or a wrist is refused (`MAP_CHAIN_JOINTS`) naming that case and the bone to leave out ([#240](https://github.com/rig-c/pose/issues/240)).
397
452
  - `aim`, one entry per bone: `"x"` (default) — the bone's own x axis is the segment; right for a limb link @rig-c/parts turned along its limb. A point — the direction from this bone's origin to that point; right for a trunk bone, whose own axis parts leaves at 0°. `"a>b"` — the direction from point a to point b; right for a head, turned by its eye line: `{ "bones": ["head"], "joints": ["r_eye", "l_eye"], "aim": ["eye_r>eye_l"] }`, or for a pelvis, turned by its hip line: `{ "name": "pelvis", "bones": ["hip"], "joints": ["r_hip", "l_hip"], "aim": ["leg_r0>leg_l0"] }`.
398
453
  - A **point** of the rig is a bone's origin (`name`), the end of its x axis (`name:tip`), or the midpoint of two of those (`"a+b"`: `"sleeve_r0+sleeve_l0"` is the shoulders' midpoint). An aim's points must be the bone itself or descend from it.
399
- - `reach: true`, on a one-bone chain from the translated joint (a trunk from `mid_hip`): the bone is aimed from where its origin is posed — the hip moved and turned — at where the pose puts the segment's end relative to mid_hip, scaled. Without it, a tilted pelvis carries the chest's origin sideways and the trunk, parallel to the pose's, misses the shoulders. Its aim is one point, from the bone's own origin: `{ "name": "trunk", "bones": ["chest"], "joints": ["mid_hip", "mid_shoulder"], "aim": ["sleeve_r0+sleeve_l0"], "reach": true }`. The chain must descend from the translated bone. **It is not free** ([#236](https://github.com/rig-c/pose/issues/236)): the aim is taken from the bone's posed origin at the pair's end, so an origin partway up the pair is a lever that turns the bone by more than the pair turns — on the example wave's trunk, its origin half way up its 150-unit reach, 2.00× the pair's mean |second difference|, carried into the head and arms that ride it — and a turning hip adds its own turn on top. It pays where a pelvis chain turns the hip, which otherwise carries the chest's origin off the pose's line; on a level hip, the trunk aimed along the pair keeps the pair's jitter and misses the shoulders only by its origin's offset swung through the trunk's lean. `JITTER` and `REACH` print what it cost.
454
+ - `reach: true`, on a one-bone chain from the translated joint (a trunk from `mid_hip`): the bone is aimed from where its origin is posed — the hip moved and turned — at where the pose puts the segment's end relative to mid_hip, scaled. Without it, a tilted pelvis carries the chest's origin sideways and the trunk, parallel to the pose's, misses the shoulders. Its aim is one point, from the bone's own origin: `{ "name": "trunk", "bones": ["chest"], "joints": ["mid_hip", "mid_shoulder"], "aim": ["sleeve_r0+sleeve_l0"], "reach": true }`. The chain must descend from the translated bone. **It is not free** ([#236](https://github.com/rig-c/pose/issues/236)): the aim is taken from the bone's posed origin at the pair's end, so an origin partway up the pair is a lever that turns the bone by more than the pair turns — on the example wave's trunk, its origin half way up its 150-unit reach, 2.00× the pair's mean |second difference|, carried into the head and arms that ride it — and a turning hip adds its own turn on top. It pays where a pelvis chain turns the hip, which otherwise carries the chest's origin off the pose's line; on a level hip, the trunk aimed along the pair keeps the pair's jitter and misses the shoulders only by its origin's offset swung through the trunk's lean. **The trade** ([#282](https://github.com/rig-c/pose/issues/282)): `reach` lands the trunk on the pose's end point from wherever its origin is posed, and pays in turn the pose does not have, more the farther the origin starts from the source joint; `reach: false` follows the measured angle — the pair's own turn and jitter — and lets the end point miss by the origin's offset. A trunk whose origin sits well up the pair on a hip that does not turn is the case to leave it off. `JITTER` and `REACH` print what it cost, and `REACH-ORIGIN` prints where it starts, with a `WARN` past 0.20 of the segment.
400
455
  - `bend`: every joint inside a chain is a hinge. `"rest"` (default) takes the side it may bend from the rig's rest pose and refuses a rest that is straight (under 0.5°); `"ccw"`/`"cw"` state it; `"free"` skips the check. Both bend refusals (`MAP_BEND_SIDE`, `POSE_BEND_ONE_WAY`) name `"free"` on the chain they refuse, and when it is right — the warning below ([#238](https://github.com/rig-c/pose/issues/238)).
401
456
  - `nod: false`, on the chain whose joints carry the eye line (`r_eye → l_eye`, [#241](https://github.com/rig-c/pose/issues/241)): the head keys no nod translate, and nothing is coupled into its rotate or the face pitch; the `NOD` line says so. Absent or `true`, the nod is read ([above](#what-it-does)). `nod` on a chain with no eye line, or anything but a boolean, is refused (`MAP_NOD`). It is a key of the chain rather than a flag because the nod is that chain's reading: the map is where a rig's author says what its head does, and a flag would have to name the chain it turns off.
402
457
  ⚠️ **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.
@@ -445,7 +500,7 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
445
500
  - `phase` (optional, [#286](https://github.com/rig-c/pose/issues/286)): `"left-step"` or `"right-step"`, the stepping (swinging) foot a gait set is drawn for and named by ([below](#alternate-drawings-alternates)). left-step: the left foot is stepping; shown while the right foot supports. A right-step drawing mirrors it: the right foot is stepping, and it shows while the left foot supports. Refused with any other value, or beside `frames` (`MAP_ALT_PHASE`).
446
501
  - Each name resolves against the built rig and the map, and a miss is refused by name, listing what exists: `MAP_ALT_SLOT`, `MAP_ALT_ATTACHMENT`, `MAP_ALT_HIDES`, `MAP_ALT_CHAIN`, `MAP_ALT_JOINT`, `MAP_ALT_POSE`, `MAP_ALT_DRAWN`, `MAP_ALT_OFFSET` (a bone that is not a bone of the set's chains), `MAP_ALT_PLACE`, `MAP_ALT_SYMMETRIC`, `MAP_ALT_SUPPORT`, `MAP_ALT_ANCHOR` and `MAP_ALT_PHASE`.
447
502
 
448
- **When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton, with a foreshortened segment (`SHORT`) weighted at `0.5 ×` (`SHORT_SWITCH_WEIGHT`, [#180](https://github.com/rig-c/pose/issues/180)). On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; once entered, a set stays active until its distance exceeds `--switch-tolerance + 15°` (`SWITCH_LEAVE_HYSTERESIS`, [#180](https://github.com/rig-c/pose/issues/180)) for at least `--switch-min` (default 0.25 s) or a competing set with a lower distance enters within `--switch-tolerance`; a run of one set shorter than `--switch-min` is dropped and the base art shows there. Sets compete only with those they share a slot (shown or hidden) or an offset bone with, so a drawing of each arm can show at once; two that compete never overlap.
503
+ **When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton, with a foreshortened segment (`SHORT`) weighted at `0.5 ×` (`SHORT_SWITCH_WEIGHT`, [#180](https://github.com/rig-c/pose/issues/180)). On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; once entered, a set stays active until its distance exceeds `--switch-tolerance + 15°` (`SWITCH_LEAVE_HYSTERESIS`, [#180](https://github.com/rig-c/pose/issues/180)) for at least `--switch-min` (default 0.25 s) or a competing set with a lower distance enters within `--switch-tolerance`; a run of one set shorter than `--switch-min` is dropped and the base art shows there. The runs it dropped are not silent: the `SWITCH-MIN` line names each set held back with the runs and their lengths, and the report's `switchRuns` lists, per set, every run it was nearest on and every run the minimum suppressed ([#279](https://github.com/rig-c/pose/issues/279)). Sets compete only with those they share a slot (shown or hidden) or an offset bone with, so a drawing of each arm can show at once; two that compete never overlap.
449
504
 
450
505
  **A gait set: `phase`.** A walk's drawings are painted per step side, and which one is right depends on which foot is planted, not on which key skeleton is nearest: on a walk the nearest-angle set can be the wrong side for half a stride ([#286](https://github.com/rig-c/pose/issues/286); measured on a synthetic walk in place, the two sets without `phase` showed one drawing on all 168 frames, 84 of them on the wrong support). A set with a `phase` is named by the foot that is stepping — left-step: the left foot is stepping; shown while the right foot supports — and is not selected by distance: it shows exactly on its supporting foot's spans, the `feet.supports` the `FEET` line prints, and switches on the hand-over — no tolerance, no `--switch-min`, no hysteresis, no bridging, and `--switch-at peak` does not move its boundaries. On a frame where no foot supports (a jump, a released step) neither shows and the base art does. The supports are read with or without `--plant-feet` (with it, from the lower ankle, [#113](https://github.com/rig-c/pose/issues/113)); a run that reads no feet — a map without both legs, a pose without both hips and ankles — is refused naming the sets (`PHASE_FEET`). Sets that compete (a common slot or offset bone) must all declare a `phase` or none, and at most one of each side (`PHASE_GROUP`): nothing would decide between a phase set and an angle set, or two drawings of one step. Sets without `phase` are selected as above, byte for byte.
451
506
 
package/cli.ts CHANGED
@@ -9,9 +9,11 @@
9
9
  import { readFileSync, writeFileSync } from 'node:fs';
10
10
  import pkg from './package.json' with { type: 'json' };
11
11
  import { clean, cleanLine, holdRegions, unheldText, untestedText, HOLD_BLEND, HOLD_MIN_SECONDS, HOLD_REACH, HOLD_SPEED, HOLD_SPEED_WINDOW, LEGS, OCCLUSION_SCORE, OCCLUSION_WINDOW, SPIKE_TRUNKS, SPIKE_WINDOW, windowProblem } from './src/clean.ts';
12
- import { convertCoco17, readPoseSequence, writePoseSequence } from './src/pose.ts';
12
+ import { convertCoco17, DEPTH_GAITS, readPoseSequence, writePoseSequence, type DepthGait } from './src/pose.ts';
13
+ import { FLAT_BAND, SIDE_ON } from './src/depth.ts';
13
14
  import { parseWindows } from './src/compare.ts';
14
- import { lineOf, Refusal } from './src/problems.ts';
15
+ import { lineOf, Problems, Refusal } from './src/problems.ts';
16
+ import { makeOutDirs, planOutDirs } from './src/outdir.ts';
15
17
  import { runCompare, type CompareArgs } from './src/runcompare.ts';
16
18
  import { PELVIS_MODES, runPelvis, type PelvisMode } from './src/pelvis.ts';
17
19
  import { PROVENANCE_FILE } from './src/provenance.ts';
@@ -21,7 +23,7 @@ const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig
21
23
 
22
24
  Usage:
23
25
  rig-pose pelvis --pose <pose.json> --out <pose.pelvis.json> (--frames <dir> [--mode silhouette|waistband] | --pelvis-keys <keys.json>) [--person <id>]
24
- rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--hold off] [--coco] [--person <id>]
26
+ rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--hold off] [--depth walk|run] [--coco] [--person <id>]
25
27
  rig-pose retarget --rig <rig> --map <map.json> --sequence <pose.json> --out <motion.json> [--animation dance] [flags]
26
28
  rig-pose convert --coco <coco17.json> --out <pose.json>
27
29
  rig-pose compare --reference <pose.json> --render <pose.json> --map <map.json> [flags]
@@ -72,6 +74,17 @@ clean (required before retarget; rig-c/pose#67)
72
74
  --hold off turn the stillness hold off (rig-c/pose#91); recorded as hold: "off"
73
75
  --hold-speed <n> the hold's speed limit, trunk lengths per frame (default: 0.015)
74
76
  --hold-reach <n> how far a held joint may lie from its span median, trunk lengths (default: 0.03)
77
+ --depth walk|run derive each limb segment's depth on every frame (rig-c/pose#281; off by default, and
78
+ without it the output is unchanged): "depth": { "<segment>": { ratio, sign, source:
79
+ "derived" } }, written as rig-pose-sequence/1.2. ratio = drawn length ÷ rest length (the
80
+ larger side's 95th-percentile length over the clip); sign +1 when the segment's far end is
81
+ nearer the camera, -1 farther, 0 flat (ratio within 0.95–1.05, or above it) or undecided
82
+ (the trunk side-on). Which way a foreshortened segment leans comes from the body, with
83
+ "forward" the trunk's facing: thigh and forearm forward (a hip flexes forward, an elbow
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
87
+ by it, and retarget --report orders the limbs front to back by it (layers)
75
88
 
76
89
  In order, each step printed with its figures: (1) spikes — a joint more than 0.2 trunk lengths
77
90
  (neck → mid_hip, the median over the clip) from its 5-frame median takes the median, each listed by
@@ -176,6 +189,12 @@ retarget
176
189
  holds the upper arm the forearm is re-aimed at the wrist. ART-RANGE: a forearm still past
177
190
  its turn, or a held arm leaving the wrist > 0.01 eye distances short of its placed point
178
191
  (the shortfall listed). RAISED lines (default: off; rig-c/pose#62)
192
+ --draw-order depth key the legs' draw order from the sequence's depth (clean --depth; rig-c/pose#304): on
193
+ 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'
195
+ slots must fill one unbroken run of the setup draw order (else the pair is not keyed and
196
+ 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)
179
198
  --idle <name> the rig's animation read as its idle (default: idle). Every idle track the new
180
199
  animation does not key, on any channel, is listed as IDLE-DROPPED (a blink, a
181
200
  breath); one it keys is IDLE-KEPT-OUT, naming the track that keys it, and so is a
@@ -197,7 +216,11 @@ retarget
197
216
  --plant-feet and --stance, a scaled bone's scalex is thinned within 5e-5 of its setup
198
217
  length, a bone under one is re-solved through its parent as reduced, and each ankle
199
218
  is held to what the reduced bones above it may move it by (rig-c/pose#94), printed
200
- at the end as "ankle drift allowed" in units and leg lengths (rig-c/pose#240)
219
+ at the end as "ankle drift allowed" in units and leg lengths (rig-c/pose#240).
220
+ --reduce <deg>,<chain>=<deg>,… gives a map chain its own tolerance, the first item every
221
+ other chain's (rig-c/pose#283): legs held tight against slide while the trunk is smoothed,
222
+ e.g. --reduce 3,leg_r=0.2,leg_l=0.2; each LAND line and ankle allowance follows its
223
+ chain's tolerance, and the run line, the REDUCE lines and the report name them
201
224
  --reduce-hip <units> with --reduce, thin the hip's translate keys too, its world origin within units
202
225
  of the dense keys (default: the hip stays dense)
203
226
  --reduce-keep-planted with --reduce and --plant-feet, keep every frame of each support span on the
@@ -360,7 +383,7 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
360
383
  const RETARGET_ALIASES = { sequence: 'pose', animation: 'name' } as const;
361
384
 
362
385
  function retargetArgs(argv: string[]): RetargetArgs {
363
- 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'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
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);
364
387
  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');
365
388
  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})`);
366
389
  const bends = f.get('bends') ?? 'refuse';
@@ -369,6 +392,8 @@ function retargetArgs(argv: string[]): RetargetArgs {
369
392
  if (switchAt !== 'peak' && switchAt !== 'first') throw new Usage(`--switch-at is "${switchAt}"; it must be peak or first`);
370
393
  const profile = f.get('profile') ?? 'spine';
371
394
  if (profile !== 'spine' && profile !== 'spine-html') throw new Usage(`--profile is "${profile}"; it must be spine or spine-html`);
395
+ const drawOrder = f.get('draw-order');
396
+ if (drawOrder !== undefined && drawOrder !== 'depth') throw new Usage(`--draw-order is "${drawOrder}"; it must be depth (the order is keyed from the sequence's depth, rig-c/pose#304)`);
372
397
  const name = f.get('name') ?? 'dance';
373
398
  if (!/^[A-Za-z0-9_.-]+$/.test(name)) throw new Usage(`${argv.includes('--name') || !argv.includes('--animation') ? '--name' : '--animation'} is "${name}"; an animation name here is letters, digits, _ . -`);
374
399
  return {
@@ -407,6 +432,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
407
432
  merge: f.get('merge'),
408
433
  ...reduceOf(f),
409
434
  output: f.has('verbose') ? 'verbose' : f.has('summary') ? 'summary' : 'default',
435
+ ...(drawOrder !== undefined ? { drawOrder: 'depth' as const } : {}),
410
436
  };
411
437
  }
412
438
 
@@ -427,7 +453,7 @@ function floorOf(f: Map<string, string>): { floor?: number } {
427
453
  }
428
454
 
429
455
  /** --reduce and --reduce-hip (rig-c/pose#32): off unless given; the hip only with the bones. --reduce-keep-planted (rig-c/pose#251): only with both --reduce and --plant-feet. */
430
- function reduceOf(f: Map<string, string>): { reduce?: number; reduceHip?: number; reduceKeepPlanted?: true } {
456
+ function reduceOf(f: Map<string, string>): { reduce?: number; reduceChains?: Record<string, number>; reduceHip?: number; reduceKeepPlanted?: true } {
431
457
  if (f.has('reduce-keep-planted')) {
432
458
  const missing = ['reduce', 'plant-feet'].filter((x) => !f.has(x));
433
459
  if (missing.length > 0) throw new Usage(`--reduce-keep-planted is given without ${missing.map((x) => `--${x}`).join(' and ')}; it keeps the support spans --plant-feet reads at dense keys inside the reduction --reduce makes`);
@@ -436,9 +462,22 @@ function reduceOf(f: Map<string, string>): { reduce?: number; reduceHip?: number
436
462
  if (f.has('reduce-hip')) throw new Usage('--reduce-hip is given without --reduce; the hip is thinned with the bones, not alone');
437
463
  return {};
438
464
  }
439
- const reduce = num(f, 'reduce', 0, (n) => n > 0 && n < 180, 'degrees in (0, 180)');
440
465
  const keep = f.has('reduce-keep-planted') ? { reduceKeepPlanted: true as const } : {};
441
- return f.has('reduce-hip') ? { reduce, reduceHip: num(f, 'reduce-hip', 0, (n) => n > 0, 'rig units > 0'), ...keep } : { reduce, ...keep };
466
+ // rig-c/pose#283: "<deg>,<chain>=<deg>,…" — the first item every chain's tolerance, then a map chain's own.
467
+ const [head, ...rest] = f.get('reduce')!.split(',');
468
+ const reduce = rest.length === 0 ? num(f, 'reduce', 0, (n) => n > 0 && n < 180, 'degrees in (0, 180)') : Number(head);
469
+ if (rest.length > 0 && (head === '' || !(reduce > 0 && reduce < 180))) throw new Usage(`--reduce is "${f.get('reduce')}"; its first item "${head}" must be degrees in (0, 180), every chain's tolerance, before any <chain>=<deg>`);
470
+ const chains: Record<string, number> = {};
471
+ for (const item of rest) {
472
+ const m = /^([^=\s]+)=(.*)$/.exec(item);
473
+ if (m === null) throw new Usage(`--reduce is "${f.get('reduce')}"; "${item}" is not <chain>=<deg> (a chain the map names, then its tolerance in degrees)`);
474
+ if (Object.hasOwn(chains, m[1]!)) throw new Usage(`--reduce is "${f.get('reduce')}"; chain "${m[1]}" is given twice`);
475
+ const v = Number(m[2]);
476
+ if (m[2] === '' || !Number.isFinite(v) || !(v > 0 && v < 180)) throw new Usage(`--reduce is "${f.get('reduce')}"; chain "${m[1]}" is "${m[2]}", and it must be degrees in (0, 180)`);
477
+ chains[m[1]!] = v;
478
+ }
479
+ const per = rest.length === 0 ? {} : { reduceChains: chains };
480
+ return f.has('reduce-hip') ? { reduce, ...per, reduceHip: num(f, 'reduce-hip', 0, (n) => n > 0, 'rig units > 0'), ...keep } : { reduce, ...per, ...keep };
442
481
  }
443
482
 
444
483
  function compareArgs(argv: string[]): CompareArgs {
@@ -515,10 +554,15 @@ function runsOf(frames: number[]): string {
515
554
 
516
555
  /** `rig-pose clean` (#67): read, clean, print each step's figures, write. */
517
556
  function runClean(argv: string[], say: (l: string) => void): void {
518
- const f = parseFlags(argv, ['pose', 'out', 'legs', 'rest', 'person', 'hold', 'hold-speed', 'hold-reach'], ['loop', 'coco', 'hips-inferred']);
557
+ const f = parseFlags(argv, ['pose', 'out', 'legs', 'rest', 'person', 'hold', 'hold-speed', 'hold-reach', 'depth'], ['loop', 'coco', 'hips-inferred']);
519
558
  const input = need(f, 'pose');
520
559
  const out = need(f, 'out');
521
- const params = { loop: f.has('loop'), legs: windowOf(f, 'legs', 5), rest: windowOf(f, 'rest', 7), hipsInferred: f.has('hips-inferred'), hold: holdOf(f) };
560
+ 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)`);
562
+ const outPath = new Problems();
563
+ const outDirs = planOutDirs([['--out', out]], outPath);
564
+ outPath.throwIfAny();
565
+ const params = { loop: f.has('loop'), legs: windowOf(f, 'legs', 5), rest: windowOf(f, 'rest', 7), hipsInferred: f.has('hips-inferred'), hold: holdOf(f), ...(gait !== undefined ? { depth: gait as DepthGait } : {}) };
522
566
  const text = readFileSync(input, 'utf8');
523
567
  const seq = f.has('coco') ? convertCoco17(text, input) : readPoseSequence(text, input);
524
568
  const c = clean(seq, params, f.get('person'), input);
@@ -555,7 +599,13 @@ function runClean(argv: string[], say: (l: string) => void): void {
555
599
  }
556
600
  say(` HOLDS ${c.holds.length} found over ${new Set(c.holds.map((x) => x.joint)).size} joint(s) (per joint: a span ≥ ${HOLD_MIN_SECONDS} s still by speed < ${h.speed} trunk/frame and reach ≤ ${h.reach} trunk; ${HOLD_BLEND}-frame blend in and out)`);
557
601
  }
602
+ if (c.depth !== null) {
603
+ const d = c.depth;
604
+ const rest = (['upper_arm', 'forearm', 'thigh', 'shin'] as const).map((k) => `${k} ${d.rest[k] === null ? 'not placed' : fmt(d.rest[k]!)}`).join(', ');
605
+ say(` DEPTH derived per segment for a ${params.depth} (rig-c/pose#281): ratio = drawn ÷ rest, rest the larger side's 95th-percentile length (${rest} px); flat within ${FLAT_BAND[0]}–${FLAT_BAND[1]}; toward ${d.signs.toward}, away ${d.signs.away}, flat or undecided ${d.signs.flat} segment-frame(s); the trunk faced the camera on ${d.facing.toward}, away on ${d.facing.away}, side-on (under ${SIDE_ON} trunk wide, sign 0) on ${d.facing.side} frame(s)`);
606
+ } else if (c.seq.frames.some((x) => x.depth !== undefined)) say(' DEPTH measured depth (a 3D detector\'s) carried through as read');
558
607
  if (c.inferred !== null) say(` INFERRED r_hip on ${c.inferred.r_hip} frame(s), l_hip on ${c.inferred.l_hip} frame(s) of ${n} marked "inferred" (every frame where it has a position): declared at clean (--hips-inferred); positions and scores unchanged`);
608
+ makeOutDirs(outDirs, say);
559
609
  writeFileSync(out, writePoseSequence(c.seq));
560
610
  say(` CLEAN ${cleanLine(c.record)}`);
561
611
  say(` wrote ${out}`);
@@ -605,7 +655,11 @@ export function main(argv: string[]): number {
605
655
  const f = parseFlags(rest, ['coco', 'out'], []);
606
656
  const input = need(f, 'coco');
607
657
  const out = need(f, 'out');
658
+ const p = new Problems();
659
+ const outDirs = planOutDirs([['--out', out]], p);
660
+ p.throwIfAny();
608
661
  const seq = convertCoco17(readFileSync(input, 'utf8'), input);
662
+ makeOutDirs(outDirs, (l) => process.stdout.write(`${l}\n`));
609
663
  writeFileSync(out, writePoseSequence(seq));
610
664
  process.stdout.write(`rig-pose convert: ${seq.frames.length} frame(s) of COCO-17 → body-18; wrote ${out}\n`);
611
665
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.38.0",
3
+ "version": "0.40.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
@@ -65,7 +65,8 @@
65
65
  * Pure: no clock, no randomness, no disk.
66
66
  */
67
67
  import { BODY18, type Body18 } from './joints.ts';
68
- import type { CleanRecord, CleanSeam, CleanSpike, CleanUnheld, Joint, PoseFrame, PoseSequence } from './pose.ts';
68
+ import { deriveDepth, depthSourceOf, type DerivedDepth } from './depth.ts';
69
+ import type { CleanRecord, CleanSeam, CleanSpike, CleanUnheld, DepthGait, Joint, PoseFrame, PoseSequence } from './pose.ts';
69
70
 
70
71
  /** A joint not held inside a region where others are (#91): the test it fails there and its figure. */
71
72
  export type Unheld = CleanUnheld;
@@ -114,6 +115,12 @@ export interface CleanParams {
114
115
  * always passes one.
115
116
  */
116
117
  hold?: { speed: number; reach: number } | 'off';
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.
122
+ */
123
+ depth?: DepthGait;
117
124
  }
118
125
 
119
126
  /** The joints `--hips-inferred` marks. */
@@ -191,6 +198,8 @@ export interface Cleaned {
191
198
  * and the largest change smoothing made, degrees; null without a track.
192
199
  */
193
200
  pelvis: { source: string; window: number; read: number; frames: number; moved: number } | null;
201
+ /** rig-c/pose#281: the depth derived with `depth` (its rest lengths and counts); null without it. */
202
+ depth: DerivedDepth | null;
194
203
  }
195
204
 
196
205
  /**
@@ -668,6 +677,10 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
668
677
  }
669
678
  const track = personTrack(seq, wanted, where);
670
679
  const n = track.length;
680
+ // rig-c/pose#281: depth a sequence already carries — a 3D detector's is carried through, clean's own is read again or refused.
681
+ const carried = depthSourceOf(seq.frames);
682
+ if (carried === 'measured' && params.depth !== undefined) p.add('CLEAN_DEPTH', `${where} carries measured depth (a 3D detector's) and --depth ${params.depth} asks to derive it; a derived reading would replace a measurement — clean without --depth, which carries the measured depth through`);
683
+ if (carried === 'derived' && params.depth === undefined) p.add('CLEAN_DEPTH', `${where} carries derived depth (clean --depth), read off positions this clean moves; give --depth walk|run to read it again on the cleaned positions`);
671
684
  const person = wanted ?? [...new Set(seq.frames.flatMap((f) => f.people.map((q) => q.id)))].sort()[0]!;
672
685
  if (params.loop) {
673
686
  const widest = Math.max(SPIKE_WINDOW, params.legs, params.rest);
@@ -825,6 +838,13 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
825
838
  record.params.pelvis = { window: params.legs };
826
839
  pelvis = { source: seq.pelvisSource, window: params.legs, read: tilt.filter((v) => v !== null).length, frames: n, moved };
827
840
  }
841
+ // rig-c/pose#281: derived on the cleaned positions, or a measured depth carried through as read.
842
+ let depth: DerivedDepth | null = null;
843
+ if (params.depth !== undefined) {
844
+ depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {}), params.depth);
845
+ depth.frames.forEach((d, i) => (frames[i]!.depth = d));
846
+ record.params.depth = { gait: params.depth };
847
+ } else if (carried === 'measured') seq.frames.forEach((f, i) => (frames[i]!.depth = f.depth ?? {}));
828
848
  const { cleaned: _was, ...rest } = seq;
829
849
  return {
830
850
  // #92: a loop is one cycle of a gait repeated in place, so it carries no travel of its own.
@@ -843,6 +863,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
843
863
  unheld,
844
864
  seams,
845
865
  pelvis,
866
+ depth,
846
867
  };
847
868
  }
848
869
 
@@ -862,7 +883,8 @@ export function cleanLine(r: CleanRecord, hold: (r: CleanRecord) => string = hol
862
883
  const jit = (v: number | null) => (v === null ? 'not measured' : v.toFixed(2));
863
884
  const hips = r.params.hipsInferred === undefined ? '' : `; hips inferred (${inferredSource(r)})`;
864
885
  const pelvis = r.params.pelvis === undefined ? '' : `; pelvisTilt smoothed as its own track over ${r.params.pelvis.window === 1 ? '1 frame (none)' : `${r.params.pelvis.window} frames`}`;
865
- return `${r.spec}: ${windows}, ${loop ? 'loop' : 'no loop'}; ${spikes}; jitter (mean |second difference| of y) ${jit(r.jitter.before)} → ${jit(r.jitter.after)} px/f²${hold(r)}${hips}${pelvis}`;
886
+ const depth = r.params.depth === undefined ? '' : `; depth derived per segment (gait ${r.params.depth.gait})`;
887
+ return `${r.spec}: ${windows}, ${loop ? 'loop' : 'no loop'}; ${spikes}; jitter (mean |second difference| of y) ${jit(r.jitter.before)} → ${jit(r.jitter.after)} px/f²${hold(r)}${hips}${pelvis}${depth}`;
866
888
  }
867
889
 
868
890
  /**