@rig-c/pose 0.29.0 → 0.30.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 +9 -6
- package/package.json +1 -1
- package/src/report.ts +88 -0
- package/src/retarget.ts +240 -120
- package/src/run.ts +64 -46
- package/src/switch.ts +64 -76
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ A retarget of the cleaned dancer, with `--flip 90 --short 0.35 --bends clamp --l
|
|
|
107
107
|
rig-pose retarget --rig out/rig --map map.json --pose dance.pose.json --out motion.json --name dance
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. When a head chain carries the eye line (`r_eye → l_eye`), the eyes' vertical travel against `neck` in the reference, scaled into the rig's units, keys a vertical `translate` nod channel on the head bone ([#111](https://github.com/rig-c/pose/issues/111)) clamped within `±2%` of the setup neck length (`head − neck`, [#181](https://github.com/rig-c/pose/issues/181)), while
|
|
110
|
+
For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. When a head chain carries the eye line (`r_eye → l_eye`), the eyes' vertical travel against `neck` in the reference, scaled into the rig's units, keys a vertical `translate` nod channel on the head bone ([#111](https://github.com/rig-c/pose/issues/111)) clamped within `±2%` of the setup neck length (`head − neck`, [#181](https://github.com/rig-c/pose/issues/181)), while on sequences where that clamp acts, the clamped vertical nod (`clampedY`) is coupled in phase into `face.pitch`'s dial (`+2.8°/unit`, mapped through `face.pitch.range`) and the head bone's `rotate` track (`+1.6°/unit`, [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205); gains calibrated against the hand-polished reference `p1`). The result is one animation in rig-c's motion format (`rigc-motion/1`): a `rotate` track per mapped bone, a `translate` track on the hip (and on the head when the reference nods), one key per pose frame — and, when the map lists alternate drawings, an `attachment` track per slot they switch (and a `translate` track per bone a set offsets).
|
|
111
111
|
|
|
112
112
|
Nothing is written until every check passes:
|
|
113
113
|
|
|
@@ -147,7 +147,10 @@ and what the run cannot decide for you it lists:
|
|
|
147
147
|
- `BODY` — per-frame body yaw read from the camera-normalised shoulder width (`r_shoulder + l_shoulder`) and hip width (`r_hip + l_hip`) projected perpendicular to the trunk against the sequence's standing width (`yaw ≈ acos(w / w_standing)`, [#173](https://github.com/rig-c/pose/issues/173)): its overall and opening (`frames 0–47`) range, every span with `|yaw| ≥ 45°`, and, when `body.yaw` or `body.sets` is declared in the map, its knee-mapped dial keys and every yaw-switched set's entered spans, swap count, and any `hides` slot keyed by another track, written as `body` in `--report`.
|
|
148
148
|
- `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
|
|
149
149
|
|
|
150
|
-
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `frames`, `duration`, `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), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `posture`, `folds`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `feet` (`planted`, `legLength`, `supports`, `jumps`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; 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? }` (`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), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances and each row of `REDUCE`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
|
|
150
|
+
`--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), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `posture`, `folds`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `feet` (`planted`, `legLength`, `supports`, `jumps`, `jumpEvents`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; 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? }` (`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), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances and each row of `REDUCE`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
|
|
151
|
+
|
|
152
|
+
Report event objects with a pose `frame` or inclusive `frames` also carry exact `t` or `times` from `frameTimes`; existing `seconds` remain span lengths, with additive `spanSeconds`. Holds preserve their frame-index `from`/`to`. Existing scan joint/chain identities remain beside map-derived `bone` (a unique mapped target) or `bones` (all actual mapped targets); no joint-name heuristic chooses a target. `feet.jumps` stays an array of original frame pairs; `feet.jumpEvents` adds matching `{ frames, times }` objects. Fold and uncovered rows preserve their discrete `times` and gaps, declare `domain: "render"`, and use the separate root `render` domain, never fabricated nearest pose-frame indices. The run hashes map/pose from the exact buffers passed to their readers, snapshots the resolved rig file before its gate, and rechecks all three before user-output writes. The writer validates coordinates before output writes and never changes motion keys. The switch diagnostic `moved.frame` is a selected-to-keyed pair with signed `frames`/`seconds` displacement; it is preserved as diagnostic data, not reinterpreted as an inclusive interval. An explicitly supplied uniform writer clock requires both positive `fps` and seconds offset `frame0`; canonical pose input does not yet declare FPS, so the retarget producer always writes exact `frameTimes`.
|
|
153
|
+
|
|
151
154
|
|
|
152
155
|
| field | each entry |
|
|
153
156
|
| --- | --- |
|
|
@@ -315,7 +318,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
315
318
|
⚠️ **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 (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 a second dance with knee bends (Toca Toca, 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.
|
|
316
319
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
317
320
|
- `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.
|
|
318
|
-
- `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)): `{ "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
|
|
321
|
+
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides` (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
|
|
319
322
|
- `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.
|
|
320
323
|
|
|
321
324
|
### A joint's range: `range`
|
|
@@ -352,8 +355,8 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
352
355
|
- `drawn` (optional): each bone's world angle (Spine degrees) as painted. It changes no key unless the set declares `symmetric`, `support` or `anchor` ([below](#alternate-drawings-alternates)): otherwise, while a set shows, a bone is keyed to the pose as always, and the art turns with it. What it gives is a figure: the art turn, how far each bone is from the angle its art was painted at (`artTurn`, and `ART-RANGE` past the bone's range).
|
|
353
356
|
- `offsets` (optional): a translate per bone, in its parent's axes, held while the set shows and 0 elsewhere (stepped keys), so the joints as painted are the pivots. A bone `translate` already keys (the hip) cannot take one.
|
|
354
357
|
- `place` (optional): `"by-hip"` places a hand that meets the body by its offset from the hip ([below](#alternate-drawings-alternates)).
|
|
355
|
-
- `symmetric` (optional, [#188](https://github.com/rig-c/pose/issues/188)): `true` normalises a two-arm symmetric gesture set (such as hands behind the head) to its `drawn` symmetry centre so both elbows sit at the same world height and neither wrist extends past the head silhouette ([below](#alternate-drawings-alternates)). Requires two 2-bone chains with `drawn` angles on all four bones (`MAP_ALT_SYMMETRIC`).
|
|
356
|
-
- `support` (optional, [#188](https://github.com/rig-c/pose/issues/188)): a chain name or non-empty list of chain names of the set (`"support": "arm_l"` or `["arm_l"]`) whose bones carry a stationary support arm (such as a waist-resting hand while the other arm gestures); while the set
|
|
358
|
+
- `symmetric` (optional, [#188](https://github.com/rig-c/pose/issues/188), [#206](https://github.com/rig-c/pose/issues/206)): `true` normalises a two-arm symmetric gesture set (such as hands behind the head) to its `drawn` symmetry centre so both elbows sit at the same world height and neither wrist extends past the head silhouette ([below](#alternate-drawings-alternates)), ramping in and out in joint space across any adjacent bridge frames where the set's slots remain shown. Requires two 2-bone chains with `drawn` angles on all four bones (`MAP_ALT_SYMMETRIC`).
|
|
359
|
+
- `support` (optional, [#188](https://github.com/rig-c/pose/issues/188), [#206](https://github.com/rig-c/pose/issues/206)): a chain name or non-empty list of chain names of the set (`"support": "arm_l"` or `["arm_l"]`) whose bones carry a stationary support arm (such as a waist-resting hand while the other arm gestures); while the set's slots show (`loShow..hiShow`), each support chain's bones anchor to `drawn` within `±2.5°` (`≤ ±3°`) of the pose's delta relative to the set's reference pose (bridging in joint space across any preceding gap frames where the leaving set's slots still show), and an unplaced active raised arm in the same set clears the head/bun outward along the 2D shoulder→wrist direction (`MAP_ALT_SUPPORT`).
|
|
357
360
|
- `anchor` (optional, [#191](https://github.com/rig-c/pose/issues/191)): a chain name or non-empty list of chain names of the set (`"anchor": "arm_l"` or `["arm_l"]`) whose hand is anchored to the set's drawn hand-tip-to-head offset in the `head` bone's local frame (`MAP_ALT_ANCHOR`, `ANCHOR_HEAD`); while the set's slots for that chain show, the target hand tip is `head_posed_origin + rotate(W_drawn − H_setup, head_posed_rot − head_drawn_rot)` and the 2-bone arm is solved by two-circle intersection (`twoBone` / `reachLimit`) inside the set's `turn`, listing any frame where `turn` saturates and leaves a residual hand-tip shortfall (`saturated: [{ frame, residual }]` on `raised`, printed as `; residual f46 9.68u, …` on `RAISED`).
|
|
358
361
|
- Each name resolves against the built rig and the map, and a miss is refused by name, listing what exists: `MAP_ALT_SLOT`, `MAP_ALT_ATTACHMENT`, `MAP_ALT_HIDES`, `MAP_ALT_CHAIN`, `MAP_ALT_JOINT`, `MAP_ALT_POSE`, `MAP_ALT_DRAWN`, `MAP_ALT_OFFSET` (a bone that is not a bone of the set's chains), `MAP_ALT_PLACE`, `MAP_ALT_SYMMETRIC`, `MAP_ALT_SUPPORT` and `MAP_ALT_ANCHOR`.
|
|
359
362
|
|
|
@@ -365,7 +368,7 @@ The bound exists because speed alone rewarded showing art far from its pose. Unb
|
|
|
365
368
|
|
|
366
369
|
Either way the `SWITCH` line ends with `moved on …, off …`: each boundary's old and new frame (the same under `first`), the frames and seconds it moved, the speed on both and its window, and the report carries the same under `moved`.
|
|
367
370
|
|
|
368
|
-
**What is written.** An `attachment` track per slot whose picture changes: the set's attachment (or `null` for a hidden slot) where a span starts, and the slot's setup attachment, as the compiled model states it, where it ends. A direct switch from one set to another puts the first set's slots back to setup on the second's first frame, so an extra slot that is empty at setup goes empty. Between two sets that both draw an arm behind the torso, a frame that belongs to neither set stays behind (the set nearer in hand position and segment distance, not frame count)
|
|
371
|
+
**What is written.** An `attachment` track per slot whose picture changes: the set's attachment (or `null` for a hidden slot) where a span starts, and the slot's setup attachment, as the compiled model states it, where it ends. A direct switch from one set to another puts the first set's slots back to setup on the second's first frame, so an extra slot that is empty at setup goes empty. Between two sets that both draw an arm behind the torso, a frame that belongs to neither set stays behind (the set nearer in hand position and segment distance, not frame count), with any adjacent bridge or `SHORT` ramp interpolated in joint space (parent-local angle for child bones) and any extended hold of a tight set included in `chainHolds`/`covered` so the held art is clamped and exempted from `capShortSteps` and `range`; the default front slot is for a frame no set can claim **and** whose arm is read in front — not a fallback ([#136](https://github.com/rig-c/pose/issues/136), [#139](https://github.com/rig-c/pose/issues/139), [#162](https://github.com/rig-c/pose/issues/162), [#180](https://github.com/rig-c/pose/issues/180), [#187](https://github.com/rig-c/pose/issues/187), [#206](https://github.com/rig-c/pose/issues/206)). A switch onto frame *i* is keyed halfway between frames *i*−1 and *i*: a stepped key on the frame's own time fires a frame late wherever a player's accumulated time lands just below it (rig-c's docs/AUTHORING.md §4.5), and halfway both the pose's times and a renderer's `i / fps` see it on frame *i*. Before the first key a slot shows its setup attachment, so no key is written at 0 unless a span starts there. Offsets are `translate` keys at the same times, `stepped`. rig-c's gate checks these keys like any other.
|
|
369
372
|
|
|
370
373
|
**TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on the 13 s front-facing dance their picture length never drops under 0.81 of its median and none reverses; on a second sequence the hip line falls to 0.17 at frames 465–491 (side-on) and the eye line to 0.22 at 26–58 (head in profile), and 3 frames reverse. 0.5 lies between. A line is reversed when it points more than 90° from the subject's right joint to the left (front-facing, it points at 0°). The lines are read after holds, held as the map's points are even where the map does not use them; a line whose points are good on no frame is named as not measured.
|
|
371
374
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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/report.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Add report coordinates without touching the retarget or any source value. */
|
|
2
|
+
import type { JointMap } from './map.ts';
|
|
3
|
+
import { isObject, Problems, Refusal } from './problems.ts';
|
|
4
|
+
|
|
5
|
+
export type ReportClock={frameTimes:readonly number[]}|{fps:number;frame0:number;frames:number};
|
|
6
|
+
export interface RenderDomain {fps:number;frames:number}
|
|
7
|
+
export interface ReportInput {path:string;sha256:string}
|
|
8
|
+
export interface ReportInputs {rig:ReportInput;map:ReportInput;pose:ReportInput}
|
|
9
|
+
function fail(field:string,text:string):never {throw new Refusal([{code:'REPORT_TIMEBASE',text:`${field}: ${text}`}]);}
|
|
10
|
+
function finite(value:unknown,field:string):number {if(typeof value!=='number'||!Number.isFinite(value)||value<0)return fail(field,`found ${String(value)}; expected finite seconds >= 0`);return value;}
|
|
11
|
+
function integer(value:unknown,field:string):number {const n=finite(value,field);if(!Number.isSafeInteger(n))fail(field,`found ${n}; expected nonnegative safe integer`);return n;}
|
|
12
|
+
export function reportFrameTimes(clock:ReportClock):number[] {
|
|
13
|
+
if('frameTimes' in clock){
|
|
14
|
+
if(!Array.isArray(clock.frameTimes))fail('frameTimes','expected an explicit array');
|
|
15
|
+
const values=clock.frameTimes.map((time,i)=>finite(time,`frameTimes[${i}]`));
|
|
16
|
+
if(!values.length)fail('frameTimes','expected at least one explicit frame');
|
|
17
|
+
for(let i=1;i<values.length;i++)if(values[i]!<=values[i-1]!)fail(`frameTimes[${i}]`,`found ${values[i]}; must follow ${values[i-1]}`);return values;
|
|
18
|
+
}
|
|
19
|
+
const fps=finite(clock.fps,'fps');if(fps===0)fail('fps','expected positive explicit rate');const frame0=finite(clock.frame0,'frame0'),count=integer(clock.frames,'frames');
|
|
20
|
+
if(count===0)fail('frames','expected at least one frame');const times=Array.from({length:count},(_,i)=>frame0+i/fps);
|
|
21
|
+
for(let i=0;i<times.length;i++){finite(times[i],`frame ${i}`);if(i>0&×[i]!<=times[i-1]!)fail(`frame ${i}`,'uniform clock cannot represent distinct frame samples');}return times;
|
|
22
|
+
}
|
|
23
|
+
export function reportRenderDomain(input:unknown,animation:string):RenderDomain {
|
|
24
|
+
if(!isObject(input)||!Array.isArray(input.sets))fail('render.sets','expected renderer frame-set metadata');
|
|
25
|
+
const set=input.sets.find((value:unknown)=>isObject(value)&&value.animation===animation);
|
|
26
|
+
if(!isObject(set))fail('render.animation',`missing ${JSON.stringify(animation)} frame-set metadata`);
|
|
27
|
+
const fps=finite(set.fps,'render.fps'),frames=integer(set.sampled,'render.frames');if(fps===0||frames===0)fail('render','expected positive declared fps and sampled frame count');return {fps,frames};
|
|
28
|
+
}
|
|
29
|
+
function clone(value:unknown):unknown {if(Array.isArray(value))return value.map(clone);if(isObject(value))return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,clone(item)]));return value;}
|
|
30
|
+
|
|
31
|
+
export function enrichReport(input:Record<string,unknown>,map:JointMap,clock:ReportClock,render:RenderDomain|null=null,inputs?:ReportInputs):Record<string,unknown> {
|
|
32
|
+
const times=reportFrameTimes(clock),report=clone(input) as Record<string,unknown>;
|
|
33
|
+
if(render!==null){const fps=finite(render.fps,'render.fps'),frames=integer(render.frames,'render.frames');if(fps===0||frames===0)fail('render','expected positive declared fps and sampled frame count');}
|
|
34
|
+
if(report.frames!==times.length)fail('frames',`report has ${String(report.frames)}, clock has ${times.length}`);
|
|
35
|
+
const problems=new Problems();
|
|
36
|
+
const time=(frame:unknown,where:string):number=>{const n=integer(frame,where);if(n>=times.length)fail(where,`frame ${n} outside ${times.length} declared frames`);return times[n]!;};
|
|
37
|
+
const add=(row:Record<string,unknown>,key:string,value:unknown,where:string)=>{
|
|
38
|
+
if(row[key]!==undefined&&JSON.stringify(row[key])!==JSON.stringify(value))problems.add('REPORT_COORDINATE',`${where}.${key}: supplied ${JSON.stringify(row[key])}, expected ${JSON.stringify(value)}`);
|
|
39
|
+
else row[key]=value;
|
|
40
|
+
};
|
|
41
|
+
// A joint can influence several mapped segments. Preserve all actual matches;
|
|
42
|
+
// only a unique match may occupy the singular bone field.
|
|
43
|
+
const bonesFor=(joint:string)=>{const bones=new Set<string>();if(map.translate?.joint===joint)bones.add(map.translate.bone);for(const chain of map.chains)for(let i=0;i<chain.bones.length;i++)if(chain.joints[i]===joint||chain.joints[i+1]===joint)bones.add(chain.bones[i]!);return [...bones];};
|
|
44
|
+
const pair=(value:unknown,where:string):[number,number]=>{if(!Array.isArray(value)||value.length!==2)fail(where,'expected inclusive [first,last] frame pair');const first=integer(value[0],where+'[0]'),last=integer(value[1],where+'[1]');if(last<first)fail(where,`found [${first},${last}]; inclusive last must be >= first`);return [first,last];};
|
|
45
|
+
const poseEvent=(where:string)=>/^report\.(?:holds|short|endOn|bends|flips|turns|range|artRange|raised|byHip|landings|shear|switches|cleaned\.(?:holds|seams|spikes\.found)|feet\.(?:supports|changes|steps|lowered|tracked|shifts)|stance\.(?:runs|over))\[\d+\]$/.test(where);
|
|
46
|
+
const targets=(row:Record<string,unknown>,where:string)=>{
|
|
47
|
+
if(row.bone!==undefined||row.slot!==undefined)return;
|
|
48
|
+
const joints=typeof row.joint==='string'?[row.joint]:typeof row.point==='string'?[row.point]:Array.isArray(row.joints)?row.joints.filter((v):v is string=>typeof v==='string'):[];
|
|
49
|
+
const chain=typeof row.chain==='string'?map.chains.find(c=>c.name===row.chain):undefined;
|
|
50
|
+
const bones=[...new Set(chain?chain.bones:joints.flatMap(bonesFor))];
|
|
51
|
+
if(bones.length===1)add(row,'bone',bones[0],where);else if(bones.length>1)add(row,'bones',bones,where);
|
|
52
|
+
// Multiple affected targets remain explicit lists, never a guessed winner.
|
|
53
|
+
if(typeof row.point==='string')add(row,'joint',row.point,where);
|
|
54
|
+
};
|
|
55
|
+
const visit=(value:unknown,where:string):void=>{
|
|
56
|
+
if(Array.isArray(value)){value.forEach((row,i)=>visit(row,`${where}[${i}]`));return;}
|
|
57
|
+
if(!isObject(value))return;
|
|
58
|
+
// Preserve uncovered's discrete renderer times; they do not index the pose.
|
|
59
|
+
if(where.startsWith('report.uncovered[')||where.startsWith('report.folds[')){
|
|
60
|
+
if(!render)fail(where,'render-domain event requires declared renderer metadata');
|
|
61
|
+
add(value,'domain','render',where);return;
|
|
62
|
+
}
|
|
63
|
+
let coordinates=false;
|
|
64
|
+
if(poseEvent(where)&&value.frame!==undefined&&typeof value.frame!=='number')fail(where+'.frame','expected nonnegative integer pose frame');
|
|
65
|
+
if(typeof value.frame==='number'){add(value,'t',time(value.frame,where+'.frame'),where);coordinates=true;}
|
|
66
|
+
if((poseEvent(where)&&value.frames!==undefined)||(Array.isArray(value.frames)&&value.frames.length===2&&value.frames.every(frame=>typeof frame==='number'))){const frames=pair(value.frames,where+'.frames');add(value,'times',frames.map((frame,i)=>time(frame,`${where}.frames[${i}]`)),where);coordinates=true;}
|
|
67
|
+
if(where.startsWith('report.holds[')&&typeof value.from==='number'&&typeof value.to==='number'){
|
|
68
|
+
pair([value.from,value.to],where+'.from/to');add(value,'frames',[value.from,value.to],where);add(value,'times',[time(value.from,where+'.from'),time(value.to,where+'.to')],where);coordinates=true;
|
|
69
|
+
}
|
|
70
|
+
if(coordinates){if(typeof value.seconds==='number')add(value,'spanSeconds',value.seconds,where);targets(value,where);}
|
|
71
|
+
// Recurse over the original fields only; added time/target arrays are data.
|
|
72
|
+
for(const [key,item] of Object.entries(value))if(!['t','times','bones','spanSeconds'].includes(key))visit(item,where+'.'+key);
|
|
73
|
+
};
|
|
74
|
+
visit(report,'report');
|
|
75
|
+
if(isObject(report.feet)&&Array.isArray(report.feet.jumps)){
|
|
76
|
+
add(report.feet,'jumpEvents',report.feet.jumps.map((frames:unknown,index:number)=>{
|
|
77
|
+
const interval=pair(frames,`feet.jumps[${index}]`);
|
|
78
|
+
return {frames:[...interval],times:interval.map((frame,i)=>time(frame,`feet.jumps[${index}][${i}]`))};
|
|
79
|
+
}),'report.feet');
|
|
80
|
+
}
|
|
81
|
+
// The actual producer uses frameTimes. Uniform clocks exist only when a
|
|
82
|
+
// caller explicitly supplies fps/frame0, never inferred from samples.
|
|
83
|
+
if('frameTimes' in clock)add(report,'frameTimes',[...times],'report');else{add(report,'fps',clock.fps,'report');add(report,'frame0',clock.frame0,'report');}
|
|
84
|
+
add(report,'render',render,'report');
|
|
85
|
+
if(inputs!==undefined){if(!isObject(inputs))throw new Refusal([{code:'REPORT_INPUTS',text:'inputs: expected rig, map and pose file identities'}]);for(const key of ['rig','map','pose'] as const){const entry=inputs[key];if(!isObject(entry)||typeof entry.path!=='string'||!entry.path||entry.path.startsWith('/')||entry.path.charCodeAt(0)===92||/^[A-Za-z]:/.test(entry.path)||typeof entry.sha256!=='string'||! /^[a-f0-9]{64}$/.test(entry.sha256))throw new Refusal([{code:'REPORT_INPUTS',text:`inputs.${key}: expected invocation-relative consumed file path and exact SHA-256`}]);}add(report,'inputs',clone(inputs),'report');}
|
|
86
|
+
problems.throwIfAny();return report;
|
|
87
|
+
}
|
|
88
|
+
export function writeReport(input:Record<string,unknown>,map:JointMap,clock:ReportClock,render:RenderDomain|null,inputs:ReportInputs):string {if(inputs===undefined)throw new Refusal([{code:'REPORT_INPUTS',text:'inputs: expected rig, map and pose file identities'}]);return JSON.stringify(enrichReport(input,map,clock,render,inputs),null,2)+'\n';}
|
package/src/retarget.ts
CHANGED
|
@@ -99,18 +99,20 @@ const DEG = 180 / Math.PI;
|
|
|
99
99
|
export const HELD_MISS = 0.01;
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Beat-nod phase-coupling gain (`°/unit`) from the
|
|
103
|
-
* `±2%` neck
|
|
104
|
-
*
|
|
105
|
-
* dancer reference hand pass (`hand/p1`
|
|
102
|
+
* Beat-nod phase-coupling gain (`°/unit`) from the clamped `head.translate.y`
|
|
103
|
+
* offset (`clampedY`, bounded by `±2%` of neck length) onto `map.face.pitch`
|
|
104
|
+
* (`pitch_dial.rotate`, `rig-c/pose#190`, `#205`) when the neck-length clamp
|
|
105
|
+
* acts on the sequence. Measured on the dancer reference hand pass (`hand/p1`
|
|
106
|
+
* HP2: `+2.8°/unit`).
|
|
106
107
|
*/
|
|
107
108
|
export const NOD_RESIDUAL_PITCH_GAIN = 2.8;
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
|
-
* Beat-nod phase-coupling gain (`°/unit`) from the
|
|
111
|
-
* `±2%` neck
|
|
112
|
-
*
|
|
113
|
-
*
|
|
111
|
+
* Beat-nod phase-coupling gain (`°/unit`) from the clamped `head.translate.y`
|
|
112
|
+
* offset (`clampedY`, bounded by `±2%` of neck length) onto the head chain
|
|
113
|
+
* bone's `rotate` track (`head.rotate`, `rig-c/pose#190`, `#205`) when the
|
|
114
|
+
* neck-length clamp acts on the sequence. Measured on the dancer reference
|
|
115
|
+
* hand pass (`hand/p1` HP2: `+1.6°/unit`).
|
|
114
116
|
*/
|
|
115
117
|
export const NOD_RESIDUAL_HEAD_GAIN = 1.6;
|
|
116
118
|
|
|
@@ -1396,14 +1398,14 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1396
1398
|
}
|
|
1397
1399
|
}
|
|
1398
1400
|
let rotDeg = targets.get(hb)?.[i] ?? restOf.get(hb) ?? 0;
|
|
1399
|
-
if (headNodReading !== null && headNodReading.bone === hb) {
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1401
|
+
if (headNodReading !== null && headNodReading.bone === hb && headNodReading.residual.some((r) => r !== 0)) {
|
|
1402
|
+
const nodY = headNodReading.clampedY[i] ?? 0;
|
|
1403
|
+
if (nodY !== 0) {
|
|
1402
1404
|
const above = mappedAbove(hb);
|
|
1403
1405
|
const hc = map.chains.find((c) => c.bones.includes(hb));
|
|
1404
1406
|
const limits = hc?.range[hb];
|
|
1405
1407
|
const cov = hc !== undefined ? covered.get(hc.name) : undefined;
|
|
1406
|
-
let next = wrap(rotDeg + NOD_RESIDUAL_HEAD_GAIN *
|
|
1408
|
+
let next = wrap(rotDeg + NOD_RESIDUAL_HEAD_GAIN * nodY);
|
|
1407
1409
|
if (limits !== undefined && cov?.[i] !== true) {
|
|
1408
1410
|
const key = wrap((next - restOf.get(hb)!) - (above === null ? 0 : turn(above, i)));
|
|
1409
1411
|
if (overflow(key, limits) > RANGE_SLACK) {
|
|
@@ -1580,9 +1582,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1580
1582
|
}
|
|
1581
1583
|
};
|
|
1582
1584
|
/**
|
|
1583
|
-
* #188: For a `set.symmetric === true` gesture set,
|
|
1584
|
-
* on frame `i`
|
|
1585
|
-
*
|
|
1585
|
+
* #188 / #207: For a `set.symmetric === true` gesture set, solve its two two-bone arm chains
|
|
1586
|
+
* on frame `i` in the chest-local frame so the diamond is symmetric about the torso axis and
|
|
1587
|
+
* torso lean carries the whole diamond without collapsing one arm into the ±20° turn clamp.
|
|
1586
1588
|
*/
|
|
1587
1589
|
const solveSymmetricFrame = (set: (typeof map.alternates)[number], i: number): { cR: Chain; cL: Chain; wR: Pt; wL: Pt; hc: Pt; ed: number } | null => {
|
|
1588
1590
|
if (set.chains.length !== 2) return null;
|
|
@@ -1624,6 +1626,21 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1624
1626
|
ed = d;
|
|
1625
1627
|
}
|
|
1626
1628
|
}
|
|
1629
|
+
let hc0: Pt = [(SR0[0] + SL0[0]) / 2, (SR0[1] + SL0[1]) / 2 + 0.85 * Math.hypot(SL0[0] - SR0[0], SL0[1] - SR0[1])];
|
|
1630
|
+
let ed0 = Math.max(1e-3, 0.30 * Math.hypot(SL0[0] - SR0[0], SL0[1] - SR0[1]));
|
|
1631
|
+
if (gestureHead !== null) {
|
|
1632
|
+
const ha0 = rigPoint(rest, gestureHead.points[0]);
|
|
1633
|
+
const hb0 = rigPoint(rest, gestureHead.points[1]);
|
|
1634
|
+
if (ha0 !== null && hb0 !== null) {
|
|
1635
|
+
const d0 = Math.hypot(hb0[0] - ha0[0], hb0[1] - ha0[1]);
|
|
1636
|
+
if (d0 > 1e-6) {
|
|
1637
|
+
hc0 = [(ha0[0] + hb0[0]) / 2, (ha0[1] + hb0[1]) / 2];
|
|
1638
|
+
ed0 = d0;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const above0 = mappedAbove(bR0);
|
|
1643
|
+
const chestTurn = above0 === null ? 0 : turn(above0, i);
|
|
1627
1644
|
const drawnR0 = set.drawn[bR0]!;
|
|
1628
1645
|
const drawnR1 = set.drawn[bR1]!;
|
|
1629
1646
|
const drawnL0 = set.drawn[bL0]!;
|
|
@@ -1632,19 +1649,39 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1632
1649
|
const turnR1 = set.turn[bR1] ?? [-20, 20];
|
|
1633
1650
|
const turnL0 = set.turn[bL0] ?? [-20, 20];
|
|
1634
1651
|
const turnL1 = set.turn[bL1] ?? [-20, 20];
|
|
1635
|
-
const
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
let
|
|
1640
|
-
|
|
1652
|
+
const erDrawnX = SR0[0] + lR0 * Math.cos((drawnR0 + dR0) / DEG);
|
|
1653
|
+
const elDrawnX = SL0[0] + lL0 * Math.cos((drawnL0 + dL0) / DEG);
|
|
1654
|
+
let aR0_c: number;
|
|
1655
|
+
let aL0_c: number;
|
|
1656
|
+
let aR1_c: number;
|
|
1657
|
+
let aL1_c: number;
|
|
1658
|
+
if (Math.abs(wrap(drawnR0 + drawnL0 - 180)) > 1 && (hc0[0] - erDrawnX > lR1 || elDrawnX - hc0[0] > lL1)) {
|
|
1659
|
+
const eyTarget = hc0[1] - 0.30 * ed0;
|
|
1660
|
+
const sR = Math.max(-1, Math.min(1, (eyTarget - SR0[1]) / Math.max(1e-6, lR0)));
|
|
1661
|
+
const sL = Math.max(-1, Math.min(1, (eyTarget - SL0[1]) / Math.max(1e-6, lL0)));
|
|
1662
|
+
const wantR0 = wrap(180 - Math.asin(sR) * DEG - dR0);
|
|
1663
|
+
const wantL0 = wrap(Math.asin(sL) * DEG - dL0);
|
|
1664
|
+
const offR0_c = Math.min(Math.max(wrap(wantR0 - drawnR0), turnR0[0]), turnR0[1]);
|
|
1665
|
+
const offL0_c = Math.min(Math.max(wrap(wantL0 - drawnL0), turnL0[0]), turnL0[1]);
|
|
1666
|
+
aR0_c = wrap(drawnR0 + offR0_c);
|
|
1667
|
+
aL0_c = wrap(drawnL0 + offL0_c);
|
|
1668
|
+
const offR1_c = Math.min(Math.max(-0.35 * offR0_c, turnR1[0]), turnR1[1]);
|
|
1669
|
+
const offL1_c = Math.min(Math.max(-0.35 * offL0_c, turnL1[0]), turnL1[1]);
|
|
1670
|
+
aR1_c = wrap(drawnR1 + offR1_c);
|
|
1671
|
+
aL1_c = wrap(drawnL1 + offL1_c);
|
|
1672
|
+
} else {
|
|
1673
|
+
const rawR0 = wrap((rawTargets.get(bR0)?.[i] ?? drawnR0) - chestTurn);
|
|
1674
|
+
const rawL0 = wrap((rawTargets.get(bL0)?.[i] ?? drawnL0) - chestTurn);
|
|
1675
|
+
const offR0 = wrap(rawR0 - drawnR0);
|
|
1676
|
+
const offL0 = wrap(rawL0 - drawnL0);
|
|
1677
|
+
const cOpen = Math.max(-4, Math.min(2, 0.15 * 0.5 * (offR0 - offL0)));
|
|
1641
1678
|
const evalDiff = (deg: number) => {
|
|
1642
1679
|
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1643
1680
|
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1644
|
-
const ar = drawnR0 + Math.min(Math.max(
|
|
1645
|
-
const al = drawnL0 + Math.min(Math.max(-
|
|
1646
|
-
const eyr =
|
|
1647
|
-
const eyl =
|
|
1681
|
+
const ar = drawnR0 + Math.min(Math.max(cOpen + dR, turnR0[0]), turnR0[1]);
|
|
1682
|
+
const al = drawnL0 + Math.min(Math.max(-cOpen + dL, turnL0[0]), turnL0[1]);
|
|
1683
|
+
const eyr = SR0[1] + lR0 * Math.sin((ar + dR0) / DEG);
|
|
1684
|
+
const eyl = SL0[1] + lL0 * Math.sin((al + dL0) / DEG);
|
|
1648
1685
|
return eyl - eyr;
|
|
1649
1686
|
};
|
|
1650
1687
|
let lo = -45;
|
|
@@ -1669,68 +1706,73 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1669
1706
|
}
|
|
1670
1707
|
const dR = deg < 0 ? 0.25 * deg : deg;
|
|
1671
1708
|
const dL = deg > 0 ? 0.25 * deg : deg;
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const eyR0 =
|
|
1675
|
-
const eyL0 =
|
|
1709
|
+
aR0_c = drawnR0 + Math.min(Math.max(cOpen + dR, turnR0[0]), turnR0[1]);
|
|
1710
|
+
aL0_c = drawnL0 + Math.min(Math.max(-cOpen + dL, turnL0[0]), turnL0[1]);
|
|
1711
|
+
const eyR0 = SR0[1] + lR0 * Math.sin((aR0_c + dR0) / DEG);
|
|
1712
|
+
const eyL0 = SL0[1] + lL0 * Math.sin((aL0_c + dL0) / DEG);
|
|
1676
1713
|
if (Math.abs(eyL0 - eyR0) > 1e-4) {
|
|
1677
|
-
const sL = Math.max(-1, Math.min(1, (eyR0 -
|
|
1714
|
+
const sL = Math.max(-1, Math.min(1, (eyR0 - SL0[1]) / Math.max(1e-6, lL0)));
|
|
1678
1715
|
const candL = wrap(Math.asin(sL) * DEG - dL0);
|
|
1679
1716
|
const tL = wrap(candL - drawnL0);
|
|
1680
|
-
if (tL >= turnL0[0] && tL <= turnL0[1])
|
|
1717
|
+
if (tL >= turnL0[0] && tL <= turnL0[1]) aL0_c = candL;
|
|
1681
1718
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
if (err < bestErr) {
|
|
1718
|
-
bestErr = err;
|
|
1719
|
-
bestA = candA;
|
|
1719
|
+
aR0_c = wrap(aR0_c);
|
|
1720
|
+
aL0_c = wrap(aL0_c);
|
|
1721
|
+
const rR0_c = (aR0_c + dR0) / DEG;
|
|
1722
|
+
const rL0_c = (aL0_c + dL0) / DEG;
|
|
1723
|
+
const exR_c = SR0[0] + lR0 * Math.cos(rR0_c);
|
|
1724
|
+
const eyR_c = SR0[1] + lR0 * Math.sin(rR0_c);
|
|
1725
|
+
const exL_c = SL0[0] + lL0 * Math.cos(rL0_c);
|
|
1726
|
+
const eyL_c = SL0[1] + lL0 * Math.sin(rL0_c);
|
|
1727
|
+
const aimFore0 = (ex: number, ey: number, l1: number, d1: number, drawn1: number, turn1: readonly [number, number], tx: number, ty: number): number => {
|
|
1728
|
+
const want = wrap(Math.atan2(ty - ey, tx - ex) * DEG - d1);
|
|
1729
|
+
const off = Math.min(Math.max(wrap(want - drawn1), turn1[0]), turn1[1]);
|
|
1730
|
+
let a1 = wrap(drawn1 + off);
|
|
1731
|
+
const r1 = (a1 + d1) / DEG;
|
|
1732
|
+
const wx = ex + l1 * Math.cos(r1);
|
|
1733
|
+
const wy = ey + l1 * Math.sin(r1);
|
|
1734
|
+
const xMin = hc0[0] - 0.30 * ed0;
|
|
1735
|
+
const xMax = hc0[0] + 0.30 * ed0;
|
|
1736
|
+
const yMin = hc0[1] + 0.15 * ed0;
|
|
1737
|
+
const yMax = hc0[1] + 0.95 * ed0;
|
|
1738
|
+
if (wx < xMin || wx > xMax || wy < yMin || wy > yMax) {
|
|
1739
|
+
let bestA = a1;
|
|
1740
|
+
let bestErr = Infinity;
|
|
1741
|
+
for (let step = -200; step <= 200; step++) {
|
|
1742
|
+
const candOff = turn1[0] + ((turn1[1] - turn1[0]) * (step + 200)) / 400;
|
|
1743
|
+
const candA = wrap(drawn1 + candOff);
|
|
1744
|
+
const cr = (candA + d1) / DEG;
|
|
1745
|
+
const cwx = ex + l1 * Math.cos(cr);
|
|
1746
|
+
const cwy = ey + l1 * Math.sin(cr);
|
|
1747
|
+
const ox = cwx < xMin ? xMin - cwx : cwx > xMax ? cwx - xMax : 0;
|
|
1748
|
+
const oy = cwy < yMin ? yMin - cwy : cwy > yMax ? cwy - yMax : 0;
|
|
1749
|
+
const err = (ox + oy) * 1000 + Math.hypot(cwx - tx, cwy - ty);
|
|
1750
|
+
if (err < bestErr) {
|
|
1751
|
+
bestErr = err;
|
|
1752
|
+
bestA = candA;
|
|
1753
|
+
}
|
|
1720
1754
|
}
|
|
1755
|
+
a1 = bestA;
|
|
1721
1756
|
}
|
|
1722
|
-
a1
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
const
|
|
1730
|
-
const
|
|
1731
|
-
|
|
1757
|
+
return a1;
|
|
1758
|
+
};
|
|
1759
|
+
aR1_c = aimFore0(exR_c, eyR_c, lR1, dR1, drawnR1, turnR1, hc0[0] - 0.20 * ed0, hc0[1] + 0.65 * ed0);
|
|
1760
|
+
aL1_c = aimFore0(exL_c, eyL_c, lL1, dL1, drawnL1, turnL1, hc0[0] + 0.20 * ed0, hc0[1] + 0.65 * ed0);
|
|
1761
|
+
}
|
|
1762
|
+
const carry = 0.35 * chestTurn;
|
|
1763
|
+
const aR0 = wrap(aR0_c + carry);
|
|
1764
|
+
const aR1 = wrap(aR1_c + carry);
|
|
1765
|
+
const aL0 = wrap(aL0_c + carry);
|
|
1766
|
+
const aL1 = wrap(aL1_c + carry);
|
|
1767
|
+
const rR0 = (aR0 + dR0) / DEG;
|
|
1768
|
+
const rR1 = (aR1 + dR1) / DEG;
|
|
1769
|
+
const rL0 = (aL0 + dL0) / DEG;
|
|
1770
|
+
const rL1 = (aL1 + dL1) / DEG;
|
|
1771
|
+
const wR: Pt = [sr[0] + lR0 * Math.cos(rR0) + lR1 * Math.cos(rR1), sr[1] + lR0 * Math.sin(rR0) + lR1 * Math.sin(rR1)];
|
|
1772
|
+
const wL: Pt = [sl[0] + lL0 * Math.cos(rL0) + lL1 * Math.cos(rL1), sl[1] + lL0 * Math.sin(rL0) + lL1 * Math.sin(rL1)];
|
|
1773
|
+
targets.get(bR0)![i] = aR0;
|
|
1732
1774
|
targets.get(bR1)![i] = aR1;
|
|
1733
|
-
targets.get(bL0)![i] =
|
|
1775
|
+
targets.get(bL0)![i] = aL0;
|
|
1734
1776
|
targets.get(bL1)![i] = aL1;
|
|
1735
1777
|
return { cR, cL, wR, wL, hc, ed };
|
|
1736
1778
|
};
|
|
@@ -2145,15 +2187,38 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2145
2187
|
const [lo, hi] = s.frames;
|
|
2146
2188
|
if (lo <= 0 || hi + 1 >= n) continue;
|
|
2147
2189
|
if (cov?.[lo - 1] !== true && cov?.[hi + 1] !== true) continue;
|
|
2190
|
+
if (cov?.[lo - 1] === true && cov?.[hi + 1] === true && hi - lo + 1 <= 10) continue;
|
|
2191
|
+
const ch = map.chains.find((c) => c.name === s.chain);
|
|
2192
|
+
const bIdx = ch !== undefined ? ch.bones.indexOf(s.bone) : -1;
|
|
2193
|
+
const parentTg = ch !== undefined && bIdx > 0 ? targets.get(ch.bones[bIdx - 1]!) : undefined;
|
|
2148
2194
|
const tg = targets.get(s.bone)!;
|
|
2149
|
-
const startA = tg[lo - 1]!;
|
|
2150
|
-
const
|
|
2195
|
+
const startA = parentTg !== undefined ? wrap(tg[lo - 1]! - parentTg[lo - 1]!) : tg[lo - 1]!;
|
|
2196
|
+
const endA = parentTg !== undefined ? wrap(tg[hi + 1]! - parentTg[hi + 1]!) : tg[hi + 1]!;
|
|
2197
|
+
const total = wrap(endA - startA);
|
|
2151
2198
|
const steps = hi - lo + 2;
|
|
2152
2199
|
for (let k = lo; k <= hi; k++) {
|
|
2153
|
-
|
|
2200
|
+
const localA = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
2201
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2154
2202
|
}
|
|
2155
2203
|
s.step = r3(total / steps);
|
|
2156
2204
|
}
|
|
2205
|
+
const interpolateChainInJointSpace = (ch: Chain, i0: number, i1: number) => {
|
|
2206
|
+
const steps = i1 - i0;
|
|
2207
|
+
if (steps < 2 || i0 < 0 || i1 >= n) return;
|
|
2208
|
+
for (let bIdx = 0; bIdx < ch.bones.length; bIdx++) {
|
|
2209
|
+
const b = ch.bones[bIdx]!;
|
|
2210
|
+
const tg = targets.get(b);
|
|
2211
|
+
if (!tg) continue;
|
|
2212
|
+
const parentTg = bIdx > 0 ? targets.get(ch.bones[bIdx - 1]!) : undefined;
|
|
2213
|
+
const startA = parentTg !== undefined ? wrap(tg[i0]! - parentTg[i0]!) : tg[i0]!;
|
|
2214
|
+
const endA = parentTg !== undefined ? wrap(tg[i1]! - parentTg[i1]!) : tg[i1]!;
|
|
2215
|
+
const total = wrap(endA - startA);
|
|
2216
|
+
for (let k = i0 + 1; k < i1; k++) {
|
|
2217
|
+
const localA = wrap(startA + ((k - i0) / steps) * total);
|
|
2218
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2157
2222
|
const holds = sw.chainHolds ?? [];
|
|
2158
2223
|
for (const h of holds) {
|
|
2159
2224
|
const swParent = sw.switches.find((s) => s.set === h.set && s.frames[0] === h.frames[0] && s.frames[1] > h.frames[1]);
|
|
@@ -2165,19 +2230,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2165
2230
|
if (lo > hi || lo <= 0 || hi + 1 >= n) continue;
|
|
2166
2231
|
const ch = map.chains.find((c) => c.name === h.chain);
|
|
2167
2232
|
if (!ch) continue;
|
|
2168
|
-
|
|
2169
|
-
for (const b of ch.bones) {
|
|
2170
|
-
const tg = targets.get(b);
|
|
2171
|
-
if (!tg) continue;
|
|
2172
|
-
const startA = tg[lo - 1]!;
|
|
2173
|
-
const total = wrap(tg[hi + 1]! - startA);
|
|
2174
|
-
for (let k = lo; k <= hi; k++) {
|
|
2175
|
-
tg[k] = wrap(startA + ((k - lo + 1) / steps) * total);
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2233
|
+
interpolateChainInJointSpace(ch, lo - 1, hi + 1);
|
|
2178
2234
|
}
|
|
2179
2235
|
}
|
|
2180
|
-
// #188 / #191: Cover the full shown/bridged span of symmetric sets, support-arm sets, and head-anchored chains after short interpolation.
|
|
2236
|
+
// #188 / #191 / #206: Cover the full shown/bridged span of symmetric sets, support-arm sets, and head-anchored chains after short interpolation.
|
|
2181
2237
|
for (let sIdx = 0; sIdx < sw.switches.length; sIdx++) {
|
|
2182
2238
|
const s = sw.switches[sIdx]!;
|
|
2183
2239
|
const set = map.alternates.find((a) => a.name === s.set)!;
|
|
@@ -2237,12 +2293,16 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2237
2293
|
for (const cn of set.chains) {
|
|
2238
2294
|
const c = map.chains.find((x) => x.name === cn);
|
|
2239
2295
|
if (!c) continue;
|
|
2240
|
-
for (
|
|
2296
|
+
for (let bIdx = 0; bIdx < c.bones.length; bIdx++) {
|
|
2297
|
+
const b = c.bones[bIdx]!;
|
|
2241
2298
|
const tg = targets.get(b)!;
|
|
2242
|
-
const
|
|
2243
|
-
const
|
|
2299
|
+
const parentTg = bIdx > 0 ? targets.get(c.bones[bIdx - 1]!) : undefined;
|
|
2300
|
+
const startA = parentTg !== undefined ? wrap(tg[loRamp]! - parentTg[loRamp]!) : tg[loRamp]!;
|
|
2301
|
+
const endA = parentTg !== undefined ? wrap(tg[loShow]! - parentTg[loShow]!) : tg[loShow]!;
|
|
2302
|
+
const total = wrap(endA - startA);
|
|
2244
2303
|
for (let k = loRamp + 1; k < loShow; k++) {
|
|
2245
|
-
|
|
2304
|
+
const localA = wrap(startA + ((k - loRamp) / steps) * total);
|
|
2305
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2246
2306
|
}
|
|
2247
2307
|
}
|
|
2248
2308
|
}
|
|
@@ -2257,23 +2317,43 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2257
2317
|
const [cR, cL] = S0_0[0] <= S0_1[0] ? [c0, c1] : [c1, c0];
|
|
2258
2318
|
const bR0 = cR.bones[0]!;
|
|
2259
2319
|
const bL0 = cL.bones[0]!;
|
|
2320
|
+
const bR1 = cR.bones[1]!;
|
|
2321
|
+
const bL1 = cL.bones[1]!;
|
|
2260
2322
|
const SR0 = rigPoint(rest, bR0)!;
|
|
2261
|
-
const ER0 = rigPoint(rest,
|
|
2323
|
+
const ER0 = rigPoint(rest, bR1)!;
|
|
2262
2324
|
const SL0 = rigPoint(rest, bL0)!;
|
|
2263
|
-
const EL0 = rigPoint(rest,
|
|
2325
|
+
const EL0 = rigPoint(rest, bL1)!;
|
|
2264
2326
|
const lR0 = Math.hypot(ER0[0] - SR0[0], ER0[1] - SR0[1]);
|
|
2265
2327
|
const lL0 = Math.hypot(EL0[0] - SL0[0], EL0[1] - SL0[1]);
|
|
2266
2328
|
const dR0 = wrap(Math.atan2(ER0[1] - SR0[1], ER0[0] - SR0[0]) * DEG - restOf.get(bR0)!);
|
|
2267
2329
|
const dL0 = wrap(Math.atan2(EL0[1] - SL0[1], EL0[0] - SL0[0]) * DEG - restOf.get(bL0)!);
|
|
2268
|
-
|
|
2330
|
+
const iEnd = s.frames[1];
|
|
2331
|
+
const iNext = Math.min(n - 1, hiShow + 1);
|
|
2332
|
+
const spanOut = Math.max(1, iNext - iEnd);
|
|
2333
|
+
const startR0 = targets.get(bR0)![iEnd]!;
|
|
2334
|
+
const startL0 = targets.get(bL0)![iEnd]!;
|
|
2335
|
+
const drawnR0 = set.drawn[bR0] ?? startR0;
|
|
2336
|
+
const drawnL0 = set.drawn[bL0] ?? startL0;
|
|
2337
|
+
const drawnR1 = set.drawn[bR1] ?? targets.get(bR1)![iEnd]!;
|
|
2338
|
+
const drawnL1 = set.drawn[bL1] ?? targets.get(bL1)![iEnd]!;
|
|
2339
|
+
const inR1 = wrap(drawnR1 + (set.turn[bR1]?.[0] ?? 0));
|
|
2340
|
+
const inL1 = wrap(drawnL1 + 0.6 * (set.turn[bL1]?.[1] ?? 0));
|
|
2341
|
+
const endR0 = targets.get(bR0)![iNext]!;
|
|
2342
|
+
const endL0 = targets.get(bL0)![iNext]!;
|
|
2343
|
+
for (let i = iEnd + 1; i <= hiShow; i++) {
|
|
2344
|
+
const u = (i - iEnd) / spanOut;
|
|
2345
|
+
const w = 1 - u;
|
|
2346
|
+
const alpha = 0.25 * u + 0.75 * u * u;
|
|
2269
2347
|
const sr = posedOrigin(bR0, i);
|
|
2270
2348
|
const sl = posedOrigin(bL0, i);
|
|
2271
|
-
const baseR =
|
|
2272
|
-
const baseL =
|
|
2273
|
-
let lo = -
|
|
2274
|
-
let hi =
|
|
2349
|
+
const baseR = wrap(startR0 + alpha * wrap(endR0 - startR0));
|
|
2350
|
+
const baseL = wrap(startL0 + alpha * wrap(endL0 - startL0));
|
|
2351
|
+
let lo = -30;
|
|
2352
|
+
let hi = 30;
|
|
2353
|
+
const dR = (deg: number) => (deg < 0 ? 0.25 * deg : deg);
|
|
2354
|
+
const dL = (deg: number) => (deg > 0 ? 0.25 * deg : deg);
|
|
2275
2355
|
const evalD = (deg: number) =>
|
|
2276
|
-
sl[1] + lL0 * Math.sin((baseL + deg + dL0) / DEG) -
|
|
2356
|
+
0.4 * w * (sl[1] - sr[1]) + lL0 * Math.sin((baseL + dL(deg) + dL0) / DEG) - lR0 * Math.sin((baseR + dR(deg) + dR0) / DEG);
|
|
2277
2357
|
let fLo = evalD(lo);
|
|
2278
2358
|
for (let it = 0; it < 32; it++) {
|
|
2279
2359
|
const mid = 0.5 * (lo + hi);
|
|
@@ -2284,24 +2364,50 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2284
2364
|
} else hi = mid;
|
|
2285
2365
|
}
|
|
2286
2366
|
const deg = 0.5 * (lo + hi);
|
|
2287
|
-
|
|
2288
|
-
|
|
2367
|
+
const aR0 = wrap(baseR + dR(deg));
|
|
2368
|
+
const aL0 = wrap(baseL + dL(deg));
|
|
2369
|
+
targets.get(bR0)![i] = aR0;
|
|
2370
|
+
targets.get(bL0)![i] = aL0;
|
|
2371
|
+
targets.get(bR1)![i] = wrap(inR1 + 0.45 * wrap(aR0 - drawnR0));
|
|
2372
|
+
targets.get(bL1)![i] = wrap(inL1 + 0.45 * wrap(aL0 - drawnL0));
|
|
2289
2373
|
}
|
|
2290
2374
|
}
|
|
2291
2375
|
}
|
|
2292
2376
|
}
|
|
2377
|
+
if (set.anchor === undefined && set.symmetric !== true && set.support === undefined && loShow < s.frames[0]) {
|
|
2378
|
+
for (const cn of s.chains) {
|
|
2379
|
+
const c = map.chains.find((x) => x.name === cn);
|
|
2380
|
+
if (!c || c.bones.length < 2) continue;
|
|
2381
|
+
const b1 = c.bones[1]!;
|
|
2382
|
+
const t1 = targets.get(b1)!;
|
|
2383
|
+
const entryWorld1 = t1[s.frames[0]]!;
|
|
2384
|
+
for (let i = loShow; i < s.frames[0]; i++) {
|
|
2385
|
+
t1[i] = entryWorld1;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2293
2389
|
if (set.support !== undefined) {
|
|
2294
2390
|
const prevSw = sIdx > 0 ? sw.switches[sIdx - 1] : undefined;
|
|
2295
2391
|
const loGap = prevSw !== undefined && s.frames[0] - prevSw.frames[1] <= 10 ? prevSw.frames[1] + 1 : loShow;
|
|
2296
2392
|
for (const cn of s.chains) {
|
|
2297
2393
|
const c = map.chains.find((x) => x.name === cn)!;
|
|
2298
2394
|
if (set.support.includes(c.name)) {
|
|
2299
|
-
for (let i =
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2395
|
+
for (let i = loShow; i <= hiShow; i++) solveSupportChainFrame(set, c, i);
|
|
2396
|
+
if (loGap < loShow && loGap >= 1) {
|
|
2397
|
+
const i0 = loGap - 1;
|
|
2398
|
+
const i1 = loShow;
|
|
2399
|
+
const steps = i1 - i0;
|
|
2400
|
+
for (let bIdx = 0; bIdx < c.bones.length; bIdx++) {
|
|
2401
|
+
const b = c.bones[bIdx]!;
|
|
2303
2402
|
const tg = targets.get(b)!;
|
|
2304
|
-
|
|
2403
|
+
const parentTg = bIdx > 0 ? targets.get(c.bones[bIdx - 1]!) : undefined;
|
|
2404
|
+
const startA = parentTg !== undefined ? wrap(tg[i0]! - parentTg[i0]!) : tg[i0]!;
|
|
2405
|
+
const endA = parentTg !== undefined ? wrap(tg[i1]! - parentTg[i1]!) : tg[i1]!;
|
|
2406
|
+
const total = wrap(endA - startA);
|
|
2407
|
+
for (let k = loGap; k < loShow; k++) {
|
|
2408
|
+
const localA = wrap(startA + ((k - i0) / steps) * total);
|
|
2409
|
+
tg[k] = parentTg !== undefined ? wrap(parentTg[k]! + localA) : localA;
|
|
2410
|
+
}
|
|
2305
2411
|
}
|
|
2306
2412
|
}
|
|
2307
2413
|
} else if (c.bones.length >= 2) {
|
|
@@ -2325,6 +2431,18 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2325
2431
|
if (i >= s.frames[0] || outSign * wrap(t0[i]! - pair[0]) < 0) t0[i] = pair[0];
|
|
2326
2432
|
if (i >= s.frames[0] || outSign * wrap(t1[i]! - pair[1]) < 0) t1[i] = pair[1];
|
|
2327
2433
|
}
|
|
2434
|
+
if (loGap <= loShow && loGap >= 1 && loShow + 1 <= hiShow) {
|
|
2435
|
+
const i0 = loGap - 1;
|
|
2436
|
+
const i1 = loShow + 1;
|
|
2437
|
+
const steps = i1 - i0;
|
|
2438
|
+
const startLocal = wrap(t1[i0]! - t0[i0]!);
|
|
2439
|
+
const endLocal = wrap(t1[i1]! - t0[i1]!);
|
|
2440
|
+
const totalLocal = wrap(endLocal - startLocal);
|
|
2441
|
+
for (let k = loGap; k <= loShow; k++) {
|
|
2442
|
+
const localA = wrap(startLocal + ((k - i0) / steps) * totalLocal);
|
|
2443
|
+
t1[k] = wrap(t0[k]! + localA);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2328
2446
|
}
|
|
2329
2447
|
}
|
|
2330
2448
|
}
|
|
@@ -2350,9 +2468,11 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2350
2468
|
}
|
|
2351
2469
|
const solvedKey = (b: string, i: number) => legPoses?.[i]!.solved.get(b);
|
|
2352
2470
|
|
|
2353
|
-
// #190:
|
|
2354
|
-
//
|
|
2355
|
-
|
|
2471
|
+
// #190 / #205: when the neck-length clamp acts on the sequence, couple the clamped
|
|
2472
|
+
// head.translate.y (`clampedY`, the signal the gains were calibrated against) into
|
|
2473
|
+
// head.rotate (+NOD_RESIDUAL_HEAD_GAIN °/unit) and pitch_dial.rotate (+NOD_RESIDUAL_PITCH_GAIN °/unit).
|
|
2474
|
+
const nodCoupled = headNodReading !== null && headNodReading.residual.some((r) => r !== 0) ? headNodReading.clampedY : null;
|
|
2475
|
+
if (headNodReading !== null && nodCoupled !== null) {
|
|
2356
2476
|
const hb = headNodReading.bone;
|
|
2357
2477
|
const tg = targets.get(hb);
|
|
2358
2478
|
if (tg !== undefined) {
|
|
@@ -2361,9 +2481,9 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2361
2481
|
const limits = hc?.range[hb];
|
|
2362
2482
|
const cov = hc !== undefined ? covered.get(hc.name) : undefined;
|
|
2363
2483
|
for (let i = 0; i < n; i++) {
|
|
2364
|
-
const
|
|
2365
|
-
if (
|
|
2366
|
-
let next = wrap(tg[i]! + NOD_RESIDUAL_HEAD_GAIN *
|
|
2484
|
+
const nodY = nodCoupled[i]!;
|
|
2485
|
+
if (nodY === 0) continue;
|
|
2486
|
+
let next = wrap(tg[i]! + NOD_RESIDUAL_HEAD_GAIN * nodY);
|
|
2367
2487
|
if (limits !== undefined && cov?.[i] !== true) {
|
|
2368
2488
|
const key = wrap((next - restOf.get(hb)!) - (above === null ? 0 : turn(above, i)));
|
|
2369
2489
|
if (overflow(key, limits) > RANGE_SLACK) {
|
|
@@ -2410,7 +2530,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
2410
2530
|
const h = heldOf('l_ear');
|
|
2411
2531
|
return 'never' in h ? null : h.at;
|
|
2412
2532
|
})();
|
|
2413
|
-
face = readFace(map.face, series.get('nose')!, series.get('r_eye')!, series.get('l_eye')!, rEar, lEar,
|
|
2533
|
+
face = readFace(map.face, series.get('nose')!, series.get('r_eye')!, series.get('l_eye')!, rEar, lEar, nodCoupled, NOD_RESIDUAL_PITCH_GAIN);
|
|
2414
2534
|
for (const axis of ['yaw', 'pitch'] as const) {
|
|
2415
2535
|
const ax = face[axis];
|
|
2416
2536
|
if (ax === undefined) continue;
|
package/src/run.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* measure where the rig landed, read the folds (and which no switch covers),
|
|
5
5
|
* and write — only when every check passed.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import { tmpdir } from 'node:os';
|
|
9
|
-
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
10
11
|
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
11
12
|
import { cleanLine, inferredSource } from './clean.ts';
|
|
12
13
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
@@ -19,6 +20,7 @@ import { landed, measureFeet, measureLanding, measurePosture, measureShear, pose
|
|
|
19
20
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
20
21
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
21
22
|
import type { BlendHold, Closure, Floor, LegScale, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
23
|
+
import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
|
|
22
24
|
import { bodyLine, faceLine } from './face.ts';
|
|
23
25
|
|
|
24
26
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -82,6 +84,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
82
84
|
}
|
|
83
85
|
args.throwIfAny();
|
|
84
86
|
const input = resolveRig(a.rig, a.baseMotion);
|
|
87
|
+
const rigSnapshot = readReportInput(input.rig);
|
|
85
88
|
const base = baseMotionOf(input);
|
|
86
89
|
const animations = base.animations as Record<string, unknown>;
|
|
87
90
|
if (a.name in animations) refuse([{ code: 'MOTION_NAME_FREE', text: `the rig's motion spec already has an animation "${a.name}" (it has ${Object.keys(animations).join(', ')}); name this one with --name` }]);
|
|
@@ -90,8 +93,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
90
93
|
refuse([{ code: 'IDLE_NAME', text: `${a.idle !== undefined ? `--idle "${idleName}"` : '--carry-idle reads the animation "idle", and'} the rig's motion spec ${input.motion === null ? '— there is none, so it has no animations' : `${input.motion} has no animation "${idleName}" (it has ${Object.keys(animations).join(', ') || 'none'})`}; name the rig's idle with --idle` }]);
|
|
91
94
|
}
|
|
92
95
|
const fragment = a.merge === undefined ? [] : readFragment(parseJson(readFileSync(a.merge, 'utf8'), a.merge, 'MERGE_JSON'), a.merge);
|
|
93
|
-
const
|
|
94
|
-
const
|
|
96
|
+
const mapSnapshot = readReportInput(a.map), poseSnapshot = readReportInput(a.pose);
|
|
97
|
+
const map = readMap(mapSnapshot.bytes.toString('utf8'), a.map);
|
|
98
|
+
const poseText = poseSnapshot.bytes.toString('utf8');
|
|
95
99
|
const seq = a.coco ? convertCoco17(poseText, a.pose) : readPoseSequence(poseText, a.pose);
|
|
96
100
|
if (seq.cleaned === undefined) refuse([{ code: 'POSE_NOT_CLEANED', text: notCleaned(a.pose, a.coco) }]);
|
|
97
101
|
const frames = personTrack(seq, a.person, a.pose);
|
|
@@ -305,48 +309,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
305
309
|
for (const u of uncovered) say(` UNCOVERED mesh "${u.attachment}" (slot "${u.slot}"; chains ${u.chains.length === 0 ? 'none' : u.chains.join(', ')}) folds with no switch covering it on ${u.times.length} frame(s): t=${spans(u.times, 1 / fps)}`);
|
|
306
310
|
}
|
|
307
311
|
|
|
308
|
-
if (a.
|
|
309
|
-
|
|
310
|
-
rmSync(a.build, { recursive: true, force: true });
|
|
311
|
-
const kept = buildInto(input.rig, join(scratch, 'with', 'motion.json'), resolve(a.build), a.profile, undefined, a.images);
|
|
312
|
-
if (!kept.green) refuse([{ code: 'RIG_GATE', text: `rigc build into --build ${a.build} is red where the same build was green in scratch (exit ${kept.run.status}); nothing written` }, ...kept.fails.map((x) => ({ code: 'RIGC', text: x }))]);
|
|
313
|
-
}
|
|
314
|
-
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
315
|
-
if (a.camera !== undefined) {
|
|
316
|
-
const ss = r.figureScale.frames.map((x) => x.s);
|
|
317
|
-
const minS = ss.length === 0 ? 1 : Math.min(...ss);
|
|
318
|
-
const maxS = ss.length === 0 ? 1 : Math.max(...ss);
|
|
319
|
-
writeFileSync(a.camera, `${JSON.stringify({
|
|
320
|
-
spec: 'rig-pose-camera/1',
|
|
321
|
-
rig: input.name,
|
|
322
|
-
animation: a.name,
|
|
323
|
-
frames: r.figureScale.frames.map((f, i) => {
|
|
324
|
-
const cam = seq.camera?.[i];
|
|
325
|
-
const camS = cam?.s ?? 1;
|
|
326
|
-
return {
|
|
327
|
-
frame: f.frame,
|
|
328
|
-
t: f.t,
|
|
329
|
-
s: f.s,
|
|
330
|
-
rawS: f.rawS,
|
|
331
|
-
tx: cam?.tx ?? 0,
|
|
332
|
-
ty: cam?.ty ?? 0,
|
|
333
|
-
videoS: Number((f.s * camS).toFixed(6)),
|
|
334
|
-
};
|
|
335
|
-
}),
|
|
336
|
-
summary: {
|
|
337
|
-
minS,
|
|
338
|
-
maxS,
|
|
339
|
-
maxStep: r.figureScale.maxStep,
|
|
340
|
-
endOverStart: r.figureScale.endOverStart,
|
|
341
|
-
},
|
|
342
|
-
}, null, 2)}\n`);
|
|
343
|
-
}
|
|
344
|
-
if (a.render !== undefined && f.meshes !== null && f.frames !== '') {
|
|
345
|
-
rmSync(a.render, { recursive: true, force: true });
|
|
346
|
-
cpSync(f.frames, a.render, { recursive: true });
|
|
347
|
-
}
|
|
348
|
-
if (a.report !== undefined) {
|
|
349
|
-
writeFileSync(a.report, `${JSON.stringify({
|
|
312
|
+
if (a.report !== undefined) assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
313
|
+
const reportText = a.report === undefined ? null : writeReport({
|
|
350
314
|
spec: 'rig-pose-report/1',
|
|
351
315
|
rigc,
|
|
352
316
|
rig: input.name,
|
|
@@ -403,8 +367,51 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
403
367
|
idleKeptOut: idle === null ? null : idle.keptOut,
|
|
404
368
|
merged: fragment.map((tr) => ({ track: trackLabel(tr, groups), keys: tr.keys.length })),
|
|
405
369
|
reduce: reduced === null ? null : { tolerance: a.reduce, hipTolerance: a.reduceHip ?? null, rows: reduced.rows },
|
|
370
|
+
}, map, { frameTimes: r.times }, f.meshes !== null && doc.slots.length > 0 ? reportRenderDomain(JSON.parse(readFileSync(join(f.frames, '..', 'frames.json'), 'utf8')), a.name) : null, {rig:rigSnapshot.identity,map:mapSnapshot.identity,pose:poseSnapshot.identity});
|
|
371
|
+
if (a.build !== undefined) {
|
|
372
|
+
// Built again where it will live, not copied: rigc's page paths are relative to the build (#18).
|
|
373
|
+
rmSync(a.build, { recursive: true, force: true });
|
|
374
|
+
const kept = buildInto(input.rig, join(scratch, 'with', 'motion.json'), resolve(a.build), a.profile, undefined, a.images);
|
|
375
|
+
if (!kept.green) refuse([{ code: 'RIG_GATE', text: `rigc build into --build ${a.build} is red where the same build was green in scratch (exit ${kept.run.status}); nothing written` }, ...kept.fails.map((x) => ({ code: 'RIGC', text: x }))]);
|
|
376
|
+
}
|
|
377
|
+
// The kept build reads the rig again; its bytes must still match the report.
|
|
378
|
+
if (a.report !== undefined) assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
|
|
379
|
+
writeFileSync(a.out, `${JSON.stringify(motion, null, 2)}\n`);
|
|
380
|
+
if (a.camera !== undefined) {
|
|
381
|
+
const ss = r.figureScale.frames.map((x) => x.s);
|
|
382
|
+
const minS = ss.length === 0 ? 1 : Math.min(...ss);
|
|
383
|
+
const maxS = ss.length === 0 ? 1 : Math.max(...ss);
|
|
384
|
+
writeFileSync(a.camera, `${JSON.stringify({
|
|
385
|
+
spec: 'rig-pose-camera/1',
|
|
386
|
+
rig: input.name,
|
|
387
|
+
animation: a.name,
|
|
388
|
+
frames: r.figureScale.frames.map((f, i) => {
|
|
389
|
+
const cam = seq.camera?.[i];
|
|
390
|
+
const camS = cam?.s ?? 1;
|
|
391
|
+
return {
|
|
392
|
+
frame: f.frame,
|
|
393
|
+
t: f.t,
|
|
394
|
+
s: f.s,
|
|
395
|
+
rawS: f.rawS,
|
|
396
|
+
tx: cam?.tx ?? 0,
|
|
397
|
+
ty: cam?.ty ?? 0,
|
|
398
|
+
videoS: Number((f.s * camS).toFixed(6)),
|
|
399
|
+
};
|
|
400
|
+
}),
|
|
401
|
+
summary: {
|
|
402
|
+
minS,
|
|
403
|
+
maxS,
|
|
404
|
+
maxStep: r.figureScale.maxStep,
|
|
405
|
+
endOverStart: r.figureScale.endOverStart,
|
|
406
|
+
},
|
|
406
407
|
}, null, 2)}\n`);
|
|
407
408
|
}
|
|
409
|
+
if (a.render !== undefined && f.meshes !== null && f.frames !== '') {
|
|
410
|
+
rmSync(a.render, { recursive: true, force: true });
|
|
411
|
+
cpSync(f.frames, a.render, { recursive: true });
|
|
412
|
+
}
|
|
413
|
+
if (a.report !== undefined && reportText !== null) writeFileSync(a.report, reportText);
|
|
414
|
+
|
|
408
415
|
if (a.camera !== undefined) say(` wrote ${a.camera} — camera track (${r.figureScale.frames.length} frames)`);
|
|
409
416
|
say(` wrote ${a.out} — animations: ${Object.keys(motion.animations).join(', ')}`);
|
|
410
417
|
return { lines, written: true };
|
|
@@ -794,3 +801,14 @@ export function notCleaned(pose: string, coco: boolean): string {
|
|
|
794
801
|
const what = coco ? `${pose} is COCO-17 (--coco), which carries no "cleaned" record` : `${pose} has no "cleaned" record`;
|
|
795
802
|
return `${what}; retarget reads a sequence rig-pose clean wrote (spikes, occlusion, smoothing; there is no flag to skip it). Run \`rig-pose clean ${coco ? '--coco ' : ''}--pose ${pose} --out ${out}\` (add --loop for one cycle of a gait) and retarget --pose ${out}`;
|
|
796
803
|
}
|
|
804
|
+
|
|
805
|
+
/** File-bound identities are assembled only in this existing IO boundary. */
|
|
806
|
+
export interface ReportInputSnapshot {file:string;bytes:Buffer;identity:ReportInput;size:number;mtimeMs:number}
|
|
807
|
+
export function readReportInput(file:string):ReportInputSnapshot {
|
|
808
|
+
const path=realpathSync(resolve(file)),before=statSync(path),bytes=readFileSync(path),after=statSync(path);
|
|
809
|
+
if(!before.isFile()||before.size!==after.size||before.mtimeMs!==after.mtimeMs)throw new Refusal([{code:'REPORT_INPUTS',text:`${file}: input changed while reading; expected one stable regular file`}]);
|
|
810
|
+
return {file:resolve(file),bytes,identity:{path:relative(process.cwd(),path).split('\\').join('/'),sha256:createHash('sha256').update(bytes).digest('hex')},size:after.size,mtimeMs:after.mtimeMs};
|
|
811
|
+
}
|
|
812
|
+
export function assertReportInputs(inputs:readonly ReportInputSnapshot[]):void {
|
|
813
|
+
for(const expected of inputs){const current=readReportInput(expected.file);if(current.identity.path!==expected.identity.path||current.identity.sha256!==expected.identity.sha256)throw new Refusal([{code:'REPORT_INPUTS_CHANGED',text:`${expected.identity.path}: consumed input changed before output writes; expected SHA-256 ${expected.identity.sha256}, found ${current.identity.sha256}`}]);}
|
|
814
|
+
}
|
package/src/switch.ts
CHANGED
|
@@ -787,14 +787,14 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
787
787
|
}
|
|
788
788
|
const tight0 = isTightSet(set0, cn);
|
|
789
789
|
const tight1 = isTightSet(set1, cn);
|
|
790
|
-
if (tight0 && set0 !== set1) {
|
|
790
|
+
if (tight0 && set0 !== set1 && g1 - g0 + 1 <= 10) {
|
|
791
791
|
let i = g0;
|
|
792
792
|
while (i <= g1 && rawChainMax(set0, cn, i) <= opts.switchTolerance + hyst) {
|
|
793
793
|
cClamp.get(cn)![i] = set0;
|
|
794
794
|
i++;
|
|
795
795
|
}
|
|
796
796
|
}
|
|
797
|
-
if (tight1 && set0 !== set1) {
|
|
797
|
+
if (tight1 && set0 !== set1 && g1 - g0 + 1 <= 10) {
|
|
798
798
|
let enter1 = g1 + 1;
|
|
799
799
|
for (let i = g0; i <= g1; i++) {
|
|
800
800
|
if (rawChainMax(set1, cn, i) <= opts.switchTolerance) {
|
|
@@ -809,63 +809,53 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
809
809
|
if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
|
-
if (!tight0 && set0 !== set1) {
|
|
813
|
-
const c = map.chains.find((x) => x.name === cn);
|
|
814
|
-
if (c !== undefined) {
|
|
815
|
-
let k = sw0.frames[1];
|
|
816
|
-
while (
|
|
817
|
-
k >= sw0.frames[0] &&
|
|
818
|
-
c.bones.some((b) => {
|
|
819
|
-
const lim = set0.turn[b];
|
|
820
|
-
const dr = set0.drawn[b];
|
|
821
|
-
const ndr = set1.drawn[b];
|
|
822
|
-
if (lim === undefined || dr === undefined || ndr === undefined) return false;
|
|
823
|
-
const t = wrap(targets.get(b)![k]! - dr);
|
|
824
|
-
return overflow(t, lim) > RANGE_SLACK && Math.abs(wrap(targets.get(b)![k + 1]! - ndr)) < Math.abs(wrap(targets.get(b)![k]! - ndr));
|
|
825
|
-
})
|
|
826
|
-
) {
|
|
827
|
-
cClamp.get(cn)![k] = null;
|
|
828
|
-
k--;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
812
|
cPick.set(cn, arr);
|
|
833
813
|
}
|
|
834
|
-
// #187 Rule 1 & Rule 2: when two gesture sets are adjacent (gap <= 10 frames,
|
|
835
|
-
//
|
|
836
|
-
//
|
|
814
|
+
// #187 Rule 1 & Rule 2 / #206: when two gesture sets are adjacent (gap <= 10 frames),
|
|
815
|
+
// unwrap any bone that turned the long way (> 180° through inverted angles in its own joint
|
|
816
|
+
// space — world angle for a chain's root bone, parent-local angle for a child bone) onto the
|
|
817
|
+
// short arc and pick one shared switch frame t_switch where the measured path crosses halfway
|
|
837
818
|
// between set0.drawn and set1.drawn.
|
|
838
|
-
const
|
|
839
|
-
set0 !== set1 &&
|
|
840
|
-
distances.get(set1.name)![g1]! < distances.get(set1.name)![g0]! &&
|
|
841
|
-
distances.get(set0.name)![g1]! > distances.get(set0.name)![g0]!;
|
|
842
|
-
const bridgeDirect = set0 !== set1 && (g1 - g0 + 1 <= 10 || movingTowardNext);
|
|
819
|
+
const bridgeDirect = set0 !== set1 && g1 - g0 + 1 <= 10;
|
|
843
820
|
let tSwitch = g1 + 1;
|
|
844
821
|
if (bridgeDirect && switchingChains.length > 0) {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
822
|
+
for (const cn of switchingChains) {
|
|
823
|
+
const ch = map.chains.find((c) => c.name === cn);
|
|
824
|
+
if (!ch) continue;
|
|
825
|
+
for (let bIdx = 0; bIdx < ch.bones.length; bIdx++) {
|
|
826
|
+
const b = ch.bones[bIdx]!;
|
|
827
|
+
const parentBone = bIdx > 0 ? ch.bones[bIdx - 1] : undefined;
|
|
828
|
+
const tg = targets.get(b) as number[] | undefined;
|
|
829
|
+
const parentTg = parentBone !== undefined ? targets.get(parentBone) : undefined;
|
|
830
|
+
const d0World = set0.drawn[b];
|
|
831
|
+
const d1World = set1.drawn[b];
|
|
832
|
+
if (tg === undefined || d0World === undefined || d1World === undefined) continue;
|
|
833
|
+
const pd0 = parentBone !== undefined ? set0.drawn[parentBone] : undefined;
|
|
834
|
+
const pd1 = parentBone !== undefined ? set1.drawn[parentBone] : undefined;
|
|
835
|
+
const jointAngle = (i: number): number =>
|
|
836
|
+
parentTg !== undefined ? wrap(tg[i]! - parentTg[i]!) : tg[i]!;
|
|
837
|
+
const d0 = pd0 !== undefined ? wrap(d0World - pd0) : d0World;
|
|
838
|
+
const d1 = pd1 !== undefined ? wrap(d1World - pd1) : d1World;
|
|
839
|
+
const i0 = sw0.frames[1];
|
|
840
|
+
const i1 = sw1.frames[0];
|
|
841
|
+
const startA = jointAngle(i0);
|
|
842
|
+
const endA = jointAngle(i1);
|
|
843
|
+
const unwrapped: number[] = [startA];
|
|
844
|
+
for (let i = i0 + 1; i <= i1; i++) {
|
|
845
|
+
unwrapped.push(unwrapped[unwrapped.length - 1]! + wrap(jointAngle(i) - jointAngle(i - 1)));
|
|
846
|
+
}
|
|
847
|
+
const totalTurn = unwrapped[unwrapped.length - 1]! - startA;
|
|
848
|
+
const shortTurn = wrap(endA - startA);
|
|
849
|
+
const invertsBetween = Array.from({ length: g1 - g0 + 1 }, (_, k) => {
|
|
850
|
+
const a = jointAngle(g0 + k);
|
|
851
|
+
return Math.abs(wrap(a - d0)) > 90 && Math.abs(wrap(a - d1)) > 90;
|
|
852
|
+
}).some(Boolean);
|
|
853
|
+
if (Math.abs(totalTurn) > 180 && Math.abs(totalTurn - shortTurn) > 1e-6 && invertsBetween && Math.abs(shortTurn) <= 150) {
|
|
854
|
+
for (let i = g0; i <= g1; i++) {
|
|
855
|
+
const p = (unwrapped[i - i0]! - startA) / totalTurn;
|
|
856
|
+
const localA = wrap(startA + p * shortTurn);
|
|
857
|
+
tg[i] = parentTg !== undefined ? wrap(parentTg[i]! + localA) : localA;
|
|
858
|
+
}
|
|
869
859
|
}
|
|
870
860
|
}
|
|
871
861
|
}
|
|
@@ -904,7 +894,8 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
904
894
|
? pairs.reduce((s, b) => s + Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)), 0) / pairs.length
|
|
905
895
|
: (distances.get(set1.name)?.[i] ?? Infinity);
|
|
906
896
|
const rootsCrossed =
|
|
907
|
-
|
|
897
|
+
switchingChains.length === 1 ||
|
|
898
|
+
rootBones.length === 0 ||
|
|
908
899
|
rootBones.every((b) => Math.abs(wrap(effAngle(b, i) - set1.drawn[b]!)) <= Math.abs(wrap(effAngle(b, i) - set0.drawn[b]!)));
|
|
909
900
|
if ((d1 <= d0 || (span > 1e-6 && d0 >= 0.5 * span)) && rootsCrossed) {
|
|
910
901
|
tSwitch = i;
|
|
@@ -912,6 +903,9 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
912
903
|
}
|
|
913
904
|
}
|
|
914
905
|
}
|
|
906
|
+
const syncBridgeClamp = (cn: string, i: number, picked: AlternateSet) => {
|
|
907
|
+
if (isTightSet(picked, cn)) cClamp.get(cn)![i] = picked;
|
|
908
|
+
};
|
|
915
909
|
const anyHeldBehind = switchingChains.some((cn) => {
|
|
916
910
|
const arr = cPick.get(cn)!;
|
|
917
911
|
for (let i = g0; i <= g1; i++) if (arr[i] === null) return false;
|
|
@@ -924,7 +918,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
924
918
|
});
|
|
925
919
|
if (bridgeDirect) {
|
|
926
920
|
for (const cn of switchingChains) {
|
|
927
|
-
for (let i = g0; i <= g1; i++)
|
|
921
|
+
for (let i = g0; i <= g1; i++) {
|
|
922
|
+
const picked = i < tSwitch ? set0 : set1;
|
|
923
|
+
cPick.get(cn)![i] = picked;
|
|
924
|
+
syncBridgeClamp(cn, i, picked);
|
|
925
|
+
}
|
|
928
926
|
}
|
|
929
927
|
} else if (!frontNear) for (const cn of chains) for (let i = g0; i <= g1; i++) cPick.get(cn)![i] = set0;
|
|
930
928
|
} else {
|
|
@@ -980,7 +978,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
980
978
|
}
|
|
981
979
|
if (bridgeDirect) {
|
|
982
980
|
for (const cn of switchingChains) {
|
|
983
|
-
for (let i = g0; i <= g1; i++)
|
|
981
|
+
for (let i = g0; i <= g1; i++) {
|
|
982
|
+
const picked = i < tSwitch ? set0 : set1;
|
|
983
|
+
cPick.get(cn)![i] = picked;
|
|
984
|
+
syncBridgeClamp(cn, i, picked);
|
|
985
|
+
}
|
|
984
986
|
}
|
|
985
987
|
}
|
|
986
988
|
}
|
|
@@ -1224,7 +1226,7 @@ export const ART_EASE_STEP = 3;
|
|
|
1224
1226
|
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>, chainHolds?: ReadonlyArray<{ set: string; chain: string; frames: [number, number] }>): void {
|
|
1225
1227
|
const boneChain = new Map<string, string>();
|
|
1226
1228
|
for (const c of map.chains) for (const b of c.bones) boneChain.set(b, c.name);
|
|
1227
|
-
const clampSpan = (set: AlternateSet, bone: string, [a,
|
|
1229
|
+
const clampSpan = (set: AlternateSet, bone: string, [a, b]: [number, number]) => {
|
|
1228
1230
|
const limits = set.turn[bone];
|
|
1229
1231
|
if (limits === undefined) return;
|
|
1230
1232
|
const drawn = set.drawn[bone]!;
|
|
@@ -1236,28 +1238,14 @@ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Ma
|
|
|
1236
1238
|
return Math.abs(c - t) > RANGE_SLACK ? c - t : 0;
|
|
1237
1239
|
};
|
|
1238
1240
|
const tightHold = limits[1] - limits[0] <= 10;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (!tightHold && b + 1 < n && delta(b) !== 0) {
|
|
1242
|
-
const nextSw = switches.find((s) => s.frames[0] > b && s.frames[0] - b <= 10 && s.set !== set.name);
|
|
1243
|
-
const nextDrawn = nextSw !== undefined ? map.alternates.find((x) => x.name === nextSw.set)?.drawn[bone] : undefined;
|
|
1244
|
-
if (nextDrawn !== undefined) {
|
|
1245
|
-
const distNext = (i: number) => Math.abs(wrap(target[i]! - nextDrawn));
|
|
1246
|
-
if (distNext(b + 1) < distNext(b)) {
|
|
1247
|
-
while (b >= a && delta(b) !== 0 && (b === a || distNext(b) < distNext(b - 1))) {
|
|
1248
|
-
b--;
|
|
1249
|
-
trimmedExit = true;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
if (a > b) return;
|
|
1241
|
+
const prevNearSw = switches.some((s) => s.frames[1] < a && a - s.frames[1] <= 11);
|
|
1242
|
+
const nextNearSw = switches.some((s) => s.frames[0] > b && s.frames[0] - b <= 11);
|
|
1255
1243
|
const da = delta(a);
|
|
1256
1244
|
const db = delta(b);
|
|
1257
|
-
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da) && !tightHold
|
|
1245
|
+
const easeIn = a > 0 && da !== 0 && Math.sign(delta(a - 1)) === Math.sign(da) && !(tightHold && prevNearSw)
|
|
1258
1246
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(da) / ART_EASE_STEP) - 1)
|
|
1259
1247
|
: 0;
|
|
1260
|
-
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db) && !tightHold &&
|
|
1248
|
+
const easeOut = b + 1 < n && db !== 0 && Math.sign(delta(b + 1)) === Math.sign(db) && !(tightHold && nextNearSw)
|
|
1261
1249
|
? Math.max(ART_EASE_FRAMES, Math.ceil(Math.abs(db) / ART_EASE_STEP) - 1)
|
|
1262
1250
|
: 0;
|
|
1263
1251
|
for (let i = a; i <= b; i++) {
|