99
1010import type {
1111 EulerDeg ,
12+ GripTarget ,
1213 JointTarget ,
1314 PosecodeIR ,
1415 ParseError ,
@@ -146,6 +147,21 @@ function resolveStep(
146147 for ( const effector of sides ) pins . push ( { effector, anchor : p . anchor } ) ;
147148 }
148149
150+ // Grip contacts: like pins, but each hand gets its OWN two-point anchor. A
151+ // bare anchor (`bar`) is rewritten per side to `bar_left`/`bar_right` so the
152+ // two hands grip shoulder-width apart; an already-sided anchor is kept as-is.
153+ const grips : GripTarget [ ] = [ ] ;
154+ for ( const g of step . grips ) {
155+ const sides = expandEffector ( g . effector ) ;
156+ if ( sides . length === 0 ) {
157+ errors . push ( { line : g . line , message : `unknown grip effector: "${ g . effector } "` } ) ;
158+ continue ;
159+ }
160+ for ( const effector of sides ) {
161+ grips . push ( { effector, anchor : sideAnchor ( g . anchor , effector ) } ) ;
162+ }
163+ }
164+
149165 // Travel is clamped to a sane studio footprint (±TRAVEL_MAX m) so a stray
150166 // large value can't fling the figure off the ground plane / out of frame.
151167 const travel = step . travel
@@ -163,6 +179,7 @@ function resolveStep(
163179 groundLock : step . groundLock ,
164180 reaches,
165181 pins,
182+ grips,
166183 ...( step . turn !== undefined ? { turnDeg : step . turn } : { } ) ,
167184 ...( travel ? { travel } : { } ) ,
168185 ...( step . cue ? { cue : step . cue } : { } ) ,
@@ -176,6 +193,19 @@ function clampNum(v: number, min: number, max: number): number {
176193 return Math . min ( max , Math . max ( min , v ) ) ;
177194}
178195
196+ /**
197+ * Rewrite a grip anchor to the effector's side: a bare anchor (`bar`) becomes
198+ * `bar_left`/`bar_right` so two hands grip shoulder-width apart. An anchor that
199+ * is already sided, or an effector without a side, is returned unchanged. The
200+ * renderer falls back to the bare anchor if a sided one isn't declared.
201+ */
202+ function sideAnchor ( anchor : string , effector : string ) : string {
203+ if ( / _ ( l e f t | r i g h t ) $ / . test ( anchor ) ) return anchor ;
204+ if ( effector . endsWith ( "_left" ) ) return `${ anchor } _left` ;
205+ if ( effector . endsWith ( "_right" ) ) return `${ anchor } _right` ;
206+ return anchor ;
207+ }
208+
179209function ensure ( map : Map < string , EulerDeg > , bone : string ) : EulerDeg {
180210 let euler = map . get ( bone ) ;
181211 if ( ! euler ) {
0 commit comments