@rig-c/pose 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/cli.ts +19 -6
- package/package.json +1 -1
- package/src/clean.ts +5 -4
- package/src/contacts.ts +250 -0
- package/src/depth.ts +53 -13
- package/src/draworder.ts +74 -23
- package/src/legs.ts +7 -0
- package/src/pose.ts +1 -1
- package/src/retarget.ts +17 -9
- package/src/run.ts +26 -9
- package/src/switch.ts +11 -4
package/README.md
CHANGED
|
@@ -141,10 +141,12 @@ A sequence carries x and y only, so nothing downstream can say which limb is nea
|
|
|
141
141
|
| segment | leans | why |
|
|
142
142
|
| --- | --- | --- |
|
|
143
143
|
| thigh | forward | a hip flexes forward, and extends back too little to foreshorten past the band |
|
|
144
|
-
| shin |
|
|
144
|
+
| shin | back | the owner's walk statement ([#336](https://github.com/rig-c/pose/issues/336)): the lifted foot is behind the supporting leg. A knee folds the shin back, in a walk and in a run |
|
|
145
145
|
| upper arm | with the opposite leg | in a gait each arm swings with the other side's leg: forward when the other thigh is the more foreshortened, back when its own is, forward (a shoulder flexes forward) when neither is |
|
|
146
146
|
| forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
|
|
147
147
|
|
|
148
|
+
The thigh's row is the owner's run statement ([#336](https://github.com/rig-c/pose/issues/336)): the lifted leg is in front, its knee protruding. The table is the same for both gaits, so `--depth walk` and `--depth run` write the same signs; the gait the flag records decides which joint `--draw-order depth` orders the legs by ([below](#draw-order-from-depth---draw-order-depth)).
|
|
149
|
+
|
|
148
150
|
The trunk faces the camera (forward is +) when its left shoulder and hip are drawn right of its right ones, away (forward is −) when they are drawn left, and is **side-on** when the shoulders and hips are drawn, on average, less than 0.1 trunk lengths (neck → mid-hip) wide: forward is then across the picture, so every sign on that frame is 0 — undecided, not guessed.
|
|
149
151
|
- **`source`** is `"derived"`. `"measured"` is for a 3D detector, which may fill the same field: it is read, never written here. `clean` carries a measured depth through as it was read (`DEPTH measured depth … carried through`), and refuses `--depth` on one (`CLEAN_DEPTH`), since a derived reading would replace a measurement. A sequence carrying derived depth is refused without `--depth` (`CLEAN_DEPTH`): its depth was read off positions the clean moves.
|
|
150
152
|
|
|
@@ -154,7 +156,7 @@ The record says it: `"depth": { "gait" }` in `params`, and the `CLEAN` line ends
|
|
|
154
156
|
|
|
155
157
|
**Layered** — `retarget` orders the limbs (`r_arm`, `l_arm`, `r_leg`, `l_leg`) front to back on every frame of a sequence that carries depth. Each joint's depth is its parent's plus its segment's, sign × rest × √(1 − ratio²) px (rest = drawn length ÷ ratio), from the shoulders and hips at 0; a limb's depth is the mean of its middle and end joints'; the nearest comes first, and equal depths (to 0.01 px) share a layer. The `DEPTH` line counts the frames ordered and which limb was in front alone how often, and the report carries `layers` ([below](#what-it-does)). Nothing is keyed from it unless `--draw-order depth` asks ([below](#draw-order-from-depth---draw-order-depth)): without the flag the motion is byte for byte the same with and without depth.
|
|
156
158
|
|
|
157
|
-
On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-depth.ts): the walk's proportions, made in 3D from joint angles and drawn by a camera in front, behind or beside, so the generator knows every joint's true depth; `test/depth.test.ts`): on the frontal run in place, the 24 shin-frames drawn at ≤ 0.5 of rest (down to 0.409), four on every stride of each leg, read −1 under `--depth run`, as the generator draws them, and
|
|
159
|
+
On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-depth.ts): the walk's proportions, made in 3D from joint angles and drawn by a camera in front, behind or beside, so the generator knows every joint's true depth; `test/depth.test.ts`): on the frontal run in place, the 24 shin-frames drawn at ≤ 0.5 of rest (down to 0.409), four on every stride of each leg, read −1 under `--depth run` and under `--depth walk`, as the generator draws them, and the two files differ only in the gait they record; on the frontal walk in place, the 28 shin-frames the generator draws away from the camera (the lifted foot behind) read −1, and none reads +1; seen from behind, the run's kicked-back shins read +1; seen from the side, the 480 segment-frames read 0.995–1.000, every sign 0, and no segment is drawn warm or cool. `layers` agrees with the generator on 60 of the run's 60 frames and 48 of the walk's 48, two limbs counting as ordered when their true depths differ by at least the depth a limb can hold while both its segments read flat (√(1 − 0.95²) × (upper + lower ÷ 2): 31.2 px for an arm, 35.9 for a leg), and tied otherwise.
|
|
158
160
|
|
|
159
161
|
### Draw order from depth: `--draw-order depth`
|
|
160
162
|
|
|
@@ -162,13 +164,19 @@ On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/ri
|
|
|
162
164
|
rig-pose retarget --rig rig/ --map map.json --sequence run.pose.json --out motion.json --name run --draw-order depth
|
|
163
165
|
```
|
|
164
166
|
|
|
165
|
-
With `--draw-order depth` (off by default; without it nothing changes) `retarget` keys the legs' draw order from the sequence's depth ([#304](https://github.com/rig-c/pose/issues/304)), so a
|
|
167
|
+
With `--draw-order depth` (off by default; without it nothing changes) `retarget` keys the legs' draw order from the sequence's depth ([#304](https://github.com/rig-c/pose/issues/304)), so a walk's lifted foot is drawn behind the supporting leg and a run's lifted leg, its knee forward, in front ([#336](https://github.com/rig-c/pose/issues/336)). It writes rig-c's draw-order timeline, `"drawOrder": [{ "t", "offsets": [{ "slot", "offset" }] }, …]` on the animation beside its `tracks`: each key moves slots against the setup order, and a key with no `offsets` is the setup order again. A key's `t` is half a frame before the frame it shows on (0 on the first frame), as the switches' attachment keys are: a key exactly on a frame time is a tie, and the render breaks it a frame late on about half of them ([#325](https://github.com/rig-c/pose/issues/325)).
|
|
166
168
|
|
|
167
169
|
- **The pair.** The legs: a leg is the map's chains naming its knee or ankle, their bones and the bones under them no other chain maps. Its slots are the slots on those bones. A slot of an alternate set (`alternates`) on none of them goes with the leg the set depicts, so a set keeps its own switch and takes the order of the leg it replaces. The legs are keyed when their slots overlap in the draw order, filling one unbroken run of the setup order. A slot of anything else drawn between them would be re-layered by the swap, and the setup order is not changed here. The pair is then not keyed, and the line names that slot. The arms across the trunk are a later card.
|
|
168
|
-
- **The rule.** On each frame the leg whose depth
|
|
170
|
+
- **The rule.** On each frame the leg whose depth is the lower is drawn behind: its slots first in the pair's run, in their setup order, then the other's. The depths must differ by at least a tie, the depth a flat chain can hold there. Inside the tie, or where a leg is not ordered, the order before holds. Before the first decided frame it is the setup order, or on a looped sequence the order the clip ends in. A key is written where the order changes.
|
|
171
|
+
- **The joint the gait names** ([#336](https://github.com/rig-c/pose/issues/336), the owner's two statements). On a sequence cleaned with `--depth walk` a leg's depth is its ankle's (the hip at 0, plus the thigh's, plus the shin's), the tie √(1 − 0.95²) × (thigh + shin) of the rest lengths (46.83 px on the fixtures): in a walk the lifted foot is behind the supporting leg. With `--depth run` it is its knee's (the thigh's), the tie √(1 − 0.95²) × thigh (24.97 px): in a run the lifted leg is in front, its knee protruding. The limb mean (`layers`, above) says neither: a lifted leg's thigh forward and shin back cancel in it, and a walk whose thighs stay inside the band leaves it at most half a shin (35 px on the fixtures), under its tie of √(1 − 0.95²) × (thigh + shin ÷ 2) (35.89 px). A measured depth names no gait and keeps the limb mean and its tie ([#304](https://github.com/rig-c/pose/issues/304)).
|
|
169
172
|
- **No depth.** On a sequence without depth the flag is refused (`DRAWORDER_NO_DEPTH`): the order would come from nowhere, and a motion written without it would read as ordered.
|
|
173
|
+
- **A walk cleaned before #336** ([#336](https://github.com/rig-c/pose/issues/336)). A derived depth whose foreshortened shin leans the way the trunk faces carries the sign table before #336, where a walk's lifted foot read forward; the ankle would draw that foot in front. The flag refuses it by name (`DEPTH_RECLEAN`), listing the frames, until it is cleaned again with this version. The table since leans the shin back in both gaits, so a record it wrote never matches. A run's signs did not change, so a run cleaned by an earlier release is read as it is, and so is a walk with no foreshortened shin (its signs read the same under both tables). Measured depth is not checked. The file format does not change.
|
|
174
|
+
|
|
175
|
+
On #281's fixtures, retargeted onto a limb-cut rig whose legs overlap (the left leg drawn first; `test/draworder.test.ts`):
|
|
170
176
|
|
|
171
|
-
|
|
177
|
+
- **The walk** (by the ankle): each of the 36 frames with one leg lifted draws that leg behind; the order changes on frames 3, 15, 27 and 39, where a lifted foot first goes back past the tie, and holds through the double support between. The generator orders the true ankles on 16 of the 48 frames, and the keyed order agrees on all 16.
|
|
178
|
+
- **The run** (by the knee): the order changes on frames 2, 12, … 52. Of the 42 frames with one leg lifted, the 30 whose lifted knee protrudes (its thigh leaning forward past the band) draw that leg in front. The other 12 (frames 0–1, 10–11, …) are the early swing: the lifted thigh is still inside the band, the generator's knees are within the tie, and the order holds — the landing leg in front, as the generator's ankles have it. The generator orders the true knees on 42 of the 60 frames, and the keyed order agrees on all 42. On frame 12, on a rig that draws the right leg first, none of the 8,763 pixels the lifted right leg draws shows the standing leg, against 2,192 in the setup order.
|
|
179
|
+
- **Read otherwise**: by the limb mean or by the ankle, the run draws the lifted leg behind on all 30 frames whose knee protrudes; by the limb mean or by the knee, the walk keys nothing (the setup order) and draws the lifted right leg in front on its 18 frames. Knees planted 10 px apart, inside the knee tie, hold the order before; without the tie they swap it.
|
|
172
180
|
|
|
173
181
|
## What it does
|
|
174
182
|
|
|
@@ -220,7 +228,7 @@ and what the run cannot decide for you it lists:
|
|
|
220
228
|
- `REACH` — with `--raised-by-head`, or a `"by-hip"` set: a placed span whose target was out of the arm's reach, too far or too near. It names the frames, which way, and the shortfall in eye distances (hip widths for `"by-hip"`). The wrist was placed at the reach limit instead. This is not `ART-RANGE`: the rig's proportions were the limit, not the art's range. A too-near frame is placed at the limit like a too-far one, and graded at the placed distance.
|
|
221
229
|
- `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length in px and as a share of `FEET`'s leg length, the spread of its picture length over it, the limits in px, the frames clamped at each limit, where its leg was free on clamped frames how far the clamps on that leg left the free ankle above or below the picture's (in the rig's leg at setup, thigh origin → ankle, and in `FEET`'s px; `legScale[].free` in the report, [#237](https://github.com/rig-c/pose/issues/237)), and (with `--plant-feet`) one `past the floor (support)` line per supporting run that asked under the floor ([#133](https://github.com/rig-c/pose/issues/133), `legScale[].support` in the report).
|
|
222
230
|
- `DEPTH` — when the sequence carries depth ([above](#derived-depth-clean---depth)): where it came from, on how many frames the limbs were ordered front to back, and which limb was in front alone how often — `DEPTH layers from the sequence's derived depth (clean --depth run): the limbs ordered front to back on N of N frame(s); in front alone: r_arm a, l_arm b, r_leg c, l_leg d; tied in front on k (the report's layers)`.
|
|
223
|
-
- `DRAWORDER` — with `--draw-order depth` ([above](#draw-order-from-depth---draw-order-depth)): the keys and the spans they key, per leg drawn behind, cut to 260 characters — `DRAWORDER from depth, 6 key(s) over 3 span(s) (leg_r behind leg_l
|
|
231
|
+
- `DRAWORDER` — with `--draw-order depth` ([above](#draw-order-from-depth---draw-order-depth)): the keys and the spans they key, per leg drawn behind, cut to 260 characters — `DRAWORDER from depth by the knee (run; tie 24.97 px), 6 key(s) over 3 span(s) (leg_r behind leg_l 2–11, 22–31, 42–51)`, naming the joint and its tie for a keyed pair ([#336](https://github.com/rig-c/pose/issues/336)) — and a line per pair not keyed, saying why.
|
|
224
232
|
- `PELVIS` — when the map has a pelvis chain (one bone, joints `r_hip → l_hip`) or the sequence a pelvis track ([above](#the-pelvis-track-rig-pose-pelvis)): where the pelvis bone's rotation came from and its swing — `PELVIS: from <source>, swing N°`, or `PELVIS: from hip line (no track)` — with the frames keyed from the hip line on a track that declined them.
|
|
225
233
|
- `PIVOT` — when the map translates a bone and has both legs: the translated bone's origin against its thigh roots' midpoint at setup, in units and hip widths — the pivot every hip turn swings the legs about.
|
|
226
234
|
- `FEET` — when the map has both legs: one `segment` line per mapped leg segment with its picture length ÷ straight length (min · median · max), the scale `--leg-scale` would ask before its clamp, printed with or without the flag so the minimum can be read before choosing the range ([#237](https://github.com/rig-c/pose/issues/237)); the supports and jumps read off the reference ankles (planted or not), and on the posed build, outside the jumps, the lowest ankle's vertical range and each support span's worst vertical range and horizontal drift of the supporting ankle, in leg lengths.
|
|
@@ -244,7 +252,7 @@ and what the run cannot decide for you it lists:
|
|
|
244
252
|
Each figure is formatted as the full line formats it, from the same record. Refusals print the same in every mode, and the exit code and the gate do not change with it; `--verbose` with `--summary` is a usage error.
|
|
245
253
|
|
|
246
254
|
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `inputs` (consumed rig spec, map and pose files, each with invocation-relative `path` and exact-byte `sha256`; a folder rig resolves to its actual rig.json; these three identities do not claim to bind optional base motion, sets or images), `frames`, `duration`, `frameTimes` (the exact motion-key time for each pose frame; never FPS inferred from duration), `render` (the separate renderer sampling domain, `{ fps, frames }` from frame-set metadata, or `null` when not measured), `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included; `reduceKeepPlanted: true` only with `--reduce-keep-planted`; `reduceChains` only with a per-chain `--reduce`), `scale`, `scaleFit` (the `SCALE` reading: `segment`, `leg`, `hip`, `outside`, `travel`; `null` without a scale), `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `jitter`, `reach`, `posture`, `folds`, `seams`, `seamPairs`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `pelvis` (the `PELVIS` reading, [above](#the-pelvis-track-rig-pose-pelvis): `{ source, trackSource, bone, chain, swing, track, hipLine, declined }` — `source` the track's or `"hip line"`, `trackSource` the sequence's `pelvisSource` or `null`, `swing` the peak-to-peak of the bone's rotate keys in degrees, `track` and `hipLine` the frames each keyed, `declined` the frames the track declined as `[first, last]` pairs; `null` when the map has no pelvis chain and the sequence no track), `feet` (`planted`, `legLength`, `straight`, `rigLeg`, `segments`, `supports`, `jumps`, `jumpEvents`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; `straight` is each side's 95th-percentile picture length hip → ankle in px and `rigLeg` each side's rig leg at setup (thigh origin → ankle) in world units, both `null` without both legs; `segments` holds one `{ chain, bone, joints, straight, ratio: { min, median, max }, why? }` per mapped leg segment, read with or without `--leg-scale` (`straight` and `ratio` `null`, and `why` said, where it was not measured), [#237](https://github.com/rig-c/pose/issues/237); a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed?, moved? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths; `moved`, on a `lifted` or `below` run, the frames whose free ankle a `--leg-scale` clamp moved off the pose's height, as `legScale[].free` is written, [#237](https://github.com/rig-c/pose/issues/237)), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances, each row of `REDUCE` and `ankleDrift`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
|
|
247
|
-
With a sequence that carries depth ([#281](https://github.com/rig-c/pose/issues/281), [above](#derived-depth-clean---depth)) the report also carries `layers`, one `{ frame, t, order, depth }` per frame: `order` the limbs front to back as layers, each an array of the limbs that share it (`[["r_arm"], ["l_arm", "r_leg"], ["l_leg"]]`), and `depth` each ordered limb's depth in px, + toward the camera. With `--draw-order depth` ([#304](https://github.com/rig-c/pose/issues/304)) it carries `drawOrder`: `{ source: "depth", pairs, keys, spans }`. `pairs` gives per pair `{ a, b, limbs, slots, keyed, tie }` (and `why` when not keyed), `keys` the timeline with each key's `frame
|
|
255
|
+
With a sequence that carries depth ([#281](https://github.com/rig-c/pose/issues/281), [above](#derived-depth-clean---depth)) the report also carries `layers`, one `{ frame, t, order, depth }` per frame: `order` the limbs front to back as layers, each an array of the limbs that share it (`[["r_arm"], ["l_arm", "r_leg"], ["l_leg"]]`), and `depth` each ordered limb's depth in px, + toward the camera. With `--draw-order depth` ([#304](https://github.com/rig-c/pose/issues/304)) it carries `drawOrder`: `{ source: "depth", pairs, keys, spans }`. `pairs` gives per pair `{ a, b, limbs, slots, keyed, tie }` (`by`, `"knee"` or `"ankle"`, when the sequence's gait names the joint the pair was read at, [#336](https://github.com/rig-c/pose/issues/336), the tie then that joint's; and `why` when not keyed), `keys` the timeline with each key's `frame` (the frame it first shows on; `t` there is that frame's time, as everywhere in the report, and the motion's key sits half a frame before it), and `spans` every span of one order `{ behind, front, frames, keyed }` (`keyed` false where the setup order already draws it so). The options carry `drawOrder: "depth"`. A run without the flag writes neither. A sequence without depth writes no `layers` key, and its report is what it was.
|
|
248
256
|
|
|
249
257
|
With a map that has a `reach` chain ([#282](https://github.com/rig-c/pose/issues/282)) the report also carries `reachOrigin`, one `{ chain, bone, joints, anchor, setup, posed: { max, frame, t }, segment, ofSegment, warn, threshold }` per reach chain in map order: the `REACH-ORIGIN` line's figures. `setup` and `posed.max` are the distance from the bone's origin to the translated bone's (where the rig puts the source joint), in rig units; `segment` the reached length; `ofSegment` the larger distance ÷ `segment`; `warn` whether it is past `threshold` (0.2). A map without one writes no `reachOrigin` key.
|
|
250
258
|
|
|
@@ -339,6 +347,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
|
|
|
339
347
|
|
|
340
348
|
- The supporting foot is the one whose reference ankle is lower in the picture. It changes only when the other ankle has been at least **0.02 leg lengths** lower for at least **0.1 s** (timed as `END-ON` runs are), and once confirmed takes support from the frame it crossed that margin ([#102](https://github.com/rig-c/pose/issues/102)); a leg length is each side's median picture length hip → ankle, and the switch compares against their mean.
|
|
341
349
|
- **Frontal views** ([#280](https://github.com/rig-c/pose/issues/280)). Seen from the front, a foot's depth is drawn as height: the nearer foot is the lower one, and it may be the one in the air. So every clip is also read by a second signal, the ankle's **vertical speed**: a planted foot moves with the ground — still under a fixed camera, or sliding back at the ground's speed in a gait drawn in place. Each side's ground speed is the median of its ankle's vertical step over the frames outside every jump (a walking foot is down on more than half of them), and a foot is down on a frame when the step into it or the step out of it departs from that speed by less than **0.01 leg lengths** (the re-plant's stillness); both feet can be down. The two signals **contradict** on a frame where the lower ankle (by 0.02 leg lengths) moves while the higher moves with the ground. A clip is read as frontal when that happens on more than **5 %** of the frames outside the jumps **and** its stride shows in depth: on each side the ankle's range against its hip (5th to 95th percentile) is larger in y than in x — a side view's stride is in x, and its stance ankle rises and falls with the leg's angle, so there the vertical speed does not decide. 5 % sits below the shortest contradiction a frontal walk makes (one frame a step: 2 of the 24 frames of a 1 s cycle, 8.3 %) and above every fixture the tests read in depth (0 %); the tests' synthetic frontal walk contradicts on 18 of 96 frames (18.8 %). On a frontal clip each frame's support is the vertical speed's: the one foot down; on frames both are down, the foot that landed, so support passes on the frame it lands (as #102 takes it from the landing frame); ankle height's only where neither foot moves with the ground. A plant then ends where the other foot lands, so no step releases one and #113's and #119's readings are not made. On a frontal clip `FEET` prints two `support:` lines — the frames contradicted and their share against 5 %, then the ankle ranges — and one `stride` line per support span: the signal that decided its frames, the frames both feet were down, and what ankle height alone would have read there, e.g. `stride l 8–15: ankle vertical speed on 8 frame(s) (both feet down on 3); ankle height alone would read r on 2 of them`. On any other clip ankle height decided as before and nothing is printed, in any output mode, so such a run's output is as it was. On the tests' synthetic frontal walk (an 8-drawing cycle on twos interpolated to 24 fps, both feet down on 4 of the 8 drawings, the nearer foot lower, the ankles moving 17 and 23 px in y) the feet read down are its contact table on all 96 frames, where ankle height alone supported a foot in the air on 16. The third signal the card names, the sole's distance to a floor line, is not read: no floor line in the picture can be declared (`--floor` is a world y, placed from the support it would have to decide).
|
|
350
|
+
- **Contact keys, by hand** ([#303](https://github.com/rig-c/pose/issues/303), the route for drawn references that [#313](https://github.com/rig-c/pose/issues/313) asked for). A drawn frontal walk can defeat every reading above: its nearer foot is drawn lower, so ankle height names the foot in the air, and a slow walk can step under the vertical speed's 0.01-leg-length stillness, so #280 never reads it as frontal. A single floor line in the picture does not help either: seen from the front the ground is a band, not a line (#303's reading). The artist's key sheet already carries the contact table, so `--contact-keys <file>` takes it: `rig-pose-contact-keys/1`, `{ "spec": "rig-pose-contact-keys/1", "keys": [ { "frame", "side" } ], "note"? }`, `side` one of `l`, `r`, `both`, `none`. Frames are the sequence's, may be fractional (a key sheet timed off the frame grid, as `--pelvis-keys`, #294), and strictly increase within `[0, frames − 1]`. **A key holds until the next one**, and the last to the clip's end: contact is a state, not an angle, so there is nothing between `l` and `r` to interpolate. A fractional key takes effect on the first frame at or after it; frames before the first key are not covered and keep the reading without keys. **The precedence is contact keys, then the vertical speed (#280, on a frontal clip), then ankle height**: the keys are applied last, over whatever the readings decided, on every frame they cover. `l` or `r` is that foot; `none` is flight, no foot supports; `both` is the foot that landed, as #280 hands over on landing: the other side from the `l` or `r` key before the run, or, where the run starts the cover or follows a `none` key (flight), the side the next `l` or `r` key names (the side that stays down after). Those neighbours are read from the keys, so a key that reaches no whole frame still counts: `l@0, none@1.1, both@1.2, l@3` holds `l` on frame 2 (rig-c/pose#338). The support spans and changes are then rebuilt from the per-frame support, as #280's are; a step, #113's `lowered` or #119's `tracked` run is kept only where it starts before the cover, cut at the frame before it (a step's re-plant inside the cover is not read), and jumps are kept as read. `FEET` prints one `support: contact keys` line (the keys, the frames they cover, how many frames they changed, the flight they declare, the frames before the first key) and one `stride` line per support span naming what decided its frames and what the reading without keys would have named, e.g. `stride l 19–37: of 19 frame(s), decided by contact keys 19; the reading without keys would name r on 4 of them`; these stride lines stand in for #280's. The report gains a `contactKeys` key (the file's path and SHA-256, the key count, the first covered frame, the runs changed) and `options.contactKeys`, only with the flag. On the tests' slow synthetic frontal walk (8 drawings a stride, each held 4.75 frames at 24 fps, keyed from its third drawing on), the support equals the keys on all 142 covered frames, where the reading without keys misses them on 16; the 10 frames before the first key keep that reading (`test/contactkeys.test.ts`). A malformed file, a `side` outside the four, a frame outside the span or one not after the key before it is refused by name (`CONTACT_KEYS`), every problem in one run, and so are keys on a map whose legs give no support to decide. Without the flag nothing is read and the output is as it was.
|
|
342
351
|
- The new support is held where it is on the frame it takes over, and the hip blends in y from the old derivation to the new over **3 frames** while taking the new derivation's x immediately ([#102](https://github.com/rig-c/pose/issues/102)), so the planted foot does not slide across the blend.
|
|
343
352
|
- A **jump** — both reference ankles **and** mid_hip more than **0.05** leg lengths above their **rolling floor**, the 95th-percentile picture y over ±1.0 s around the frame, for at least **2 frames**, starting from the first immediately preceding push-off frame where both ankles are already above their rolling floor while mid_hip lags ([#142](https://github.com/rig-c/pose/issues/142)) — releases the plant: the hip follows the pose's travel from where it was, and re-plants on landing (blended the same way). At a walking hand-over the front foot has not landed yet while the back ankle lifts its heel and the hip rises 0.051–0.065 leg lengths; when the front foot lands while the back ankle stays above its floor, a back ankle that holds its height in y (< **0.01** leg lengths a frame for **2 frames**) is on a raised heel rather than in flight and is not read as a jump ([#101](https://github.com/rig-c/pose/issues/101)). The body has to lift: an ankle's picture y drifts with depth, and under one floor for the whole sequence the subject in `dance-a` standing still (frames 0–29 and 277–312, mid_hip moving 10 and 14 px) read as jumping, 152–155 of 313 frames in all.
|
|
344
353
|
- **In place** ([#92](https://github.com/rig-c/pose/issues/92)). On a sequence with `"travel": "in-place"`, the supporting foot of a figure that does not travel moves back in the picture while the body stays. Holding it where it planted turns that slide into the body's travel, so the hip walks off and never comes back. Under the declaration each plant's anchor follows its own ankle's picture x, scaled into the rig, from the frame it planted, and keeps its y on the one floor. No step releases a foot, since a foot that follows its track has no plant to drag. Every foot may take support, and the lower one does, by the rules above. The loop is then closed: the hip's x on frame i moves by −drift · i / (n − 1), where drift is its x on the last frame less its x on the first. The last frame's hip x is then the first's, and the loop carries no net travel. The y is the floor's and is not touched. `FEET` prints the drift closed, in place of the steps line, and the report's `feet.closure` carries `{ drift, frames }` (`null` otherwise). On the tests' synthetic in-place loop (40 frames, each supporting ankle sliding back 2 px a frame, the hips still), the hip moves less than 10⁻⁹ px. The same joints without the declaration put it 68 px forward over the loop. With a slide of 0.04 leg lengths a frame, both feet are released as stepping and never re-plant without the declaration, and the supports alternate with it. Jumps are read as before. **What planting is and is not needed for** ([#242](https://github.com/rig-c/pose/issues/242)): the declaration is the sequence's, so the support reading (no step releases a foot), the camera-zoom normalisations and the head nod (no detrend window, no clamp) take it with or without `--plant-feet`. Planting is needed only for what acts on a planted hip: the anchor following its ankle's track and the loop closure, since without a plant there is no anchor to follow and the hip is the pose's own travel. Without `--plant-feet` the run says so in a `TRAVEL` line: `sequence declares travel: in-place but --plant-feet is off — its supports and the head nod are read in place as with it; the loop closure is --plant-feet's (it closes the planted hip), so the hip follows the pose's own travel: x +0.00 units from the first frame to the last, not closed` (the figure is the hip's measured x drift; `and the map translates no bone` when nothing is translated). On the tests' synthetic walk in place (7 cycles of 24 frames, the supporting ankle sliding up to 0.09 leg lengths a frame, the eyes bobbing 3 px twice a cycle), without `--plant-feet` the supports are 15 spans covering all 168 frames (the seam splits the first) and the head's rotate key and nod are the same on cycle 1 and cycle 7 to 0.001; reading the declaration only under `--plant-feet`, as before, they were 2 spans covering 3 frames, and the head's cycle 7 differed from cycle 1 by 1.057° and 0.661 units. That head figure came from the nod's detrend window cut at the clip's ends, which [#241](https://github.com/rig-c/pose/issues/241) wraps on a loop, so it no longer reproduces under that reading either.
|
|
@@ -458,7 +467,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
458
467
|
⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (`dance-a`, 313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material, arms go `"free"`. Legs on `"rest"` with `--bends clamp` hold only while the knees stay near straight. On `dance-b` (a second dance with deeper knee bends, rig-c/pose#27), that setting straightened the left knee for most of the dance: `leg_l1` was 48.3° off at p90, against 0.0–0.3° with free legs. Leave legs `"free"` unless the dance keeps its knees straight.
|
|
459
468
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
460
469
|
- `translate.scale` is one length measured on both: two points on the rig, two points in the pose (its median over the frames where both are good). Pick a long segment that does not foreshorten much.
|
|
461
|
-
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides
|
|
470
|
+
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides`, each half a frame before the frame it shows on as the switches' and the draw order's keys are ([#334](https://github.com/rig-c/pose/issues/334)) (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
|
|
462
471
|
- `body` (optional, [#173](https://github.com/rig-c/pose/issues/173)): `{ "yaw"?: { "bone": "body_yaw", "range": [-30, 30], "sets"?: [{ "set": "T8", "above": 45 }] }, "sets"?: [{ "set": "T8", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Maps the per-frame body yaw read from the camera-normalised shoulder and hip width (`BODY`) into `range` with the same `0.8 × range` knee mapping as `face`, keying the named dial bone's `rotate` track, and switches any declared `sets` (`body.sets` or `body.yaw.sets`) with the same `above` / `above − 10°` hysteresis (overriding `face` sets and arm `alternates` on slots they share while active). Without `body` in the map, `BODY` and `--report`'s `body` still report the reading while leaving every track in `motion.json` unchanged.
|
|
463
472
|
|
|
464
473
|
### A joint's range: `range`
|
package/cli.ts
CHANGED
|
@@ -82,8 +82,10 @@ clean (required before retarget; rig-c/pose#67)
|
|
|
82
82
|
(the trunk side-on). Which way a foreshortened segment leans comes from the body, with
|
|
83
83
|
"forward" the trunk's facing: thigh and forearm forward (a hip flexes forward, an elbow
|
|
84
84
|
bends back); an upper arm with the opposite leg (forward when the other thigh is the more
|
|
85
|
-
foreshortened); a shin
|
|
86
|
-
|
|
85
|
+
foreshortened); a shin back (a knee folds it back) — the same in a walk and a run, after
|
|
86
|
+
the owner's two statements (rig-c/pose#336): a walk's lifted foot is behind the supporting
|
|
87
|
+
leg, a run's lifted leg is in front, its knee protruding. The gait is this flag's value and
|
|
88
|
+
decides the joint --draw-order depth reads. tools/skeleton.ts colours the segments
|
|
87
89
|
by it, and retarget --report orders the limbs front to back by it (layers)
|
|
88
90
|
|
|
89
91
|
In order, each step printed with its figures: (1) spikes — a joint more than 0.2 trunk lengths
|
|
@@ -191,10 +193,20 @@ retarget
|
|
|
191
193
|
(the shortfall listed). RAISED lines (default: off; rig-c/pose#62)
|
|
192
194
|
--draw-order depth key the legs' draw order from the sequence's depth (clean --depth; rig-c/pose#304): on
|
|
193
195
|
each frame the leg whose depth reads farther from the camera is drawn behind the other,
|
|
194
|
-
a tie (equal depths) holding the order before, in rig-c's drawOrder timeline. The
|
|
196
|
+
a tie (equal depths) holding the order before, in rig-c's drawOrder timeline. The depth is
|
|
197
|
+
read at the joint the gait names (rig-c/pose#336): a walk (clean --depth walk) by the ankle,
|
|
198
|
+
the lifted foot behind; a run by the knee, the lifted leg in front. The legs'
|
|
195
199
|
slots must fill one unbroken run of the setup draw order (else the pair is not keyed and
|
|
196
200
|
the line names the slot between); a set's slot goes with the leg it depicts. Refused on
|
|
197
|
-
a sequence without depth (DRAWORDER_NO_DEPTH)
|
|
201
|
+
a sequence without depth (DRAWORDER_NO_DEPTH), and on a walk cleaned before rig-c/pose#336
|
|
202
|
+
(a shin leaning forward: DEPTH_RECLEAN; clean it again). DRAWORDER lines (default: off)
|
|
203
|
+
--contact-keys <file> which foot supports, by hand (rig-c/pose#303): rig-pose-contact-keys/1, { "spec":
|
|
204
|
+
"rig-pose-contact-keys/1", "keys": [{ "frame", "side" }], "note"? }, side l | r | both |
|
|
205
|
+
none; frames the sequence's, fractional allowed, strictly increasing in [0, N − 1]. A key
|
|
206
|
+
holds until the next one (contact is a state, not interpolated); before the first key the
|
|
207
|
+
support is read as without it. On every frame they cover the keys decide, over the
|
|
208
|
+
vertical speed (#280) and ankle height: none is flight; both, the foot that landed. Malformed
|
|
209
|
+
keys are refused by name (CONTACT_KEYS). FEET support and stride lines (default: off)
|
|
198
210
|
--idle <name> the rig's animation read as its idle (default: idle). Every idle track the new
|
|
199
211
|
animation does not key, on any channel, is listed as IDLE-DROPPED (a blink, a
|
|
200
212
|
breath); one it keys is IDLE-KEPT-OUT, naming the track that keys it, and so is a
|
|
@@ -383,7 +395,7 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
|
|
|
383
395
|
const RETARGET_ALIASES = { sequence: 'pose', animation: 'name' } as const;
|
|
384
396
|
|
|
385
397
|
function retargetArgs(argv: string[]): RetargetArgs {
|
|
386
|
-
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'reference', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor', 'draw-order'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
|
|
398
|
+
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'reference', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor', 'draw-order', 'contact-keys'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
|
|
387
399
|
if (f.has('verbose') && f.has('summary')) throw new Usage('--verbose and --summary are both given; --verbose prints every line in full and --summary one line per measure, so give one');
|
|
388
400
|
if (f.has('reference') && !f.has('build')) throw new Usage(`--reference needs --build: the record it goes in is written beside the build (${PROVENANCE_FILE})`);
|
|
389
401
|
const bends = f.get('bends') ?? 'refuse';
|
|
@@ -433,6 +445,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
433
445
|
...reduceOf(f),
|
|
434
446
|
output: f.has('verbose') ? 'verbose' : f.has('summary') ? 'summary' : 'default',
|
|
435
447
|
...(drawOrder !== undefined ? { drawOrder: 'depth' as const } : {}),
|
|
448
|
+
...(f.has('contact-keys') ? { contactKeysFile: f.get('contact-keys')! } : {}),
|
|
436
449
|
};
|
|
437
450
|
}
|
|
438
451
|
|
|
@@ -558,7 +571,7 @@ function runClean(argv: string[], say: (l: string) => void): void {
|
|
|
558
571
|
const input = need(f, 'pose');
|
|
559
572
|
const out = need(f, 'out');
|
|
560
573
|
const gait = f.get('depth');
|
|
561
|
-
if (gait !== undefined && !(DEPTH_GAITS as readonly string[]).includes(gait)) throw new Usage(`--depth is "${gait}"; it must be ${DEPTH_GAITS.join(' or ')} (the gait decides
|
|
574
|
+
if (gait !== undefined && !(DEPTH_GAITS as readonly string[]).includes(gait)) throw new Usage(`--depth is "${gait}"; it must be ${DEPTH_GAITS.join(' or ')} (the gait decides the joint --draw-order depth orders the legs by: the ankle in a walk, the knee in a run)`);
|
|
562
575
|
const outPath = new Problems();
|
|
563
576
|
const outDirs = planOutDirs([['--out', out]], outPath);
|
|
564
577
|
outPath.throwIfAny();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/clean.ts
CHANGED
|
@@ -116,9 +116,10 @@ export interface CleanParams {
|
|
|
116
116
|
*/
|
|
117
117
|
hold?: { speed: number; reach: number } | 'off';
|
|
118
118
|
/**
|
|
119
|
-
* rig-c/pose#281: derive each frame's depth (`src/depth.ts`)
|
|
120
|
-
*
|
|
121
|
-
*
|
|
119
|
+
* rig-c/pose#281: derive each frame's depth (`src/depth.ts`) on the cleaned
|
|
120
|
+
* positions, and record this gait (rig-c/pose#336: it decides the joint the
|
|
121
|
+
* draw order reads, not a sign). Absent, no depth is derived and the output is
|
|
122
|
+
* what it was before #281.
|
|
122
123
|
*/
|
|
123
124
|
depth?: DepthGait;
|
|
124
125
|
}
|
|
@@ -841,7 +842,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
841
842
|
// rig-c/pose#281: derived on the cleaned positions, or a measured depth carried through as read.
|
|
842
843
|
let depth: DerivedDepth | null = null;
|
|
843
844
|
if (params.depth !== undefined) {
|
|
844
|
-
depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {})
|
|
845
|
+
depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {}));
|
|
845
846
|
depth.frames.forEach((d, i) => (frames[i]!.depth = d));
|
|
846
847
|
record.params.depth = { gait: params.depth };
|
|
847
848
|
} else if (carried === 'measured') seq.frames.forEach((f, i) => (frames[i]!.depth = f.depth ?? {}));
|
package/src/contacts.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--contact-keys` (rig-c/pose#303): the support declared by hand, frame by
|
|
3
|
+
* frame, for a clip no reading gets right — a drawn frontal walk. Seen from the
|
|
4
|
+
* front, depth is drawn as height, so the ground is a band in the picture and
|
|
5
|
+
* the nearer foot is drawn lower (#303's reading: no single floor line matches a
|
|
6
|
+
* contact table); a slow drawn walk can also step under the vertical speed's
|
|
7
|
+
* still tolerance, so #280 never takes over. The artist's key sheet already
|
|
8
|
+
* carries the contact table: these keys are that data, not a guess.
|
|
9
|
+
*
|
|
10
|
+
* `rig-pose-contact-keys/1`: `{ spec, keys: [{ frame, side }], note? }`, `side`
|
|
11
|
+
* one of `l | r | both | none`. Frames are the sequence's, may be fractional (a
|
|
12
|
+
* key sheet timed off the frame grid, as `--pelvis-keys`, #294), and strictly
|
|
13
|
+
* increase within [0, frames − 1].
|
|
14
|
+
*
|
|
15
|
+
* **A key holds until the next one** (a step function; the last holds to the
|
|
16
|
+
* clip's end). Contact is a state, not an angle: a foot is down or it is not, and
|
|
17
|
+
* there is no value between `l` and `r` to interpolate. A key at a fractional
|
|
18
|
+
* frame takes effect on the first frame at or after it. Frames before the first
|
|
19
|
+
* key are not covered: the support there is the reading without keys.
|
|
20
|
+
*
|
|
21
|
+
* Precedence: on every covered frame the keys decide, over #280's vertical
|
|
22
|
+
* speed (a frontal clip) and ankle height. `l`/`r`: that foot supports. `none`:
|
|
23
|
+
* flight, no foot supports. `both`: the foot that landed supports, as #280 hands
|
|
24
|
+
* over on landing. The run's neighbours are read from the keys, not the frames,
|
|
25
|
+
* so a key that reaches no frame counts (rig-c/pose#338): after an `l` or `r`
|
|
26
|
+
* key, the other side; after a `none` key (flight), or where the run starts the
|
|
27
|
+
* cover, the side the next `l` or `r` key names (the side that stays down after);
|
|
28
|
+
* else, at the cover's start, the support on the frame before (the reading
|
|
29
|
+
* without keys); a run none of those name keeps the reading without keys (`kept`).
|
|
30
|
+
*/
|
|
31
|
+
import type { Feet, Side } from './legs.ts';
|
|
32
|
+
import { isObject, parseJson, Problems, refuseUnknownKeys, typeName } from './problems.ts';
|
|
33
|
+
|
|
34
|
+
export const CONTACT_KEYS_SPEC = 'rig-pose-contact-keys/1';
|
|
35
|
+
|
|
36
|
+
/** A contact key's value: the foot down, both, or none (flight). */
|
|
37
|
+
export type ContactSide = Side | 'both' | 'none';
|
|
38
|
+
const VALUES: readonly ContactSide[] = ['l', 'r', 'both', 'none'];
|
|
39
|
+
|
|
40
|
+
export interface ContactKeys {
|
|
41
|
+
/** The file's keys, as read: strictly increasing frames within the sequence's span. */
|
|
42
|
+
keys: Array<{ frame: number; side: ContactSide }>;
|
|
43
|
+
/** Per frame, the key in force (the last key at or before the frame), or null before the first key. */
|
|
44
|
+
key: Array<ContactSide | null>;
|
|
45
|
+
/** The frames of keys no frame takes: the next key comes before the next whole frame (two keys between two frames). */
|
|
46
|
+
unreached: number[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `rig-pose-contact-keys/1`, read against a sequence of `frames` frames. Every
|
|
51
|
+
* problem is collected and refused by name (`CONTACT_KEYS`): a file that is not
|
|
52
|
+
* JSON or not the shape, a `spec` that is not this one, a `side` outside the
|
|
53
|
+
* four, a frame outside [0, frames − 1], a frame not after the key before it.
|
|
54
|
+
*/
|
|
55
|
+
export function readContactKeys(text: string, where: string, frames: number): ContactKeys {
|
|
56
|
+
const doc = parseJson(text, where, 'CONTACT_KEYS');
|
|
57
|
+
const p = new Problems();
|
|
58
|
+
const shape = `{ "spec": "${CONTACT_KEYS_SPEC}", "keys": [{ "frame", "side" }], "note"? }`;
|
|
59
|
+
if (!isObject(doc)) {
|
|
60
|
+
p.add('CONTACT_KEYS', `${where} is ${typeName(doc)}; it must be ${shape}`);
|
|
61
|
+
p.throwIfAny();
|
|
62
|
+
throw new Error('unreachable');
|
|
63
|
+
}
|
|
64
|
+
refuseUnknownKeys(p, 'CONTACT_KEYS', where, doc, ['spec', 'keys', 'note']);
|
|
65
|
+
if (doc.spec !== CONTACT_KEYS_SPEC) p.add('CONTACT_KEYS', `${where} "spec" is ${typeName(doc.spec)}; it must be "${CONTACT_KEYS_SPEC}"`);
|
|
66
|
+
if ('note' in doc && typeof doc.note !== 'string') p.add('CONTACT_KEYS', `${where} "note" is ${typeName(doc.note)}; it must be a string`);
|
|
67
|
+
const keys: ContactKeys['keys'] = [];
|
|
68
|
+
if (!Array.isArray(doc.keys) || doc.keys.length === 0) p.add('CONTACT_KEYS', `${where} "keys" is ${typeName(doc.keys)}; it must be a non-empty array of { frame, side }`);
|
|
69
|
+
else doc.keys.forEach((k: unknown, i: number) => {
|
|
70
|
+
const kw = `${where} keys[${i}]`;
|
|
71
|
+
if (!isObject(k)) {
|
|
72
|
+
p.add('CONTACT_KEYS', `${kw} is ${typeName(k)}; a key is { frame, side }`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
refuseUnknownKeys(p, 'CONTACT_KEYS', kw, k, ['frame', 'side']);
|
|
76
|
+
const okF = typeof k.frame === 'number' && Number.isFinite(k.frame) && k.frame >= 0 && k.frame <= frames - 1;
|
|
77
|
+
const okS = typeof k.side === 'string' && (VALUES as readonly string[]).includes(k.side);
|
|
78
|
+
if (!okF) p.add('CONTACT_KEYS', `${kw} "frame" is ${typeName(k.frame)}; it must be a frame of the sequence in [0, ${frames - 1}] (fractional allowed)`);
|
|
79
|
+
if (!okS) p.add('CONTACT_KEYS', `${kw} "side" is ${typeName(k.side)}; it must be one of l, r, both, none (the foot down, both feet, or flight)`);
|
|
80
|
+
if (!okF || !okS) return;
|
|
81
|
+
const prev = keys[keys.length - 1];
|
|
82
|
+
if (prev !== undefined && (k.frame as number) <= prev.frame) p.add('CONTACT_KEYS', `${kw} "frame" is ${k.frame as number}, not after the key before it (${prev.frame}); frames strictly increase`);
|
|
83
|
+
else keys.push({ frame: k.frame as number, side: k.side as ContactSide });
|
|
84
|
+
});
|
|
85
|
+
p.throwIfAny();
|
|
86
|
+
const key: Array<ContactSide | null> = [];
|
|
87
|
+
const reached = new Set<number>();
|
|
88
|
+
let at = -1;
|
|
89
|
+
for (let i = 0; i < frames; i++) {
|
|
90
|
+
while (at + 1 < keys.length && keys[at + 1]!.frame <= i) at++;
|
|
91
|
+
key.push(at < 0 ? null : keys[at]!.side);
|
|
92
|
+
if (at >= 0) reached.add(at);
|
|
93
|
+
}
|
|
94
|
+
return { keys, key, unreached: keys.filter((_, k) => !reached.has(k)).map((k) => k.frame) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** rig-c/pose#303: what the contact keys did to a clip's support, set on `Feet` by `contactKeysOf`. */
|
|
98
|
+
export interface ContactKeysReading {
|
|
99
|
+
keys: ContactKeys['keys'];
|
|
100
|
+
/** Per frame, the key in force, or null before the first key. */
|
|
101
|
+
key: Array<ContactSide | null>;
|
|
102
|
+
/** The first frame a key covers; every frame from it to the last is covered. */
|
|
103
|
+
from: number;
|
|
104
|
+
/** Per frame, the support read without the keys (#280's or ankle height's). */
|
|
105
|
+
before: Array<Side | null>;
|
|
106
|
+
/** Per frame, whether a key decided its support (false outside the cover, and on a `both` run no side names, `kept`). */
|
|
107
|
+
keyed: boolean[];
|
|
108
|
+
/** The frames of `both` runs that keep the reading without keys: no keyed support before, no single-foot key after. */
|
|
109
|
+
kept: number[];
|
|
110
|
+
/** Runs of frames whose support the keys changed: the support without them (`was`) and with them (`now`; null: flight). */
|
|
111
|
+
changed: Array<{ frames: [number, number]; was: Side | null; now: Side | null }>;
|
|
112
|
+
/** As `ContactKeys.unreached`. */
|
|
113
|
+
unreached: number[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The support with the contact keys applied, last, over the reading `supportsOf`
|
|
118
|
+
* made (the header's precedence), then rebuilt as #280's frontal reading
|
|
119
|
+
* rebuilds it: support spans and changes from the per-frame support. Outside
|
|
120
|
+
* the cover the support is the reading without keys, unchanged. A step that
|
|
121
|
+
* released a plant (report only) and #113's `lowered` and #119's `tracked` runs
|
|
122
|
+
* are kept where they start before the cover, cut at the frame before it
|
|
123
|
+
* (rig-c/pose#338; a step's re-plant inside the cover is not read, null): inside
|
|
124
|
+
* it a plant ends where the keys hand support over. Jumps are kept as read (the
|
|
125
|
+
* hip's rise); on a jump frame a key still decides the support.
|
|
126
|
+
*/
|
|
127
|
+
export function contactKeysOf(feet: Feet, keys: ContactKeys): Feet {
|
|
128
|
+
const n = feet.support.length;
|
|
129
|
+
const before = feet.support.map((s) => s ?? null);
|
|
130
|
+
const support = [...before];
|
|
131
|
+
const keyed = new Array<boolean>(n).fill(false);
|
|
132
|
+
const kept: number[] = [];
|
|
133
|
+
const other = (s: Side): Side => (s === 'r' ? 'l' : 'r');
|
|
134
|
+
const from = keys.key.findIndex((k) => k !== null);
|
|
135
|
+
/** Per frame, the index in `keys.keys` of the key in force (−1 before the first). */
|
|
136
|
+
const at: number[] = [];
|
|
137
|
+
for (let i = 0, k = -1; i < n; i++) {
|
|
138
|
+
while (k + 1 < keys.keys.length && keys.keys[k + 1]!.frame <= i) k++;
|
|
139
|
+
at.push(k);
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < n; i++) {
|
|
142
|
+
const k = keys.key[i] ?? null;
|
|
143
|
+
if (k === null) continue;
|
|
144
|
+
if (k !== 'both') {
|
|
145
|
+
support[i] = k === 'none' ? null : k;
|
|
146
|
+
keyed[i] = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let b = i;
|
|
150
|
+
while (b + 1 < n && keys.key[b + 1] === 'both') b++;
|
|
151
|
+
// rig-c/pose#338: the states either side come from the keys themselves, not from the frames — a key that reaches no frame (none@1.1 before both@1.2) is the state the run follows.
|
|
152
|
+
let k0 = at[i]!;
|
|
153
|
+
while (k0 > 0 && keys.keys[k0 - 1]!.side === 'both') k0--;
|
|
154
|
+
let k1 = at[b]! + 1;
|
|
155
|
+
while (k1 < keys.keys.length && keys.keys[k1]!.side === 'both') k1++;
|
|
156
|
+
const prevSide = k0 > 0 ? keys.keys[k0 - 1]!.side : null;
|
|
157
|
+
const nextSide = k1 < keys.keys.length ? keys.keys[k1]!.side : null;
|
|
158
|
+
const landed: Side | null = prevSide === 'r' || prevSide === 'l' ? other(prevSide) : nextSide === 'r' || nextSide === 'l' ? nextSide : prevSide === null && i > 0 ? support[i - 1]! : null;
|
|
159
|
+
for (let f = i; f <= b; f++) {
|
|
160
|
+
if (landed === null) kept.push(f);
|
|
161
|
+
else {
|
|
162
|
+
support[f] = landed;
|
|
163
|
+
keyed[f] = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
i = b;
|
|
167
|
+
}
|
|
168
|
+
const supports: Feet['supports'] = [];
|
|
169
|
+
const changes: Feet['changes'] = [];
|
|
170
|
+
const changed: ContactKeysReading['changed'] = [];
|
|
171
|
+
for (let i = 0; i < n; i++) {
|
|
172
|
+
const s = support[i] ?? null;
|
|
173
|
+
if (s !== before[i]) {
|
|
174
|
+
const last = changed[changed.length - 1];
|
|
175
|
+
if (last !== undefined && last.frames[1] === i - 1 && last.was === before[i] && last.now === s) last.frames[1] = i;
|
|
176
|
+
else changed.push({ frames: [i, i], was: before[i]!, now: s });
|
|
177
|
+
}
|
|
178
|
+
if (s === null) continue;
|
|
179
|
+
const prev = i > 0 ? (support[i - 1] ?? null) : null;
|
|
180
|
+
if (prev !== s) changes.push({ frame: i, from: prev, to: s });
|
|
181
|
+
const last = supports[supports.length - 1];
|
|
182
|
+
if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
|
|
183
|
+
else supports.push({ side: s, frames: [i, i] });
|
|
184
|
+
}
|
|
185
|
+
const cut = <T extends { frames: [number, number] }>(runs: readonly T[] | undefined): T[] => (runs ?? []).filter((r) => r.frames[0] < from).map((r) => ({ ...r, frames: [r.frames[0], Math.min(r.frames[1], from - 1)] as [number, number] }));
|
|
186
|
+
const lowered = feet.lowered === undefined ? undefined : cut(feet.lowered);
|
|
187
|
+
const tracked = feet.tracked === undefined ? undefined : cut(feet.tracked);
|
|
188
|
+
return {
|
|
189
|
+
legLength: feet.legLength,
|
|
190
|
+
support,
|
|
191
|
+
supports,
|
|
192
|
+
jumps: feet.jumps,
|
|
193
|
+
// rig-c/pose#338: a step running into the cover ends on the frame before it, as lowered and tracked do; a re-plant inside the cover is the keys' to decide, so it is not read (null).
|
|
194
|
+
steps: cut(feet.steps).map((s) => (s.replant !== null && s.replant >= from ? { ...s, replant: null } : s)),
|
|
195
|
+
changes,
|
|
196
|
+
...(lowered !== undefined && lowered.length > 0 ? { lowered } : {}),
|
|
197
|
+
...(tracked !== undefined && tracked.length > 0 ? { tracked } : {}),
|
|
198
|
+
...(feet.signal !== undefined ? { signal: feet.signal } : {}),
|
|
199
|
+
contactKeys: { keys: keys.keys, key: keys.key, from, before, keyed, kept, changed, unreached: keys.unreached },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The signal names a stride line uses for the frames a key did not decide. */
|
|
204
|
+
const SIGNAL_NAMES: Record<string, string> = { speed: 'ankle vertical speed', height: 'ankle height' };
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The FEET lines of a run with contact keys (rig-c/pose#303), in place of #280's
|
|
208
|
+
* stride lines: one `support: contact keys` line — the keys, the frames they
|
|
209
|
+
* cover, how many they changed, the flight they declare, what stands outside —
|
|
210
|
+
* then one `stride` line per support span naming the signal that decided its
|
|
211
|
+
* frames (`contact keys`, else the reading without keys) and what that reading
|
|
212
|
+
* alone would have named.
|
|
213
|
+
*/
|
|
214
|
+
export function contactKeysLines(feet: Feet): string[] {
|
|
215
|
+
const c = feet.contactKeys;
|
|
216
|
+
if (c === undefined) return [];
|
|
217
|
+
const n = feet.support.length;
|
|
218
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
219
|
+
const changed = c.changed.reduce((s, r) => s + r.frames[1] - r.frames[0] + 1, 0);
|
|
220
|
+
const flight = c.key.filter((k) => k === 'none').length;
|
|
221
|
+
const tail = [
|
|
222
|
+
...(flight > 0 ? [`${flight} keyed none (flight, no foot supports)`] : []),
|
|
223
|
+
...(c.kept.length > 0 ? [`${c.kept.length} keyed both with no side to hold (no keyed support before, no single-foot key after) keep it`] : []),
|
|
224
|
+
...(c.from > 0 ? [`frames ${span([0, c.from - 1])}, before the first key, keep it`] : []),
|
|
225
|
+
...(c.unreached.length > 0 ? [`key(s) at ${c.unreached.join(', ')} reach no frame (the next key comes first)`] : []),
|
|
226
|
+
];
|
|
227
|
+
const out = [`support: contact keys (rig-c/pose#303, --contact-keys) — ${c.keys.length} key(s) cover frames ${span([c.from, n - 1])} of ${n}, each holding until the next one; they change the reading without them on ${changed} frame(s)${tail.length > 0 ? `; ${tail.join('; ')}` : ''}`];
|
|
228
|
+
const by = feet.signal?.by;
|
|
229
|
+
for (const sp of feet.supports) {
|
|
230
|
+
let keyed = 0;
|
|
231
|
+
let differ = 0;
|
|
232
|
+
const rest = new Map<string, number>();
|
|
233
|
+
const was = new Set<string>();
|
|
234
|
+
for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
|
|
235
|
+
if (c.keyed[i]) keyed++;
|
|
236
|
+
else {
|
|
237
|
+
const name = SIGNAL_NAMES[by?.[i] ?? 'height'] ?? 'ankle height';
|
|
238
|
+
rest.set(name, (rest.get(name) ?? 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
if (c.before[i] !== sp.side) {
|
|
241
|
+
differ++;
|
|
242
|
+
was.add(c.before[i] ?? 'none');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const parts = [...(keyed > 0 ? [`contact keys ${keyed}`] : []), ...[...rest].map(([k, v]) => `${k} ${v}`)];
|
|
246
|
+
const alone = differ === 0 ? 'the reading without keys names the same' : `the reading without keys would name ${[...was].join(' or ')} on ${differ} of them`;
|
|
247
|
+
out.push(`stride ${sp.side} ${span(sp.frames)}: of ${sp.frames[1] - sp.frames[0] + 1} frame(s), decided by ${parts.join(', ')}; ${alone}`);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
package/src/depth.ts
CHANGED
|
@@ -13,10 +13,17 @@
|
|
|
13
13
|
* | segment | sense when foreshortened | why |
|
|
14
14
|
* | --------- | ------------------------ | --- |
|
|
15
15
|
* | thigh | forward | a hip flexes forward; it extends back too little to foreshorten past the band |
|
|
16
|
-
* | shin |
|
|
16
|
+
* | shin | back | the owner's walk statement (rig-c/pose#336): the lifted foot is behind the supporting leg — a knee folds the shin back |
|
|
17
17
|
* | upper arm | with the opposite leg | in a gait each arm swings with the opposite leg: forward when the other side's thigh is the more foreshortened of the two, back when its own is; forward (a shoulder flexes forward) when neither thigh is foreshortened |
|
|
18
18
|
* | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
|
|
19
19
|
*
|
|
20
|
+
* The thigh's row is the owner's run statement (rig-c/pose#336): the lifted leg
|
|
21
|
+
* is in front, its knee protruding — a raised thigh brings the knee forward.
|
|
22
|
+
* The table is the same in a walk and a run, so `clean --depth walk|run` writes
|
|
23
|
+
* the same signs for both; the gait it records decides which joint
|
|
24
|
+
* `--draw-order depth` orders the legs by (`src/draworder.ts`): a walk by the
|
|
25
|
+
* ankle (the lifted foot behind), a run by the knee (the lifted knee in front).
|
|
26
|
+
*
|
|
20
27
|
* "Forward" is the trunk's facing: toward the camera (+1) when the figure faces
|
|
21
28
|
* it (its left shoulder and hip drawn right of its right ones), away (−1) when
|
|
22
29
|
* it faces away, and undecided (sign 0) when the trunk is side-on — shoulders
|
|
@@ -86,8 +93,8 @@ export interface DerivedDepth {
|
|
|
86
93
|
signs: { toward: number; away: number; flat: number };
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
/** Each frame's depth, derived from the picture's lengths
|
|
90
|
-
export function deriveDepth(track: readonly Joints[]
|
|
96
|
+
/** Each frame's depth, derived from the picture's lengths (the table above; the same for every gait). */
|
|
97
|
+
export function deriveDepth(track: readonly Joints[]): DerivedDepth {
|
|
91
98
|
const lengths = new Map(DEPTH_SEGMENT_NAMES.map((s) => [s, track.map((j) => drawn(j, s))]));
|
|
92
99
|
const rest = {} as DerivedDepth['rest'];
|
|
93
100
|
for (const k of ['upper_arm', 'forearm', 'thigh', 'shin'] as const) {
|
|
@@ -112,7 +119,7 @@ export function deriveDepth(track: readonly Joints[], gait: DepthGait): DerivedD
|
|
|
112
119
|
if (r === null) continue;
|
|
113
120
|
let sense: -1 | 1;
|
|
114
121
|
const kind = kindOf(s);
|
|
115
|
-
if (kind === 'shin') sense =
|
|
122
|
+
if (kind === 'shin') sense = -1;
|
|
116
123
|
else if (kind === 'upper_arm') {
|
|
117
124
|
// The arm swings with the opposite leg: its own thigh and the other side's, by how foreshortened each is.
|
|
118
125
|
const own = ratio(`${s.slice(0, 2)}thigh` as DepthSegment);
|
|
@@ -143,19 +150,52 @@ export function segmentDepth(d: SegmentDepth, length: number): number {
|
|
|
143
150
|
return d.sign * (length / d.ratio) * Math.sqrt(1 - r * r);
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
/**
|
|
154
|
+
* A limb's two joint depths on one frame, px (+ toward the camera): its middle joint (the upper segment's) and its end
|
|
155
|
+
* joint (the middle's plus the lower segment's), from the shoulder or hip at 0, unrounded. Null where the upper segment
|
|
156
|
+
* is not named and placed; `end` null where the lower one is not (the middle joint does not need it: rig-c/pose#336).
|
|
157
|
+
*/
|
|
158
|
+
export function limbJointDepths(joints: Joints, depth: FrameDepth, limb: Limb): { mid: number; end: number | null } | null {
|
|
159
|
+
const [upper, lower] = LIMBS[limb];
|
|
160
|
+
const du = depth[upper];
|
|
161
|
+
const dl = depth[lower];
|
|
162
|
+
const lu = drawn(joints, upper);
|
|
163
|
+
const ll = drawn(joints, lower);
|
|
164
|
+
if (du === undefined || lu === null) return null;
|
|
165
|
+
const mid = segmentDepth(du, lu);
|
|
166
|
+
return { mid, end: dl === undefined || ll === null ? null : mid + segmentDepth(dl, ll) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* rig-c/pose#336: the frames of a derived depth on which a shin leans the way the trunk faces — the sign table before
|
|
171
|
+
* #336, where a walk's foreshortened shin read forward. The table since leans it back (−facing) in both gaits, and a run's
|
|
172
|
+
* never changed, so only a record a release before #336 cleaned with `--depth walk` carries one. A record with no
|
|
173
|
+
* foreshortened shin reads the same under both tables and is not listed.
|
|
174
|
+
*/
|
|
175
|
+
export function forwardShinFrames(track: readonly Joints[], frames: ReadonlyArray<{ depth?: FrameDepth }>): number[] {
|
|
176
|
+
const out: number[] = [];
|
|
177
|
+
frames.forEach((f, i) => {
|
|
178
|
+
const face = facingOf(track[i] ?? {});
|
|
179
|
+
if (face === 0) return;
|
|
180
|
+
if ((['r_shin', 'l_shin'] as const).some((s) => f.depth?.[s]?.source === 'derived' && f.depth[s]!.sign === face)) out.push(i);
|
|
181
|
+
});
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The DEPTH_RECLEAN refusal (#336): the file, the gait it records, the frames (the first five), and what to run. */
|
|
186
|
+
export function depthRecleanText(file: string, gait: DepthGait | null, frames: readonly number[]): string {
|
|
187
|
+
const shown = frames.slice(0, 5).join(', ') + (frames.length > 5 ? `, … +${frames.length - 5} more` : '');
|
|
188
|
+
const g = gait ?? 'walk';
|
|
189
|
+
return `${file} carries derived depth (clean --depth ${gait ?? '?'}) whose foreshortened shin leans forward, the way the trunk faces, on ${frames.length} frame(s) (${shown}): the sign table before rig-c/pose#336, where a walk's lifted foot read forward. --draw-order depth reads a walk at the ankle and would draw that foot in front of the supporting leg. Clean it again with this version (rig-pose clean --depth ${g}), which leans a foreshortened shin back in both gaits; a sequence cleaned with --depth run by an earlier release is read as it is (its signs did not change), and measured depth is not checked`;
|
|
190
|
+
}
|
|
191
|
+
|
|
146
192
|
/** The limbs' front-to-back order on one frame, from its depth and its joints' drawn lengths. */
|
|
147
193
|
export function layersOf(joints: Joints, depth: FrameDepth): FrameLayers {
|
|
148
194
|
const z: Partial<Record<Limb, number>> = {};
|
|
149
195
|
for (const limb of LIMB_NAMES) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const lu = drawn(joints, upper);
|
|
154
|
-
const ll = drawn(joints, lower);
|
|
155
|
-
if (du === undefined || dl === undefined || lu === null || ll === null) continue;
|
|
156
|
-
const mid = segmentDepth(du, lu);
|
|
157
|
-
const end = mid + segmentDepth(dl, ll);
|
|
158
|
-
z[limb] = Math.round(((mid + end) / 2) * 100) / 100;
|
|
196
|
+
const j = limbJointDepths(joints, depth, limb);
|
|
197
|
+
if (j === null || j.end === null) continue;
|
|
198
|
+
z[limb] = Math.round(((j.mid + j.end) / 2) * 100) / 100;
|
|
159
199
|
}
|
|
160
200
|
const placed = LIMB_NAMES.filter((l) => z[l] !== undefined).sort((a, b) => z[b]! - z[a]! || LIMB_NAMES.indexOf(a) - LIMB_NAMES.indexOf(b));
|
|
161
201
|
const order: Limb[][] = [];
|
package/src/draworder.ts
CHANGED
|
@@ -36,19 +36,38 @@
|
|
|
36
36
|
* ordered, holds the order before; before the first decided frame, the setup
|
|
37
37
|
* order — or, on a looped sequence, the order the clip ends in, held across
|
|
38
38
|
* the seam. A key is written where the order changes, and none while it is the
|
|
39
|
-
* setup order from the start.
|
|
39
|
+
* setup order from the start. Its time is half a frame before the frame it
|
|
40
|
+
* shows on (0 on frame 0), the rule the switches' attachment keys follow
|
|
41
|
+
* (`keyAtFrame` in `switch.ts`): a key exactly on a frame time is a tie the
|
|
42
|
+
* render breaks a frame late about half the time (rig-c/pose#325).
|
|
43
|
+
* - **By the joint the gait names** (rig-c/pose#336, the owner's two
|
|
44
|
+
* statements). On a sequence cleaned with `--depth walk` the pair is ordered
|
|
45
|
+
* by each leg's ankle depth (the hip at 0, plus the thigh's, plus the shin's:
|
|
46
|
+
* `limbJointDepths`' end) — in a walk the lifted foot is behind the supporting
|
|
47
|
+
* leg; with `--depth run` by the knee depth (the thigh's: its middle) — in a
|
|
48
|
+
* run the lifted leg is in front, its knee protruding. The limb mean cannot
|
|
49
|
+
* say either: a lifted leg's thigh forward and shin back cancel in it, and a
|
|
50
|
+
* walk's flat thigh leaves it at most half a shin (35 px on the fixtures),
|
|
51
|
+
* under the limb tie. The tie is scaled to the joint, what a flat chain can
|
|
52
|
+
* hold there: √(1 − 0.95²) × thigh at the knee, × (thigh + shin) at the
|
|
53
|
+
* ankle, the smaller leg's. Measured depth names no gait and keeps the limb
|
|
54
|
+
* mean and its tie (#304).
|
|
40
55
|
*
|
|
41
56
|
* Pure: no clock, no randomness, no disk.
|
|
42
57
|
*/
|
|
43
58
|
import type { Body18 } from './joints.ts';
|
|
44
|
-
import { FLAT_BAND, LIMBS, type FrameLayers, type Limb } from './depth.ts';
|
|
59
|
+
import { FLAT_BAND, LIMBS, limbJointDepths, type FrameLayers, type Limb } from './depth.ts';
|
|
45
60
|
import { knownSetsOf, type JointMap, type Rest, type RigSets } from './map.ts';
|
|
46
|
-
import { DEPTH_SEGMENTS, type DepthSegment, type FrameDepth, type Joint } from './pose.ts';
|
|
61
|
+
import { DEPTH_SEGMENTS, type DepthGait, type DepthSegment, type FrameDepth, type Joint } from './pose.ts';
|
|
62
|
+
import { keyAtFrame } from './switch.ts';
|
|
47
63
|
|
|
48
64
|
/** The limb pairs `--draw-order depth` keys. */
|
|
49
65
|
export const DRAW_ORDER_PAIRS: ReadonlyArray<readonly [Limb, Limb]> = [['r_leg', 'l_leg']];
|
|
50
66
|
/** What one segment inside the flat band can hide out of the picture, per unit of its rest length: √(1 − 0.95²). */
|
|
51
67
|
export const TIE_PER_LENGTH = Math.sqrt(1 - FLAT_BAND[0] * FLAT_BAND[0]);
|
|
68
|
+
/** The joint a gait's legs are ordered by (rig-c/pose#336): a walk's lifted foot is behind, a run's lifted knee in front. */
|
|
69
|
+
export const ORDER_JOINT: Record<DepthGait, LegJoint> = { walk: 'ankle', run: 'knee' };
|
|
70
|
+
export type LegJoint = 'knee' | 'ankle';
|
|
52
71
|
/** The DRAWORDER line's cap, characters (the span list is cut to it; the report carries every span). */
|
|
53
72
|
export const DRAW_ORDER_LINE_MAX = 260;
|
|
54
73
|
|
|
@@ -56,7 +75,7 @@ export interface DrawOrderOffset {
|
|
|
56
75
|
slot: string;
|
|
57
76
|
offset: number;
|
|
58
77
|
}
|
|
59
|
-
/** One key as written into the animation (`t` the frame
|
|
78
|
+
/** One key as written into the animation (`t` half a frame before the frame it shows on, 0 on frame 0: rig-c/pose#325). */
|
|
60
79
|
export interface DrawOrderKey {
|
|
61
80
|
t: number;
|
|
62
81
|
offsets?: DrawOrderOffset[];
|
|
@@ -71,6 +90,8 @@ export interface DrawOrderPair {
|
|
|
71
90
|
keyed: boolean;
|
|
72
91
|
/** #281's tie for the pair, px: depths closer than this hold the order before (null where a rest length is unread: no tie, equal depths only). */
|
|
73
92
|
tie: number | null;
|
|
93
|
+
/** #336: the joint the pair is ordered by, from the gait clean recorded (the tie then the joint's); absent, the limb mean (#304). */
|
|
94
|
+
by?: LegJoint;
|
|
74
95
|
/** Why the pair is not keyed. */
|
|
75
96
|
why?: string;
|
|
76
97
|
/** The slots of anything else drawn between the pair's slots, in setup order (#324: the line counts them first and cuts this list). */
|
|
@@ -80,7 +101,7 @@ export interface DrawOrderPair {
|
|
|
80
101
|
export interface DrawOrderGap {
|
|
81
102
|
a: string;
|
|
82
103
|
b: string;
|
|
83
|
-
/** |depth a − depth b| over the frames both limbs read, px: the median and the largest. */
|
|
104
|
+
/** |depth a − depth b| over the frames both limbs read, px: the median and the largest (at the pair's joint, when it has one). */
|
|
84
105
|
median: number | null;
|
|
85
106
|
max: number | null;
|
|
86
107
|
/** The same as a fraction of the leg (upper + lower rest length of the limb the tie is read from); null where a rest length is unread. */
|
|
@@ -103,7 +124,7 @@ export interface DrawOrderSpan {
|
|
|
103
124
|
}
|
|
104
125
|
export interface DrawOrderReading {
|
|
105
126
|
pairs: DrawOrderPair[];
|
|
106
|
-
/** The `drawOrder` timeline, with the frame each key
|
|
127
|
+
/** The `drawOrder` timeline as written, with the frame each key first shows on. */
|
|
107
128
|
keys: Array<DrawOrderKey & { frame: number }>;
|
|
108
129
|
/** Every span of one decided order, setup or keyed, in frame order per pair. */
|
|
109
130
|
spans: DrawOrderSpan[];
|
|
@@ -225,12 +246,15 @@ function restOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | unde
|
|
|
225
246
|
|
|
226
247
|
/**
|
|
227
248
|
* #281's tie for two limbs, px: √(1 − 0.95²) × (upper + lower ÷ 2), the smaller limb's; null where a rest length is unread.
|
|
249
|
+
* By a joint (#336): √(1 − 0.95²) × upper at the knee, × (upper + lower) at the ankle — what a flat chain can hold there.
|
|
228
250
|
* `leg` is that limb's upper + lower rest length (#324's unit for the gap).
|
|
229
251
|
*/
|
|
230
|
-
function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, limbs: readonly [Limb, Limb]): { tie: number | null; leg: number | null } {
|
|
252
|
+
function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, limbs: readonly [Limb, Limb], by: LegJoint | undefined): { tie: number | null; leg: number | null } {
|
|
231
253
|
const rests = limbs.map((l) => {
|
|
232
254
|
const [u, w] = LIMBS[l].map((s) => restOf(track, depth, s));
|
|
233
|
-
|
|
255
|
+
// The knee's tie needs the upper segment alone (#336: a leg whose ankle is never placed still orders at the knee).
|
|
256
|
+
if (by === 'knee') return u === null || u === undefined ? null : { hidden: u, leg: w === null || w === undefined ? null : u + w };
|
|
257
|
+
return u === null || u === undefined || w === null || w === undefined ? null : { hidden: by === 'ankle' ? u + w : u + w / 2, leg: u + w };
|
|
234
258
|
});
|
|
235
259
|
if (rests[0] == null || rests[1] == null) return { tie: null, leg: null };
|
|
236
260
|
const r = rests[1].hidden < rests[0].hidden ? rests[1] : rests[0];
|
|
@@ -240,13 +264,13 @@ function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undef
|
|
|
240
264
|
const round = (x: number, k: number): number => Math.round(x * 10 ** k) / 10 ** k;
|
|
241
265
|
|
|
242
266
|
/** One pair's leg depth gap against its tie over the frames (the rule's own test: unequal and at least the tie). */
|
|
243
|
-
function gapOf(pair: DrawOrderPair,
|
|
267
|
+
function gapOf(pair: DrawOrderPair, zs: ReadonlyArray<Partial<Record<Limb, number>>>, leg: number | null): DrawOrderGap {
|
|
244
268
|
const tie = pair.tie ?? 0;
|
|
245
269
|
const gaps: number[] = [];
|
|
246
270
|
let passed = 0;
|
|
247
|
-
for (const
|
|
248
|
-
const za =
|
|
249
|
-
const zb =
|
|
271
|
+
for (const z of zs) {
|
|
272
|
+
const za = z[pair.limbs[0]];
|
|
273
|
+
const zb = z[pair.limbs[1]];
|
|
250
274
|
if (za === undefined || zb === undefined) continue;
|
|
251
275
|
gaps.push(Math.abs(za - zb));
|
|
252
276
|
if (za !== zb && Math.abs(za - zb) >= tie) passed++;
|
|
@@ -262,25 +286,42 @@ function gapOf(pair: DrawOrderPair, layers: readonly FrameLayers[], leg: number
|
|
|
262
286
|
/**
|
|
263
287
|
* The draw-order keys from the frames' layers. `track` and `depth` are the sequence's
|
|
264
288
|
* joints and depth per frame (the rest lengths the tie reads); `order` is the rig's
|
|
265
|
-
* setup draw order (the compiled model's slots); `times` the frames' times.
|
|
289
|
+
* setup draw order (the compiled model's slots); `times` the frames' times. `gait` is
|
|
290
|
+
* the gait clean recorded for its derived depth: a walk orders by the ankle, a run by
|
|
291
|
+
* the knee (#336); none (measured depth) by the limb mean (#304).
|
|
266
292
|
*/
|
|
267
|
-
export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, times: readonly number[], map: JointMap, rest: Rest, order: ReadonlyArray<{ name: string; bone: string }>, sets?: RigSets, loop = false): DrawOrderReading {
|
|
293
|
+
export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, times: readonly number[], map: JointMap, rest: Rest, order: ReadonlyArray<{ name: string; bone: string }>, sets?: RigSets, loop = false, gait: DepthGait | null = null): DrawOrderReading {
|
|
294
|
+
const by = gait === null ? undefined : ORDER_JOINT[gait];
|
|
268
295
|
const resolved = DRAW_ORDER_PAIRS.map((p) => resolvePair(p, map, rest, order, sets));
|
|
269
296
|
const legs = resolved.map((r) => {
|
|
270
|
-
const t = tieOf(track, depth, r.pair.limbs);
|
|
297
|
+
const t = tieOf(track, depth, r.pair.limbs, by);
|
|
271
298
|
r.pair.tie = t.tie;
|
|
299
|
+
if (by !== undefined) r.pair.by = by;
|
|
272
300
|
return t.leg;
|
|
273
301
|
});
|
|
302
|
+
// #336: per frame, each limb's depth at the gait's joint (rounded as the layers are); without a gait, the layers' limb means.
|
|
303
|
+
const zs: ReadonlyArray<Partial<Record<Limb, number>>> =
|
|
304
|
+
by === undefined
|
|
305
|
+
? layers.map((l) => l.depth)
|
|
306
|
+
: track.map((j, i) => {
|
|
307
|
+
const z: Partial<Record<Limb, number>> = {};
|
|
308
|
+
for (const limb of new Set(DRAW_ORDER_PAIRS.flat())) {
|
|
309
|
+
const d = depth[i] === undefined ? null : limbJointDepths(j, depth[i]!, limb);
|
|
310
|
+
const v = d === null ? null : by === 'knee' ? d.mid : d.end;
|
|
311
|
+
if (v !== null) z[limb] = Math.round(v * 100) / 100;
|
|
312
|
+
}
|
|
313
|
+
return z;
|
|
314
|
+
});
|
|
274
315
|
const names = order.map((s) => s.name);
|
|
275
316
|
// Per pair, per frame: who is behind ('a' | 'b'), or null for the setup order before any decision.
|
|
276
317
|
const states = resolved.map((r) => {
|
|
277
318
|
const tie = r.pair.tie ?? 0;
|
|
278
319
|
const pass = (start: 'a' | 'b' | null): Array<'a' | 'b' | null> => {
|
|
279
320
|
let state = start;
|
|
280
|
-
return
|
|
321
|
+
return zs.map((z) => {
|
|
281
322
|
if (!r.pair.keyed) return null;
|
|
282
|
-
const za =
|
|
283
|
-
const zb =
|
|
323
|
+
const za = z[r.pair.limbs[0]];
|
|
324
|
+
const zb = z[r.pair.limbs[1]];
|
|
284
325
|
if (za !== undefined && zb !== undefined && za !== zb && Math.abs(za - zb) >= tie) state = za < zb ? 'a' : 'b';
|
|
285
326
|
return state;
|
|
286
327
|
});
|
|
@@ -298,12 +339,14 @@ export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readon
|
|
|
298
339
|
.sort((x, y) => names.indexOf(x.slot) - names.indexOf(y.slot));
|
|
299
340
|
const keys: DrawOrderReading['keys'] = [];
|
|
300
341
|
let last = '[]';
|
|
301
|
-
for (let f = 0; f <
|
|
342
|
+
for (let f = 0; f < zs.length; f++) {
|
|
302
343
|
const offs = offsetsAt(f);
|
|
303
344
|
const text = JSON.stringify(offs);
|
|
304
345
|
if (text === last) continue;
|
|
305
346
|
last = text;
|
|
306
|
-
|
|
347
|
+
// Half a frame before the frame it shows on, as the switches' keys (`keyAtFrame`, rig-c/pose#325).
|
|
348
|
+
const t = keyAtFrame(times, f);
|
|
349
|
+
keys.push(offs.length === 0 ? { frame: f, t } : { frame: f, t, offsets: offs });
|
|
307
350
|
}
|
|
308
351
|
const spans: DrawOrderSpan[] = [];
|
|
309
352
|
resolved.forEach((r, k) => {
|
|
@@ -314,7 +357,7 @@ export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readon
|
|
|
314
357
|
else spans.push({ behind: s === 'a' ? r.pair.a : r.pair.b, front: s === 'a' ? r.pair.b : r.pair.a, frames: [f, f], keyed: (s === 'a' ? r.aBehind : r.bBehind).length > 0 });
|
|
315
358
|
});
|
|
316
359
|
});
|
|
317
|
-
return { pairs: resolved.map((r) => r.pair), keys, spans, gaps: resolved.map((r, k) => gapOf(r.pair,
|
|
360
|
+
return { pairs: resolved.map((r) => r.pair), keys, spans, gaps: resolved.map((r, k) => gapOf(r.pair, zs, legs[k]!)) };
|
|
318
361
|
}
|
|
319
362
|
|
|
320
363
|
/** The animation's `drawOrder` value: the keys without their frames. */
|
|
@@ -330,7 +373,9 @@ const span = (f: [number, number]): string => (f[0] === f[1] ? `${f[0]}` : `${f[
|
|
|
330
373
|
*/
|
|
331
374
|
export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): string[] {
|
|
332
375
|
const keyed = d.spans.filter((s) => s.keyed);
|
|
333
|
-
|
|
376
|
+
// #336: a keyed pair ordered by a joint names it and its joint-scaled tie.
|
|
377
|
+
const joint = d.pairs.find((p) => p.by !== undefined && p.keyed);
|
|
378
|
+
const head = `${prefix}from depth${joint === undefined ? '' : ` by the ${joint.by} (${joint.by === 'ankle' ? 'walk' : 'run'}; tie ${joint.tie === null ? 'none' : `${joint.tie} px`})`}, ${d.keys.length} key(s) over ${keyed.length} span(s)`;
|
|
334
379
|
const groups = new Map<string, string[]>();
|
|
335
380
|
for (const s of keyed) {
|
|
336
381
|
const g = `${s.behind} behind ${s.front}`;
|
|
@@ -366,12 +411,18 @@ export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): stri
|
|
|
366
411
|
/** px with its fraction of the leg: "12.3 px (0.084 leg)". */
|
|
367
412
|
const pxLeg = (px: number | null, legs: number | null): string => (px === null ? 'unread' : `${px} px${legs === null ? '' : ` (${legs} leg)`}`);
|
|
368
413
|
|
|
414
|
+
/** " at the knee" / " at the ankle" for a pair ordered by a joint (#336), else nothing. */
|
|
415
|
+
const gapAt = (d: DrawOrderReading, g: DrawOrderGap): string => {
|
|
416
|
+
const j = d.pairs.find((p) => p.a === g.a && p.b === g.b)?.by;
|
|
417
|
+
return j === undefined ? '' : ` at the ${j}`;
|
|
418
|
+
};
|
|
419
|
+
|
|
369
420
|
/**
|
|
370
421
|
* The DRAWGAP lines (rig-c/pose#324): one per pair, keyed or not — the leg depth gap's median and
|
|
371
422
|
* max against the tie, and how many frames passed it. Cut to `DRAW_ORDER_LINE_MAX` characters.
|
|
372
423
|
*/
|
|
373
424
|
export function drawGapLines(d: DrawOrderReading, prefix = 'DRAWGAP '): string[] {
|
|
374
425
|
return (d.gaps ?? []).map((g) =>
|
|
375
|
-
`${prefix}${g.a} / ${g.b}: leg depth gap median ${pxLeg(g.median, g.medianLegs)}, max ${pxLeg(g.max, g.maxLegs)}, against the tie of ${g.tie === null ? 'none (a rest length is unread; equal depths only)' : pxLeg(g.tie, g.tieLegs)}; ${g.passed} of ${g.frames} frame(s) passed it`.slice(0, DRAW_ORDER_LINE_MAX),
|
|
426
|
+
`${prefix}${g.a} / ${g.b}: leg depth gap${gapAt(d, g)} median ${pxLeg(g.median, g.medianLegs)}, max ${pxLeg(g.max, g.maxLegs)}, against the tie of ${g.tie === null ? 'none (a rest length is unread; equal depths only)' : pxLeg(g.tie, g.tieLegs)}; ${g.passed} of ${g.frames} frame(s) passed it`.slice(0, DRAW_ORDER_LINE_MAX),
|
|
376
427
|
);
|
|
377
428
|
}
|
package/src/legs.ts
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
* frames (`closeLoop`), so the loop carries no net travel.
|
|
59
59
|
*/
|
|
60
60
|
import type { Chain, JointMap, Rest } from './map.ts';
|
|
61
|
+
import type { ContactKeysReading } from './contacts.ts';
|
|
61
62
|
|
|
62
63
|
type Pt = [number, number];
|
|
63
64
|
/** A world 2×2: columns (a, c) — the x axis — and (b, d) — the y axis. */
|
|
@@ -522,6 +523,12 @@ export interface Feet {
|
|
|
522
523
|
* `supportsOf` on every reading.
|
|
523
524
|
*/
|
|
524
525
|
signal?: SupportSignal;
|
|
526
|
+
/**
|
|
527
|
+
* rig-c/pose#303: what `--contact-keys` did — the keys, the frames they cover,
|
|
528
|
+
* the support without them and the runs they changed. Set by `contactKeysOf`,
|
|
529
|
+
* applied last; absent without the flag.
|
|
530
|
+
*/
|
|
531
|
+
contactKeys?: ContactKeysReading;
|
|
525
532
|
}
|
|
526
533
|
|
|
527
534
|
/** A run of frames whose support the lower-ankle rule (rig-c/pose#113) reads differently from the reading before it. */
|
package/src/pose.ts
CHANGED
|
@@ -74,7 +74,7 @@ export interface SegmentDepth {
|
|
|
74
74
|
source: DepthSource;
|
|
75
75
|
}
|
|
76
76
|
export type FrameDepth = Partial<Record<DepthSegment, SegmentDepth>>;
|
|
77
|
-
/** The gait `clean --depth` was told the clip is: it decides the
|
|
77
|
+
/** The gait `clean --depth` was told the clip is (rig-c/pose#336): the signs are the same for both; it decides the joint `--draw-order depth` orders the legs by. */
|
|
78
78
|
export const DEPTH_GAITS = ['walk', 'run'] as const;
|
|
79
79
|
export type DepthGait = (typeof DEPTH_GAITS)[number];
|
|
80
80
|
/**
|
package/src/retarget.ts
CHANGED
|
@@ -80,8 +80,9 @@ import type { Joint, Travel } from './pose.ts';
|
|
|
80
80
|
import { TRAVEL_IN_PLACE } from './pose.ts';
|
|
81
81
|
import { Problems, Refusal } from './problems.ts';
|
|
82
82
|
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SetRuns, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
|
-
import { clampArt, coveredChains, isRaised, measureArt, overflow, phaseGroupProblems, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
83
|
+
import { clampArt, coveredChains, isRaised, keyAtFrame, measureArt, overflow, phaseGroupProblems, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
84
84
|
import type { BlendHold, Closure, Feet, Floor, LegScale, LegSegmentRatio, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
|
+
import { contactKeysOf, type ContactKeys } from './contacts.ts';
|
|
85
86
|
import { figureScaleOf, freeAnkleShiftOf, holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, percentile, plantHip, posePath, rootOf, scaleSupport, shiftOver, SIDES, stanceOf, stanceRangeOf, STRAIGHT_PERCENTILE, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
86
87
|
import type { BodyReading, FaceReading, YawSetSwitch } from './face.ts';
|
|
87
88
|
import { readBody, readFace, readFaceAngles, selectYawSets } from './face.ts';
|
|
@@ -181,6 +182,12 @@ export interface RetargetOptions extends SwitchOptions {
|
|
|
181
182
|
* the hip line on the frames it does not; absent, from the hip line.
|
|
182
183
|
*/
|
|
183
184
|
pelvis?: { source: string; tilt: ReadonlyArray<number | null> };
|
|
185
|
+
/**
|
|
186
|
+
* rig-c/pose#303: the hand contact keys (`--contact-keys`), read against the
|
|
187
|
+
* sequence. Applied last over the supports read (`contactKeysOf`): on every
|
|
188
|
+
* frame they cover they decide the support. Absent, nothing changes.
|
|
189
|
+
*/
|
|
190
|
+
contactKeys?: ContactKeys;
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
/**
|
|
@@ -1020,6 +1027,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1020
1027
|
else if (plantOn) p.add('LEGS_MAP', `--plant-feet reads both hips and both ankles from the pose; the map's chains use ${['r_hip', 'l_hip', 'r_ankle', 'l_ankle'].filter((j) => !series.has(j)).join(', ')} nowhere`);
|
|
1021
1028
|
}
|
|
1022
1029
|
}
|
|
1030
|
+
// rig-c/pose#303: the contact keys decide last, over every reading above; with no support read they have nothing to decide, so they are refused.
|
|
1031
|
+
if (opts.contactKeys !== undefined) {
|
|
1032
|
+
if (feet !== null) feet = contactKeysOf(feet, opts.contactKeys);
|
|
1033
|
+
else p.add('CONTACT_KEYS', `--contact-keys sets which foot supports, and no support is read: ${legs !== null && 'problems' in legs ? legs.problems.join('; ') : `the map's chains use ${['r_hip', 'l_hip', 'r_ankle', 'l_ankle'].filter((j) => !series.has(j)).join(', ')} nowhere`}`);
|
|
1034
|
+
}
|
|
1023
1035
|
const heldPt = (name: string): Pt[] | null => {
|
|
1024
1036
|
const known = series.get(name);
|
|
1025
1037
|
if (known !== undefined) return known;
|
|
@@ -3048,7 +3060,6 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3048
3060
|
if (!slotOrder.includes(sl)) slotOrder.push(sl);
|
|
3049
3061
|
}
|
|
3050
3062
|
}
|
|
3051
|
-
const yawExactAt = new Map<string, Set<number>>();
|
|
3052
3063
|
for (const s of activeYawSets) {
|
|
3053
3064
|
const def = known.get(s.set);
|
|
3054
3065
|
if (def === undefined) continue;
|
|
@@ -3058,10 +3069,6 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3058
3069
|
const row = sw.shown.get(sl)!;
|
|
3059
3070
|
for (const [lo, hi] of s.spans) {
|
|
3060
3071
|
for (let i = lo; i <= hi; i++) row[i] = v;
|
|
3061
|
-
const exact = yawExactAt.get(sl) ?? new Set<number>();
|
|
3062
|
-
exact.add(lo);
|
|
3063
|
-
if (hi + 1 < n) exact.add(hi + 1);
|
|
3064
|
-
yawExactAt.set(sl, exact);
|
|
3065
3072
|
}
|
|
3066
3073
|
}
|
|
3067
3074
|
}
|
|
@@ -3071,11 +3078,12 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3071
3078
|
if (row === undefined) continue;
|
|
3072
3079
|
let prev = slots.get(sl)?.setup ?? null;
|
|
3073
3080
|
const keys: AttachmentKey[] = [];
|
|
3074
|
-
const exact = yawExactAt.get(sl);
|
|
3075
3081
|
row.forEach((v, i) => {
|
|
3076
3082
|
if (v === prev) return;
|
|
3077
|
-
|
|
3078
|
-
|
|
3083
|
+
// Half a frame before the frame it shows on, as every discrete key pose writes (`keyAtFrame`, rig-c/pose#334):
|
|
3084
|
+
// written on their frames' exact times, 7 of the 10 yaw swaps of the 24 fps head turn in test/retarget.test.ts
|
|
3085
|
+
// showed a frame late under the render's clock.
|
|
3086
|
+
keys.push({ t: keyAtFrame(keyTimes, i), v });
|
|
3079
3087
|
prev = v;
|
|
3080
3088
|
});
|
|
3081
3089
|
if (keys.length > 0) rebuilt.push({ slot: sl, property: 'attachment', keys });
|
package/src/run.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { tmpdir } from 'node:os';
|
|
|
10
10
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
11
11
|
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
12
12
|
import { cleanLine, cleanSummaryLine, inferredSource } from './clean.ts';
|
|
13
|
-
import { depthLine, layersOf, type FrameLayers } from './depth.ts';
|
|
13
|
+
import { depthLine, depthRecleanText, forwardShinFrames, layersOf, type FrameLayers } from './depth.ts';
|
|
14
14
|
import { drawGapLines, drawOrderFromDepth, drawOrderKeys, drawOrderLines, type DrawOrderReading } from './draworder.ts';
|
|
15
15
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
16
16
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
@@ -27,6 +27,7 @@ import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
|
|
|
27
27
|
import { PROVENANCE_FILE, provenanceText } from './provenance.ts';
|
|
28
28
|
import { madeLine, planOutDirs } from './outdir.ts';
|
|
29
29
|
import { bodyLine, faceLine } from './face.ts';
|
|
30
|
+
import { contactKeysLines, readContactKeys } from './contacts.ts';
|
|
30
31
|
|
|
31
32
|
export interface RetargetArgs extends RetargetOptions {
|
|
32
33
|
rig: string;
|
|
@@ -81,6 +82,8 @@ export interface RetargetArgs extends RetargetOptions {
|
|
|
81
82
|
output?: 'default' | 'verbose' | 'summary';
|
|
82
83
|
/** `--draw-order depth` (rig-c/pose#304): key the animation's draw order from the sequence's depth. Absent: no draw-order key, the output as before. */
|
|
83
84
|
drawOrder?: 'depth';
|
|
85
|
+
/** `--contact-keys` (rig-c/pose#303): the hand contact keys' file, `rig-pose-contact-keys/1`. Absent: the support as read, the output as before. */
|
|
86
|
+
contactKeysFile?: string;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
export interface RunResult {
|
|
@@ -104,7 +107,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
104
107
|
throw new Refusal(problems);
|
|
105
108
|
};
|
|
106
109
|
const args = new Problems();
|
|
107
|
-
for (const [flag, path] of [['--map', a.map], ['--pose', a.pose], ['--merge', a.merge]] as const) {
|
|
110
|
+
for (const [flag, path] of [['--map', a.map], ['--pose', a.pose], ['--merge', a.merge], ['--contact-keys', a.contactKeysFile]] as const) {
|
|
108
111
|
if (path !== undefined && !existsSync(path)) args.add('ARG_PATH', `${flag} ${path} does not exist`);
|
|
109
112
|
}
|
|
110
113
|
// rig-c/pose#239: the report is written unless asked elsewhere — beside the motion, as <out>.report.json.
|
|
@@ -141,6 +144,15 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
141
144
|
// #304: the draw order is read off the sequence's depth; asked for on a sequence without it, refused by name.
|
|
142
145
|
if (a.drawOrder === 'depth' && !seq.frames.some((x) => x.depth !== undefined)) refuse([{ code: 'DRAWORDER_NO_DEPTH', text: `--draw-order depth keys the draw order from the sequence's depth, and ${a.pose} carries none (rig-pose-sequence/${seq.cleaned!.params.depth === undefined ? '1 or 1.1, no "depth" on any frame' : '1.2 with no frame naming a segment'}); clean it with --depth walk|run, or retarget without --draw-order` }]);
|
|
143
146
|
const frames = personTrack(seq, a.person, a.pose);
|
|
147
|
+
// #336: a derived depth whose shins lean the way the trunk faces was cleaned with the table before #336 (a walk's lifted
|
|
148
|
+
// foot forward); the draw order now reads a walk at the ankle, and would draw that foot in front. Refused by name.
|
|
149
|
+
if (a.drawOrder === 'depth') {
|
|
150
|
+
const old = forwardShinFrames(frames, seq.frames);
|
|
151
|
+
if (old.length > 0) refuse([{ code: 'DEPTH_RECLEAN', text: depthRecleanText(a.pose, seq.cleaned!.params.depth?.gait ?? null, old) }]);
|
|
152
|
+
}
|
|
153
|
+
// rig-c/pose#303: the contact keys, read against the sequence's frames before any gate runs.
|
|
154
|
+
const contactKeysSnapshot = a.contactKeysFile === undefined ? null : readFileSync(a.contactKeysFile);
|
|
155
|
+
const contactKeys = contactKeysSnapshot === null ? null : readContactKeys(contactKeysSnapshot.toString('utf8'), a.contactKeysFile!, seq.frames.length);
|
|
144
156
|
const times = seq.frames.map((f) => f.t);
|
|
145
157
|
|
|
146
158
|
const scratch = mkdtempSync(join(tmpdir(), 'rig-pose-'));
|
|
@@ -156,7 +168,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
156
168
|
const slots = slotsOf(doc0);
|
|
157
169
|
resolveMap(map, rest, a.map, slots, input.sets);
|
|
158
170
|
|
|
159
|
-
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}), ...(seq.pelvisSource !== undefined ? { pelvis: { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } } : {}) }, slots, input.sets);
|
|
171
|
+
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}), ...(seq.pelvisSource !== undefined ? { pelvis: { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } } : {}), ...(contactKeys !== null ? { contactKeys } : {}) }, slots, input.sets);
|
|
160
172
|
if (fragment.length > 0 && ((r.face?.sets?.length ?? 0) > 0 || (r.body?.sets?.length ?? 0) > 0)) {
|
|
161
173
|
const known = knownSetsOf(map, input.sets);
|
|
162
174
|
for (const s of [...(r.face?.sets ?? []), ...(r.body?.sets ?? [])]) {
|
|
@@ -227,8 +239,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
227
239
|
// rig-c/pose#281: the limbs' front-to-back order from the sequence's depth, only when it carries depth.
|
|
228
240
|
const layers: FrameLayers[] | null = seq.frames.some((x) => x.depth !== undefined) ? frames.map((j, i) => layersOf(j, seq.frames[i]!.depth ?? {})) : null;
|
|
229
241
|
if (layers !== null) say(` ${depthLine(layers, seq.frames, seq.cleaned!.params.depth?.gait ?? null)}`);
|
|
230
|
-
// rig-c/pose#304: with --draw-order depth, the legs' draw order keyed from
|
|
231
|
-
const drawOrder: DrawOrderReading | null = a.drawOrder === 'depth' && layers !== null ? drawOrderFromDepth(layers, frames, seq.frames.map((x) => x.depth), r.times, map, rest, doc0.slots, input.sets, seq.cleaned!.params.loop) : null;
|
|
242
|
+
// rig-c/pose#304: with --draw-order depth, the legs' draw order keyed from the depth (#336: at the joint the recorded gait names).
|
|
243
|
+
const drawOrder: DrawOrderReading | null = a.drawOrder === 'depth' && layers !== null ? drawOrderFromDepth(layers, frames, seq.frames.map((x) => x.depth), r.times, map, rest, doc0.slots, input.sets, seq.cleaned!.params.loop, seq.cleaned!.params.depth?.gait ?? null) : null;
|
|
232
244
|
if (drawOrder !== null) for (const l of [...drawOrderLines(drawOrder), ...drawGapLines(drawOrder)]) say(` ${l}`);
|
|
233
245
|
if (r.pivot !== null) say(` PIVOT bone "${r.pivot.bone}"'s origin is ${r.pivot.distance.toFixed(2)} units (${r.pivot.hipWidths.toFixed(3)} hip widths) from its thigh roots' midpoint (${r.pivot.roots.join(' + ')}): dx ${r.pivot.offset[0].toFixed(2)}, dy ${r.pivot.offset[1].toFixed(2)}, at setup — the pivot every hip turn swings the legs about`);
|
|
234
246
|
if (r.feet !== null) {
|
|
@@ -237,7 +249,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
237
249
|
say(` FEET ${r.planted ? 'planted' : 'read, not planted (no --plant-feet)'}: leg length (median hip → ankle) r ${f.legLength.r.toFixed(1)}px, l ${f.legLength.l.toFixed(1)}px; ${f.supports.length} support span(s), ${f.changes.filter((c) => c.from !== null).length} change(s) of support, ${f.jumps.length} jump(s), ${f.steps.length} step(s)`);
|
|
238
250
|
for (const sg of r.segments) say(` FEET ${segmentLine(sg)}`);
|
|
239
251
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
240
|
-
|
|
252
|
+
// rig-c/pose#303: with contact keys, their stride lines stand in for #280's (the keys decide the frames they cover).
|
|
253
|
+
const signal = f.signal === undefined ? [] : signalLines(f.signal, f.supports);
|
|
254
|
+
for (const l of f.contactKeys === undefined ? signal : [...signal.filter((x) => !x.startsWith('stride ')), ...contactKeysLines(f)]) say(` FEET ${l}`);
|
|
241
255
|
say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
|
|
242
256
|
if (f.lowered !== undefined) say(` FEET ${loweredLine(f.lowered)}`);
|
|
243
257
|
if (f.tracked !== undefined) say(` FEET tracked (the lower foot supports on the floor, following its scaled ankle track until still, rig-c/pose#119): ${f.tracked.map((x) => `${x.side} ${span(x.frames)}`).join(', ')}`);
|
|
@@ -423,7 +437,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
423
437
|
|
|
424
438
|
assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
425
439
|
// Every retarget option as resolved: the report's `options`, and with the name the build record's `flags` (#270).
|
|
426
|
-
const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', ...(a.reduceChains === undefined ? {} : { reduceChains: a.reduceChains }), reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}), ...(a.drawOrder !== undefined ? { drawOrder: a.drawOrder } : {}) };
|
|
440
|
+
const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', ...(a.reduceChains === undefined ? {} : { reduceChains: a.reduceChains }), reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}), ...(a.drawOrder !== undefined ? { drawOrder: a.drawOrder } : {}), ...(a.contactKeysFile !== undefined ? { contactKeys: a.contactKeysFile } : {}) };
|
|
427
441
|
const reportText = writeReport({
|
|
428
442
|
spec: 'rig-pose-report/1',
|
|
429
443
|
rigc,
|
|
@@ -485,8 +499,11 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
485
499
|
pelvis: r.pelvis,
|
|
486
500
|
// Only when the sequence carries depth (#281): a sequence without it writes the report it always has.
|
|
487
501
|
...(layers !== null ? { layers: layers.map((l, frame) => ({ frame, order: l.order, depth: l.depth })) } : {}),
|
|
488
|
-
// Only with --draw-order depth (#304).
|
|
489
|
-
|
|
502
|
+
// Only with --draw-order depth (#304). A key's `t` here is its frame's time, the report's clock (#214); the
|
|
503
|
+
// motion's key is written half a frame before it (#325).
|
|
504
|
+
...(drawOrder !== null ? { drawOrder: { source: 'depth', pairs: drawOrder.pairs, keys: drawOrder.keys.map((k) => ({ ...k, t: r.times[k.frame]! })), spans: drawOrder.spans, gaps: drawOrder.gaps ?? [] } } : {}),
|
|
505
|
+
// Only with --contact-keys (#303).
|
|
506
|
+
...(r.feet?.contactKeys !== undefined && contactKeysSnapshot !== null ? { contactKeys: { path: a.contactKeysFile, sha256: createHash('sha256').update(contactKeysSnapshot).digest('hex'), keys: r.feet.contactKeys.keys.length, from: r.feet.contactKeys.from, changed: r.feet.contactKeys.changed, kept: r.feet.contactKeys.kept, unreached: r.feet.contactKeys.unreached } } : {}),
|
|
490
507
|
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
|
|
491
508
|
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, straight: r.legStraight, rigLeg: r.rigLeg, segments: r.segments, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), ...(r.feet.tracked === undefined ? {} : { tracked: r.feet.tracked }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
|
|
492
509
|
shear,
|
package/src/switch.ts
CHANGED
|
@@ -476,8 +476,15 @@ function moveBoundaries(vals: ReadonlyArray<AlternateSet | null>, pos: readonly
|
|
|
476
476
|
return moves;
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
/**
|
|
480
|
-
|
|
479
|
+
/**
|
|
480
|
+
* The key time for a stepped change that shows on frame i: half a frame before it, 0 on the first frame. A key
|
|
481
|
+
* exactly on a frame time is a tie the render breaks either way (its track time is a running sum of 1/fps, key
|
|
482
|
+
* times are held as float32, and a key applies when its time ≤ the track time), so about half such keys land a
|
|
483
|
+
* frame late (rig-c/pose#325); the midpoint sits half a frame from both ties. Every discrete key pose writes goes
|
|
484
|
+
* through it: the attachment and offset keys here, the draw-order keys in `draworder.ts`, and the face and body yaw
|
|
485
|
+
* sets' attachment keys in `retarget.ts` (rig-c/pose#334).
|
|
486
|
+
*/
|
|
487
|
+
export const keyAtFrame = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
|
|
481
488
|
|
|
482
489
|
function keyHandOffset(a: AlternateSet, map: JointMap, cn: string): [number, number] | null {
|
|
483
490
|
const c = map.chains.find((x) => x.name === cn);
|
|
@@ -1116,7 +1123,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
1116
1123
|
const keys: AttachmentKey[] = [];
|
|
1117
1124
|
row.forEach((v, i) => {
|
|
1118
1125
|
if (v === prev) return;
|
|
1119
|
-
keys.push({ t:
|
|
1126
|
+
keys.push({ t: keyAtFrame(times, i), v });
|
|
1120
1127
|
prev = v;
|
|
1121
1128
|
});
|
|
1122
1129
|
if (keys.length > 0) slotTracks.push({ slot: s, property: 'attachment', keys });
|
|
@@ -1129,7 +1136,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
1129
1136
|
row.forEach((v, i) => {
|
|
1130
1137
|
const prev = row[i - 1];
|
|
1131
1138
|
if (i === 0 || (prev![0] === v[0] && prev![1] === v[1])) return;
|
|
1132
|
-
keys.push({ t:
|
|
1139
|
+
keys.push({ t: keyAtFrame(times, i), v: [r3(v[0]), r3(v[1])] });
|
|
1133
1140
|
});
|
|
1134
1141
|
// Every key but the last holds until the next (a stepped curve on the last has nothing to ease to).
|
|
1135
1142
|
for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';
|