Skip to content

Commit cd5804c

Browse files
committed
docs: L3.2 bar-grip system design spec
1 parent a588e23 commit cd5804c

1 file changed

Lines changed: 148 additions & 0 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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

Comments
 (0)