|
| 1 | +# L3.2 — Bar-Grip System (Design) |
| 2 | + |
| 3 | +**Date:** 2026-07-11 |
| 4 | +**Status:** Approved (design) |
| 5 | +**Sub-project:** Layer 3, slice 2 of the animation-naturalness program |
| 6 | +**Branch:** `feat/l3-post-ik` (isolated worktree `/Users/aaaa/Developer/posecode-l3`) |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 1. Context and motivation |
| 11 | + |
| 12 | +Pull-up / dead-hang / hanging-knee-raise look broken today (diagnosed earlier): |
| 13 | + |
| 14 | +1. The `bar` prop exposes a **single centre anchor** `(0, barH, 0)` (`props.ts`). `pin: hands bar` |
| 15 | + expands to two hand pins that `applyPins` **averages** into one body translation, so both |
| 16 | + hands are driven toward bar-centre — they converge instead of gripping shoulder-width apart. |
| 17 | +2. `applyPins` only **translates the whole body**; it never bends the arm, so the hands sit |
| 18 | + wherever the authored shoulder/elbow angles put them relative to the body — not on the bar. |
| 19 | +3. **Fingers never wrap** the bar; the flat open palm floats at it. |
| 20 | + |
| 21 | +**Goal:** a `grip` contact that makes each hand actually hold the bar — two shoulder-width grip |
| 22 | +points, per-hand arm IK so each wrist lands on its point while the body hangs/pulls, and a |
| 23 | +procedural finger wrap around the bar. Both sides improve: new `grip` DSL directive + editor |
| 24 | +support; render solvers; updated library moves. |
| 25 | + |
| 26 | +### Non-goals |
| 27 | + |
| 28 | +- Foot-flat (shipped in L3.1). Look-at (L4). Mocap clips (L1). |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## 2. Approach |
| 33 | + |
| 34 | +### 2.1 Two-point bar anchors (`props.ts`) |
| 35 | + |
| 36 | +The `bar` prop gains `bar_left` and `bar_right` anchors at `(±GRIP_HALF, barH, 0)` with |
| 37 | +`GRIP_HALF ≈ 0.18` (shoulder-width grip). The existing centre `bar` anchor stays for |
| 38 | +back-compat. `dip-bars` already has per-rail geometry; its `bars` anchor is unchanged here. |
| 39 | + |
| 40 | +### 2.2 The `grip` DSL directive |
| 41 | + |
| 42 | +New step-child `grip: <effector> <anchor>`, parsed exactly like `pin` (`parser.ts`), producing |
| 43 | +`GripTarget { effector, anchor }`. Resolution (`clamp.ts`) expands `hands` → `hand_left`, |
| 44 | +`hand_right` **and rewrites the anchor per side**: a bare anchor `bar` becomes `bar_left` for |
| 45 | +the left hand and `bar_right` for the right (if those side anchors are declared by the prop); |
| 46 | +a side-specific anchor is used verbatim. So `grip: hands bar` → `[{hand_left,bar_left}, |
| 47 | +{hand_right,bar_right}]`. Stored on `Phase.grips`. |
| 48 | + |
| 49 | +### 2.3 The grip solve (render) |
| 50 | + |
| 51 | +`applyGrips(grips)` in `index.ts`, run in the frame loop where pins run (after ground-lock, |
| 52 | +before the floor clamp), does three things per the diagnosed fix: |
| 53 | + |
| 54 | +1. **Body translate (vertical pull):** like `applyPins`, translate the root by the average |
| 55 | + (anchor − wrist) delta. Authored elbow flex raises the wrists toward the shoulders, so the |
| 56 | + body rises to keep them at the bar — this is what produces the pull-up motion, and it is |
| 57 | + preserved. |
| 58 | +2. **Per-hand arm IK (exact placement + natural angle):** for each grip, `solveCCD` on the arm |
| 59 | + chain `[shoulder, elbow]` (ROM-clamped, reusing the viewer's `reachChain`/`jointLimitsFor`) |
| 60 | + drives that wrist onto its bar anchor. This fixes the shoulder-width placement and angles the |
| 61 | + arms naturally toward the grips instead of straight up. Limits are widened to admit the |
| 62 | + authored angle, so IK closes the residual gap without fighting the pose. |
| 63 | +3. **Finger wrap** (see 2.4). |
| 64 | + |
| 65 | +### 2.4 Procedural finger wrap (`contacts.ts`) |
| 66 | + |
| 67 | +`wrapGrip(m, grips)` curls the fingers of each gripping hand around the bar. For each of the |
| 68 | +four fingers, rotate the knuckle bone about its flex axis by a curl angle derived from the bar |
| 69 | +radius and finger length so the fingertip closes onto the cylinder surface; the thumb opposes |
| 70 | +(curls from the other side). A single tunable `GRIP_CURL` base with per-finger scaling gives a |
| 71 | +believable wrap. This replaces the manual `fingers: flex …` / `thumb: …` lines the current |
| 72 | +pull-up hand-authored. Runs after the arm IK so the hand is already at the bar. |
| 73 | + |
| 74 | +### 2.5 `grip` vs `pin` |
| 75 | + |
| 76 | +`pin` stays for contacts that only translate the body (box step-up, chair dip, dip-bars |
| 77 | +support). `grip` is the bar/rail hold: two-point anchor + arm IK + finger wrap. Keeping them |
| 78 | +separate keeps each directive single-purpose and the editor guidance clear. |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## 3. Components and boundaries |
| 83 | + |
| 84 | +- **`packages/posecode-parser`:** `types.ts` (`GripTarget`, `Phase.grips`); `parser.ts` |
| 85 | + (`AstStep.grips`, parse `grip:` like `pin:`); `schema.ts` (grip array schema); `clamp.ts` |
| 86 | + (expand effector + per-side anchor rewrite); `index.ts` (export `GripTarget`). |
| 87 | +- **`packages/posecode-render`:** `props.ts` (`bar_left`/`bar_right`); `contacts.ts` |
| 88 | + (`wrapGrip`, `GRIP_CURL`); `index.ts` (`applyGrips`, wire into `frame()` + pass `info.grips`; |
| 89 | + `timeline.ts` carries `grips` on keyframes / sample output). |
| 90 | +- **`packages/posecode-language` + `lsp`:** `vocab.ts` (`grip` in `CHILD_KEYWORDS`, |
| 91 | + `KEYWORD_DOCS`); completion already offers child keywords; hover via `KEYWORD_DOCS`; |
| 92 | + tmLanguage keyword; `REACH_EFFECTORS` reused for the effector completion after `grip:`. |
| 93 | +- **Docs:** `pull-up.posecode`, `dead-hang.posecode`, `hanging-knee-raise.posecode` switch |
| 94 | + `pin: hands bar` → `grip: hands bar` and drop the manual finger lines. |
| 95 | + |
| 96 | +### Data flow |
| 97 | + |
| 98 | +``` |
| 99 | +grip: hands bar |
| 100 | + → parser AstStep.grips |
| 101 | + → clamp: expand → [{hand_left,bar_left},{hand_right,bar_right}] |
| 102 | + → Phase.grips → timeline keyframe → sample().grips |
| 103 | + → frame(): applyGrips → body translate (pull) + per-hand arm IK (place) + wrapGrip (fingers) |
| 104 | +``` |
| 105 | + |
| 106 | +## 4. Error handling |
| 107 | + |
| 108 | +- Unknown grip effector / anchor: line-anchored parse error (mirror pin/reach). |
| 109 | +- A `bar` anchor with no `bar_left`/`bar_right` declared (prop absent): fall back to the centre |
| 110 | + `bar` anchor so a malformed doc still resolves rather than crashing. |
| 111 | +- Missing arm bones or unreachable target: `solveCCD` returns the closest ROM-safe pose (existing |
| 112 | + behavior); the body translate still hangs the figure. |
| 113 | + |
| 114 | +## 5. Testing (TDD) |
| 115 | + |
| 116 | +Parser: |
| 117 | +1. `grip: hands bar` resolves to two per-side grips with `bar_left`/`bar_right` anchors. |
| 118 | +2. `grip: hand_left bar_left` verbatim; unknown effector errors with its line. |
| 119 | + |
| 120 | +Render: |
| 121 | +3. `props` bar exposes `bar_left`/`bar_right` at ±GRIP_HALF. |
| 122 | +4. After `applyGrips`, each wrist is within tolerance of its bar anchor (hands land shoulder-width |
| 123 | + on the bar, not at centre). |
| 124 | +5. `wrapGrip` curls the finger bones (finger flex increases from rest) for a gripping hand. |
| 125 | +6. Existing pin/reach/foot-flat tests stay green. |
| 126 | + |
| 127 | +Editor: |
| 128 | +7. `grip` completes as a child keyword and hovers with its doc. |
| 129 | + |
| 130 | +Manual: browser-verify pull-up — hands grip the bar shoulder-width with wrapped fingers, body |
| 131 | +hangs below and rises on the pull; no console errors. |
| 132 | + |
| 133 | +## 6. Risks |
| 134 | + |
| 135 | +- **Arm IK vs authored pull:** IK could over-correct and flatten the pull motion. Mitigation: run |
| 136 | + the body translate first (drives the rise), then IK ROM-clamped+widened to the authored angle, |
| 137 | + so IK only closes the residual placement gap. |
| 138 | +- **Finger wrap tuning:** a fixed curl may over/under-close for the bar radius. Mitigation: derive |
| 139 | + curl from bar radius; keep `GRIP_CURL` a named constant tuned against the live pull-up. |
| 140 | +- **`grips` plumbed through timeline:** mirror exactly how `pins` already flow so no sampling path |
| 141 | + is missed. |
| 142 | + |
| 143 | +## 7. Definition of done |
| 144 | + |
| 145 | +- `grip` parses/resolves to two-point side anchors; render places both hands on the bar with arm |
| 146 | + IK and wraps the fingers; editor supports `grip`. |
| 147 | +- pull-up / dead-hang / hanging-knee-raise use `grip`; browser-verified hands grip the bar. |
| 148 | +- All suites green; typecheck clean. |
0 commit comments